1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 13:29:41 +02:00

refactor(app): move react components to react codebase [EE-3179] (#6971)

This commit is contained in:
Chaim Lev-Ari 2022-06-17 19:18:42 +03:00 committed by GitHub
parent 212400c283
commit 18252ab854
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
346 changed files with 642 additions and 644 deletions

View file

@ -0,0 +1,4 @@
.boxselector_wrapper {
display: flex;
flex-flow: row wrap;
}

View file

@ -0,0 +1,3 @@
.root {
width: 100%;
}

View file

@ -0,0 +1,85 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
import { BoxSelector } from './BoxSelector';
import { BoxSelectorOption } from './types';
const meta: Meta = {
title: 'BoxSelector',
component: BoxSelector,
};
export default meta;
export { Example, LimitedFeature };
function Example() {
const [value, setValue] = useState(3);
const options: BoxSelectorOption<number>[] = [
{
description: 'description 1',
icon: 'fa fa-rocket',
id: '1',
value: 3,
label: 'option 1',
},
{
description: 'description 2',
icon: 'fa fa-rocket',
id: '2',
value: 4,
label: 'option 2',
},
];
return (
<BoxSelector
radioName="name"
onChange={(value: number) => {
setValue(value);
}}
value={value}
options={options}
/>
);
}
function LimitedFeature() {
initFeatureService(Edition.CE);
const [value, setValue] = useState(3);
const options: BoxSelectorOption<number>[] = [
{
description: 'description 1',
icon: 'fa fa-rocket',
id: '1',
value: 3,
label: 'option 1',
},
{
description: 'description 2',
icon: 'fa fa-rocket',
id: '2',
value: 4,
label: 'option 2',
feature: FeatureId.ACTIVITY_AUDIT,
},
];
return (
<BoxSelector
radioName="name"
onChange={(value: number) => {
setValue(value);
}}
value={value}
options={options}
/>
);
}
// regular example
// story with limited feature

View file

@ -0,0 +1,59 @@
import { render, fireEvent } from '@/react-tools/test-utils';
import { BoxSelector, Props } from './BoxSelector';
import { BoxSelectorOption } from './types';
function renderDefault<T extends string | number>({
options = [],
onChange = () => {},
radioName = 'radio',
value,
}: Partial<Props<T>> = {}) {
return render(
<BoxSelector
options={options}
onChange={onChange}
radioName={radioName}
value={value || 0}
/>
);
}
test('should render with the initial value selected and call onChange when clicking a different value', async () => {
const options: BoxSelectorOption<number>[] = [
{
description: 'description 1',
icon: 'fa fa-rocket',
id: '1',
value: 3,
label: 'option 1',
},
{
description: 'description 2',
icon: 'fa fa-rocket',
id: '2',
value: 4,
label: 'option 2',
},
];
const onChange = jest.fn();
const { getByLabelText } = renderDefault({
options,
onChange,
value: options[0].value,
});
const item1 = getByLabelText(options[0].label, {
exact: false,
}) as HTMLInputElement;
expect(item1.checked).toBeTruthy();
const item2 = getByLabelText(options[1].label, {
exact: false,
}) as HTMLInputElement;
expect(item2.checked).toBeFalsy();
fireEvent.click(item2);
expect(onChange).toHaveBeenCalledWith(options[1].value, false);
});

View file

@ -0,0 +1,36 @@
import clsx from 'clsx';
import './BoxSelector.css';
import styles from './BoxSelector.module.css';
import { BoxSelectorItem } from './BoxSelectorItem';
import { BoxSelectorOption } from './types';
export interface Props<T extends number | string> {
radioName: string;
value: T;
onChange(value: T, limitedToBE: boolean): void;
options: BoxSelectorOption<T>[];
}
export function BoxSelector<T extends number | string>({
radioName,
value,
options,
onChange,
}: Props<T>) {
return (
<div className={clsx('boxselector_wrapper', styles.root)} role="radiogroup">
{options.map((option) => (
<BoxSelectorItem
key={option.id}
radioName={radioName}
option={option}
onChange={onChange}
selectedValue={value}
disabled={option.disabled && option.disabled()}
tooltip={option.tooltip && option.tooltip()}
/>
))}
</div>
);
}

View file

@ -0,0 +1,112 @@
.boxselector_wrapper > div,
.boxselector_wrapper box-selector-item {
--selected-item-color: var(--blue-2);
flex: 1;
padding: 5px;
}
.boxselector_wrapper .boxselector_header {
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
user-select: none;
}
.boxselector_header .fa,
.fab {
font-weight: normal;
}
.boxselector_wrapper input[type='radio'] {
display: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label {
cursor: pointer;
background-color: var(--bg-boxselector-wrapper-disabled-color);
}
.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover {
cursor: pointer;
}
.boxselector_wrapper label {
font-weight: normal;
font-size: 12px;
display: block;
background: var(--bg-boxselector-color);
border: 1px solid var(--border-boxselector-color);
border-radius: 2px;
padding: 10px 10px 0 10px;
text-align: center;
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
cursor: not-allowed;
pointer-events: none;
}
.boxselector_wrapper input[type='radio']:checked + label {
background: var(--selected-item-color);
color: white;
padding-top: 20px;
border-color: var(--selected-item-color);
}
.boxselector_wrapper input[type='radio']:checked + label::after {
color: var(--selected-item-color);
font-family: 'Font Awesome 5 Free';
border: 2px solid var(--selected-item-color);
content: '\f00c';
font-size: 16px;
font-weight: bold;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
height: 30px;
width: 30px;
line-height: 26px;
text-align: center;
border-radius: 50%;
background: white;
box-shadow: 0 2px 5px -2px rgba(0, 0, 0, 0.25);
}
@media only screen and (max-width: 700px) {
.boxselector_wrapper {
flex-direction: column;
}
}
.box-selector-item.limited.business {
--selected-item-color: var(--BE-only);
}
.box-selector-item.limited.business label {
border-color: var(--BE-only);
border-width: 2px;
}
.box-selector-item .limited-icon {
position: absolute;
left: 1em;
top: calc(50% - 0.5em);
height: 1em;
}
@media (min-width: 992px) {
.box-selector-item .limited-icon {
left: 2em;
}
}
.box-selector-item.limited.business :checked + label {
color: initial;
}

View file

@ -0,0 +1,77 @@
import { Meta } from '@storybook/react';
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
import { BoxSelectorItem } from './BoxSelectorItem';
import { BoxSelectorOption } from './types';
const meta: Meta = {
title: 'BoxSelector/Item',
args: {
selected: false,
description: 'description',
icon: 'fa-rocket',
label: 'label',
},
};
export default meta;
interface ExampleProps {
selected?: boolean;
description?: string;
icon?: string;
label?: string;
feature?: FeatureId;
}
function Template({
selected,
description = 'description',
icon,
label = 'label',
feature,
}: ExampleProps) {
const option: BoxSelectorOption<number> = {
description,
icon: `fa ${icon}`,
id: 'id',
label,
value: 1,
feature,
};
return (
<div className="boxselector_wrapper">
<BoxSelectorItem
onChange={() => {}}
option={option}
radioName="radio"
selectedValue={selected ? option.value : 0}
/>
</div>
);
}
export const Example = Template.bind({});
export function SelectedItem() {
return <Template selected />;
}
SelectedItem.args = {
selected: true,
};
export function LimitedFeatureItem() {
initFeatureService(Edition.CE);
return <Template feature={FeatureId.ACTIVITY_AUDIT} />;
}
export function SelectedLimitedFeatureItem() {
initFeatureService(Edition.CE);
return <Template feature={FeatureId.ACTIVITY_AUDIT} selected />;
}

View file

@ -0,0 +1,77 @@
import clsx from 'clsx';
import ReactTooltip from 'react-tooltip';
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
import './BoxSelectorItem.css';
import { BoxSelectorOption } from './types';
interface Props<T extends number | string> {
radioName: string;
option: BoxSelectorOption<T>;
onChange(value: T, limitedToBE: boolean): void;
selectedValue: T;
disabled?: boolean;
tooltip?: string;
}
export function BoxSelectorItem<T extends number | string>({
radioName,
option,
onChange,
selectedValue,
disabled,
tooltip,
}: Props<T>) {
const limitedToBE = isLimitedToBE(option.feature);
const tooltipId = `box-selector-item-${radioName}-${option.id}`;
return (
<div
className={clsx('box-selector-item', {
business: limitedToBE,
limited: limitedToBE,
})}
data-tip
data-for={tooltipId}
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
>
<input
type="radio"
name={radioName}
id={option.id}
checked={option.value === selectedValue}
value={option.value}
disabled={disabled}
onChange={() => onChange(option.value, limitedToBE)}
/>
<label htmlFor={option.id} data-cy={`${radioName}_${option.value}`}>
{limitedToBE && <i className="fas fa-briefcase limited-icon" />}
<div className="boxselector_header">
{!!option.icon && (
<i
className={clsx(option.icon, 'space-right')}
aria-hidden="true"
/>
)}
{option.label}
</div>
<p className="box-selector-item-description">{option.description}</p>
</label>
{tooltip && (
<ReactTooltip
place="bottom"
className="portainer-tooltip"
id={tooltipId}
>
{tooltip}
</ReactTooltip>
)}
</div>
);
}

View file

@ -0,0 +1,2 @@
export { BoxSelector } from './BoxSelector';
export type { BoxSelectorOption } from './types';

View file

@ -0,0 +1,12 @@
import type { FeatureId } from '@/portainer/feature-flags/enums';
export interface BoxSelectorOption<T> {
id: string;
icon: string;
label: string;
description: string;
value: T;
disabled?: () => boolean;
tooltip?: () => string;
feature?: FeatureId;
}