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
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer*
|
||||
storybook-static
|
||||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
|
|
|
@ -600,7 +600,7 @@ a[ng-click] {
|
|||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
.tag:not(.token) {
|
||||
padding: 2px 6px;
|
||||
color: white;
|
||||
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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue