feat(app): create react button component [EE-1948] (#6022)

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
pull/6088/head
Marcelo Rydel 2021-11-16 05:33:01 -07:00 committed by GitHub
parent 6b91a813f0
commit 41993ad378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 432 additions and 2 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json

View File

@ -600,7 +600,7 @@ a[ng-click] {
padding-top: 7px;
}
.tag {
.tag:not(.token) {
padding: 2px 6px;
color: white;
background-color: var(--blue-2);

View File

@ -0,0 +1,3 @@
.add-button {
border: none;
}

View File

@ -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!');
},
};

View File

@ -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();
});

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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();
});

View File

@ -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';
}
}

View File

@ -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>
);
}

View File

@ -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();
});

View File

@ -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';
}
}

View File

@ -0,0 +1,7 @@
import { Button } from './Button';
import { AddButton } from './AddButton';
import { ButtonGroup } from './ButtonGroup';
export { Button, AddButton, ButtonGroup };
export default Button;

View File

@ -91,6 +91,7 @@
"bootstrap": "^3.4.0",
"chardet": "^1.3.0",
"chart.js": "~2.7.0",
"clsx": "^1.1.1",
"codemirror": "~5.30.0",
"core-js": "^3.16.3",
"fast-json-patch": "^3.0.0-1",
@ -214,4 +215,4 @@
"pre-commit": "lint-staged"
}
}
}
}