mirror of https://github.com/portainer/portainer
feat(app): create react button component [EE-1948] (#6022)
Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>pull/6088/head
parent
6b91a813f0
commit
41993ad378
|
@ -3,6 +3,7 @@ bower_components
|
||||||
dist
|
dist
|
||||||
portainer-checksum.txt
|
portainer-checksum.txt
|
||||||
api/cmd/portainer/portainer*
|
api/cmd/portainer/portainer*
|
||||||
|
storybook-static
|
||||||
.tmp
|
.tmp
|
||||||
**/.vscode/settings.json
|
**/.vscode/settings.json
|
||||||
**/.vscode/tasks.json
|
**/.vscode/tasks.json
|
||||||
|
|
|
@ -600,7 +600,7 @@ a[ng-click] {
|
||||||
padding-top: 7px;
|
padding-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag:not(.token) {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
color: white;
|
color: white;
|
||||||
background-color: var(--blue-2);
|
background-color: var(--blue-2);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.add-button {
|
||||||
|
border: none;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
|
||||||
|
import { AddButton, Props } from './AddButton';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: AddButton,
|
||||||
|
title: 'Components/Buttons/AddButton',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
function Template({ label, onClick }: JSX.IntrinsicAttributes & Props) {
|
||||||
|
return <AddButton label={label} onClick={onClick} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<Props> = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
label: 'Create new container',
|
||||||
|
onClick: () => {
|
||||||
|
alert('Hello AddButton!');
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { AddButton, Props } from './AddButton';
|
||||||
|
|
||||||
|
function renderDefault({
|
||||||
|
label = 'default label',
|
||||||
|
onClick = () => {},
|
||||||
|
}: Partial<Props> = {}) {
|
||||||
|
return render(<AddButton label={label} onClick={onClick} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should display a AddButton component and allow onClick', async () => {
|
||||||
|
const label = 'test label';
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const { findByText } = renderDefault({ label, onClick });
|
||||||
|
|
||||||
|
const buttonLabel = await findByText(label);
|
||||||
|
expect(buttonLabel).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(buttonLabel);
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import styles from './AddButton.module.css';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddButton({ label, onClick }: Props) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'label',
|
||||||
|
'label-default',
|
||||||
|
'interactive',
|
||||||
|
styles.addButton
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<i className="fa fa-plus-circle space-right" aria-hidden="true" /> {label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { Button, Props } from './Button';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: Button,
|
||||||
|
title: 'Components/Buttons/Button',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
function Template({
|
||||||
|
onClick,
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
disabled,
|
||||||
|
}: JSX.IntrinsicAttributes & PropsWithChildren<Props>) {
|
||||||
|
return (
|
||||||
|
<Button onClick={onClick} color={color} size={size} disabled={disabled}>
|
||||||
|
<i className="fa fa-download" aria-hidden="true" /> Primary Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<PropsWithChildren<Props>> = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
color: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
disabled: false,
|
||||||
|
onClick: () => {
|
||||||
|
alert('Hello Button!');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Disabled() {
|
||||||
|
return (
|
||||||
|
<Button color="primary" onClick={() => {}} disabled>
|
||||||
|
Disabled Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Warning() {
|
||||||
|
return (
|
||||||
|
<Button color="warning" onClick={() => {}}>
|
||||||
|
Warning Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Success() {
|
||||||
|
return (
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
Success Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Danger() {
|
||||||
|
return (
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
Danger Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Default() {
|
||||||
|
return (
|
||||||
|
<Button color="default" onClick={() => {}}>
|
||||||
|
<i className="fa fa-plus-circle" aria-hidden="true" /> Add an environment
|
||||||
|
variable
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link() {
|
||||||
|
return (
|
||||||
|
<Button color="link" onClick={() => {}}>
|
||||||
|
Link Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function XSmall() {
|
||||||
|
return (
|
||||||
|
<Button color="primary" onClick={() => {}} size="xsmall">
|
||||||
|
XSmall Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Small() {
|
||||||
|
return (
|
||||||
|
<Button color="primary" onClick={() => {}} size="small">
|
||||||
|
Small Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Large() {
|
||||||
|
return (
|
||||||
|
<Button color="primary" onClick={() => {}} size="large">
|
||||||
|
Large Button
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { Button, Props } from './Button';
|
||||||
|
|
||||||
|
function renderDefault({
|
||||||
|
type = 'button',
|
||||||
|
color = 'primary',
|
||||||
|
size = 'small',
|
||||||
|
disabled = false,
|
||||||
|
onClick = () => {},
|
||||||
|
children = null,
|
||||||
|
}: Partial<PropsWithChildren<Props>> = {}) {
|
||||||
|
return render(
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
color={color}
|
||||||
|
size={size}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should display a Button component and allow onClick', async () => {
|
||||||
|
const children = 'test label';
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const { findByText } = renderDefault({ children, onClick });
|
||||||
|
|
||||||
|
const buttonLabel = await findByText(children);
|
||||||
|
expect(buttonLabel).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(buttonLabel);
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
type Type = 'submit' | 'reset' | 'button';
|
||||||
|
type Color = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'link';
|
||||||
|
type Size = 'xsmall' | 'small' | 'large';
|
||||||
|
export interface Props {
|
||||||
|
type?: Type;
|
||||||
|
color?: Color;
|
||||||
|
size?: Size;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
type = 'button',
|
||||||
|
color = 'primary',
|
||||||
|
size = 'small',
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<Props>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
/* eslint-disable-next-line react/button-has-type */
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
className={clsx('btn', `btn-${color}`, sizeClass(size))}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeClass(size?: Size) {
|
||||||
|
switch (size) {
|
||||||
|
case 'large':
|
||||||
|
return 'btn-lg';
|
||||||
|
case 'xsmall':
|
||||||
|
return 'btn-xs';
|
||||||
|
default:
|
||||||
|
return 'btn-sm';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { ButtonGroup, Props } from './ButtonGroup';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: ButtonGroup,
|
||||||
|
title: 'Components/Buttons/ButtonGroup',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
function Template({
|
||||||
|
size,
|
||||||
|
}: JSX.IntrinsicAttributes & PropsWithChildren<Props>) {
|
||||||
|
return (
|
||||||
|
<ButtonGroup size={size}>
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
<i className="fa fa-stop space-right" aria-hidden="true" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
<i className="fa fa-bomb space-right" aria-hidden="true" />
|
||||||
|
Kill
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onClick={() => {}}>
|
||||||
|
<i className="fa fa-sync space-right" aria-hidden="true" />
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" disabled onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Resume
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
<i className="fa fa-trash-alt space-right" aria-hidden="true" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<PropsWithChildren<Props>> = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
size: 'small',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Xsmall() {
|
||||||
|
return (
|
||||||
|
<ButtonGroup size="xsmall">
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
<i className="fa fa-stop space-right" aria-hidden="true" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onClick={() => {}}>
|
||||||
|
<i className="fa fa-sync space-right" aria-hidden="true" />
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Small() {
|
||||||
|
return (
|
||||||
|
<ButtonGroup size="small">
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
<i className="fa fa-stop space-right" aria-hidden="true" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onClick={() => {}}>
|
||||||
|
<i className="fa fa-sync space-right" aria-hidden="true" />
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Large() {
|
||||||
|
return (
|
||||||
|
<ButtonGroup size="large">
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="danger" onClick={() => {}}>
|
||||||
|
<i className="fa fa-stop space-right" aria-hidden="true" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
<Button color="success" onClick={() => {}}>
|
||||||
|
<i className="fa fa-play space-right" aria-hidden="true" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onClick={() => {}}>
|
||||||
|
<i className="fa fa-sync space-right" aria-hidden="true" />
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { ButtonGroup, Props } from './ButtonGroup';
|
||||||
|
|
||||||
|
function renderDefault({
|
||||||
|
size = 'small',
|
||||||
|
children = 'null',
|
||||||
|
}: Partial<PropsWithChildren<Props>> = {}) {
|
||||||
|
return render(<ButtonGroup size={size}>{children}</ButtonGroup>);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should display a ButtonGroup component', async () => {
|
||||||
|
const { findByRole } = renderDefault({});
|
||||||
|
|
||||||
|
const element = await findByRole('group');
|
||||||
|
expect(element).toBeTruthy();
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
type Size = 'xsmall' | 'small' | 'large';
|
||||||
|
export interface Props {
|
||||||
|
size?: Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ButtonGroup({
|
||||||
|
size = 'small',
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<Props>) {
|
||||||
|
return (
|
||||||
|
<div className={clsx('btn-group', sizeClass(size))} role="group">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeClass(size: Size | undefined) {
|
||||||
|
switch (size) {
|
||||||
|
case 'xsmall':
|
||||||
|
return 'btn-group-xs';
|
||||||
|
case 'large':
|
||||||
|
return 'btn-group-lg';
|
||||||
|
default:
|
||||||
|
return 'btn-group-sm';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { AddButton } from './AddButton';
|
||||||
|
import { ButtonGroup } from './ButtonGroup';
|
||||||
|
|
||||||
|
export { Button, AddButton, ButtonGroup };
|
||||||
|
|
||||||
|
export default Button;
|
|
@ -91,6 +91,7 @@
|
||||||
"bootstrap": "^3.4.0",
|
"bootstrap": "^3.4.0",
|
||||||
"chardet": "^1.3.0",
|
"chardet": "^1.3.0",
|
||||||
"chart.js": "~2.7.0",
|
"chart.js": "~2.7.0",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
"codemirror": "~5.30.0",
|
"codemirror": "~5.30.0",
|
||||||
"core-js": "^3.16.3",
|
"core-js": "^3.16.3",
|
||||||
"fast-json-patch": "^3.0.0-1",
|
"fast-json-patch": "^3.0.0-1",
|
||||||
|
|
Loading…
Reference in New Issue