feat(app): introduce form framework [EE-1946] (#6272)

pull/6259/head
Chaim Lev-Ari 2021-12-20 19:21:19 +02:00 committed by GitHub
parent c5fe994cd2
commit 4f7b432f44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 815 additions and 339 deletions

View File

@ -6,6 +6,7 @@ import settingsModule from './settings';
import featureFlagModule from './feature-flags';
import userActivityModule from './user-activity';
import servicesModule from './services';
import teamsModule from './teams';
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
@ -32,6 +33,7 @@ angular
userActivityModule,
'portainer.shared.datatable',
servicesModule,
teamsModule,
])
.config([
'$stateRegistryProvider',

View File

@ -1,17 +1,19 @@
import { PropsWithChildren } from 'react';
import clsx from 'clsx';
type Type = 'submit' | 'reset' | 'button';
type Type = 'submit' | 'button' | 'reset';
type Color = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'link';
type Size = 'xsmall' | 'small' | 'medium' | 'large';
export interface Props {
type?: Type;
color?: Color;
size?: Size;
disabled?: boolean;
title?: string;
className?: string;
onClick: () => void;
dataCy?: string;
type?: Type;
onClick?: () => void;
}
export function Button({
@ -20,12 +22,14 @@ export function Button({
size = 'small',
disabled = false,
className,
dataCy,
onClick,
title,
children,
}: PropsWithChildren<Props>) {
return (
<button
data-cy={dataCy}
/* eslint-disable-next-line react/button-has-type */
type={type}
disabled={disabled}

View File

@ -0,0 +1,36 @@
import { Meta } from '@storybook/react';
import { LoadingButton } from './LoadingButton';
export default {
component: LoadingButton,
title: 'Components/Buttons/LoadingButton',
} as Meta;
interface Args {
loadingText: string;
isLoading: boolean;
}
function Template({ loadingText, isLoading }: Args) {
return (
<LoadingButton loadingText={loadingText} isLoading={isLoading}>
<i className="fa fa-download" aria-hidden="true" /> Download
</LoadingButton>
);
}
Template.args = {
loadingText: 'loading',
isLoading: false,
};
export const Example = Template.bind({});
export function IsLoading() {
return (
<LoadingButton loadingText="loading" isLoading>
<i className="fa fa-download" aria-hidden="true" /> Download
</LoadingButton>
);
}

View File

@ -0,0 +1,43 @@
import { render } from '@/react-tools/test-utils';
import { LoadingButton } from './LoadingButton';
test('when isLoading is true should show spinner and loading text', async () => {
const loadingText = 'loading';
const children = 'not visible';
const { findByLabelText, queryByText, findByText } = render(
<LoadingButton loadingText={loadingText} isLoading>
{children}
</LoadingButton>
);
const buttonLabel = queryByText(children);
expect(buttonLabel).toBeNull();
const spinner = await findByLabelText('loading');
expect(spinner).toBeVisible();
const loadingTextElem = await findByText(loadingText);
expect(loadingTextElem).toBeVisible();
});
test('should show children when false', async () => {
const loadingText = 'loading';
const children = 'visible';
const { queryByLabelText, queryByText } = render(
<LoadingButton loadingText={loadingText} isLoading={false}>
{children}
</LoadingButton>
);
const buttonLabel = queryByText(children);
expect(buttonLabel).toBeVisible();
const spinner = queryByLabelText('loading');
expect(spinner).toBeNull();
const loadingTextElem = queryByText(loadingText);
expect(loadingTextElem).toBeNull();
});

View File

@ -0,0 +1,39 @@
import { PropsWithChildren } from 'react';
import { type Props as ButtonProps, Button } from './Button';
interface Props extends ButtonProps {
loadingText: string;
isLoading: boolean;
}
export function LoadingButton({
loadingText,
isLoading,
disabled,
type = 'submit',
children,
...buttonProps
}: PropsWithChildren<Props>) {
return (
<Button
// eslint-disable-next-line react/jsx-props-no-spreading
{...buttonProps}
type={type}
disabled={disabled || isLoading}
>
{isLoading ? (
<>
<i
className="fa fa-circle-notch fa-spin space-right"
aria-label="loading"
aria-hidden="true"
/>
{loadingText}
</>
) : (
children
)}
</Button>
);
}

View File

@ -0,0 +1,31 @@
import { UserViewModel } from '@/portainer/models/user';
export function createMockUser(id: number, username: string): UserViewModel {
return {
Id: id,
Username: username,
Role: 2,
UserTheme: '',
EndpointAuthorizations: {},
PortainerAuthorizations: {
PortainerDockerHubInspect: true,
PortainerEndpointExtensionAdd: true,
PortainerEndpointExtensionRemove: true,
PortainerEndpointGroupInspect: true,
PortainerEndpointGroupList: true,
PortainerEndpointInspect: true,
PortainerEndpointList: true,
PortainerMOTD: true,
PortainerRoleList: true,
PortainerTeamList: true,
PortainerTemplateInspect: true,
PortainerTemplateList: true,
PortainerUserInspect: true,
PortainerUserList: true,
PortainerUserMemberships: true,
},
RoleName: 'user',
Checked: false,
AuthenticationMethod: '',
};
}

View File

@ -0,0 +1,27 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { UsersSelector } from './UsersSelector';
import { createMockUser } from './UsersSelector.mocks';
const meta: Meta = {
title: 'Components/UsersSelector',
component: UsersSelector,
};
export default meta;
export function Example() {
const [selectedUsers, setSelectedUsers] = useState([10]);
const users = [createMockUser(1, 'user1'), createMockUser(2, 'user2')];
return (
<UsersSelector
value={selectedUsers}
onChange={setSelectedUsers}
users={users}
placeholder="Select one or more users"
/>
);
}

View File

@ -0,0 +1,40 @@
import Select from 'react-select';
import { UserViewModel } from '@/portainer/models/user';
type UserId = number;
interface Props {
value: UserId[];
onChange(value: UserId[]): void;
users: UserViewModel[];
dataCy?: string;
inputId?: string;
placeholder?: string;
}
export function UsersSelector({
value,
onChange,
users,
dataCy,
inputId,
placeholder,
}: Props) {
return (
<Select
isMulti
getOptionLabel={(user) => user.Username}
getOptionValue={(user) => user.Id}
options={users}
value={users.filter((user) => value.includes(user.Id))}
closeMenuOnSelect={false}
onChange={(selectedUsers) =>
onChange(selectedUsers.map((user) => user.Id))
}
data-cy={dataCy}
inputId={inputId}
placeholder={placeholder}
/>
);
}

View File

@ -0,0 +1 @@
export { UsersSelector } from './UsersSelector';

View File

@ -1,8 +1,5 @@
.container {
display: flex;
align-items: center;
}
.space-right {
margin-right: 1rem;
width: 100%;
}

View File

@ -1,7 +1,7 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { TextInput, Select } from '../Input';
import { Input, Select } from '../Input';
import { FormControl } from './FormControl';
@ -21,7 +21,12 @@ function TextField({ label, tooltip = '' }: TextFieldProps) {
const inputId = 'input';
return (
<FormControl inputId={inputId} label={label} tooltip={tooltip}>
<TextInput id={inputId} type="text" value={value} onChange={setValue} />
<Input
id={inputId}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</FormControl>
);
}
@ -43,7 +48,7 @@ function SelectField({ label, tooltip = '' }: TextFieldProps) {
<Select
className="form-control"
value={value}
onChange={(value) => setValue(value)}
onChange={(e) => setValue(parseInt(e.target.value, 10))}
options={options}
/>
</FormControl>

View File

@ -1,41 +0,0 @@
import clsx from 'clsx';
import { HTMLInputTypeAttribute } from 'react';
import { InputProps } from './types';
interface Props extends InputProps {
type?: HTMLInputTypeAttribute;
onChange(value: string): void;
value: number | string;
component?: 'input' | 'textarea';
rows?: number;
readonly?: boolean;
}
export function BaseInput({
component = 'input',
value,
disabled,
id,
readonly,
required,
type,
className,
rows,
onChange,
}: Props) {
const Component = component;
return (
<Component
value={value}
disabled={disabled}
id={id}
readOnly={readonly}
required={required}
type={type}
className={clsx('form-control', className)}
onChange={(e) => onChange(e.target.value)}
rows={rows}
/>
);
}

View File

@ -1,10 +1,10 @@
import { Meta, Story } from '@storybook/react';
import { useState } from 'react';
import { TextInput } from './TextInput';
import { Input } from './Input';
export default {
title: 'Components/Form/TextInput',
title: 'Components/Form/Input',
args: {
disabled: false,
},
@ -16,7 +16,14 @@ interface Args {
export function TextField({ disabled }: Args) {
const [value, setValue] = useState('');
return <TextInput value={value} onChange={setValue} disabled={disabled} />;
return (
<Input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
disabled={disabled}
/>
);
}
export const DisabledTextField: Story<Args> = TextField.bind({});

View File

@ -0,0 +1,15 @@
import clsx from 'clsx';
import { InputHTMLAttributes } from 'react';
export function Input({
className,
...props
}: InputHTMLAttributes<HTMLInputElement>) {
return (
<input
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
className={clsx('form-control', className)}
/>
);
}

View File

@ -1,25 +0,0 @@
import { Meta, Story } from '@storybook/react';
import { useState } from 'react';
import { NumberInput } from './NumberInput';
export default {
title: 'Components/Form/NumberInput',
args: {
disabled: false,
},
} as Meta;
interface Args {
disabled?: boolean;
}
export function Example({ disabled }: Args) {
const [value, setValue] = useState(0);
return <NumberInput value={value} onChange={setValue} disabled={disabled} />;
}
export const DisabledNumberInput: Story<Args> = Example.bind({});
DisabledNumberInput.args = {
disabled: true,
};

View File

@ -1,35 +0,0 @@
import clsx from 'clsx';
import { BaseInput } from './BaseInput';
import { InputProps } from './types';
interface Props extends InputProps {
value: number;
readonly?: boolean;
onChange(value: number): void;
}
export function NumberInput({
disabled,
required,
id,
value,
className,
readonly,
placeholder,
onChange,
}: Props) {
return (
<BaseInput
id={id}
type="number"
className={clsx(className, 'form-control')}
value={value}
disabled={disabled}
readonly={readonly}
required={required}
placeholder={placeholder}
onChange={(value) => onChange(parseFloat(value))}
/>
);
}

View File

@ -21,9 +21,9 @@ export function Example({ disabled }: Args) {
{ value: 2, label: 'two' },
];
return (
<Select<number>
<Select
value={value}
onChange={setValue}
onChange={(e) => setValue(parseInt(e.target.value, 10))}
disabled={disabled}
options={options}
/>

View File

@ -1,36 +1,25 @@
import clsx from 'clsx';
import { FormEvent } from 'react';
import { ChangeProps, InputProps } from './types';
import { SelectHTMLAttributes } from 'react';
interface Option<T extends string | number> {
value: T;
label: string;
}
interface Props<T extends string | number> extends InputProps, ChangeProps<T> {
interface Props<T extends string | number> {
options: Option<T>[];
}
export function Select<T extends number | string>({
options,
onChange,
value,
className,
disabled,
id,
required,
placeholder,
}: Props<T>) {
...props
}: Props<T> & SelectHTMLAttributes<HTMLSelectElement>) {
return (
<select
value={value}
disabled={disabled}
id={id}
required={required}
className={clsx(className, 'form-control')}
placeholder={placeholder}
onChange={handleChange}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
className={clsx('form-control', className)}
>
{options.map((item) => (
<option value={item.value} key={item.value}>
@ -39,10 +28,4 @@ export function Select<T extends number | string>({
))}
</select>
);
function handleChange(e: FormEvent<HTMLSelectElement>) {
const { selectedIndex } = e.currentTarget;
const option = options[selectedIndex];
onChange(option.value);
}
}

View File

@ -1,35 +0,0 @@
import { HTMLInputTypeAttribute } from 'react';
import { BaseInput } from './BaseInput';
import { ChangeProps, InputProps } from './types';
interface TextInputProps extends InputProps, ChangeProps<string> {
type?: HTMLInputTypeAttribute;
readonly?: boolean;
}
export function TextInput({
id,
type = 'text',
value,
className,
onChange,
disabled,
readonly,
required,
placeholder,
}: TextInputProps) {
return (
<BaseInput
id={id}
type={type}
className={className}
value={value}
onChange={onChange}
disabled={disabled}
readonly={readonly}
required={required}
placeholder={placeholder}
/>
);
}

View File

@ -1,31 +1,15 @@
import { BaseInput } from './BaseInput';
import { ChangeProps, InputProps } from './types';
import clsx from 'clsx';
import { TextareaHTMLAttributes } from 'react';
interface Props extends InputProps, ChangeProps<string> {
rows?: number;
}
export function Textarea({
rows,
export function TextArea({
className,
onChange,
value,
id,
placeholder,
disabled,
required,
}: Props & InputProps) {
...props
}: TextareaHTMLAttributes<HTMLTextAreaElement>) {
return (
<BaseInput
component="textarea"
id={id}
rows={rows}
className={className}
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
required={required}
<textarea
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
className={clsx('form-control', className)}
/>
);
}

View File

@ -1,3 +1,2 @@
export { NumberInput } from './NumberInput';
export { TextInput } from './TextInput';
export { Input } from './Input';
export { Select } from './Select';

View File

@ -1,12 +0,0 @@
export interface InputProps {
id?: string;
className?: string;
required?: boolean;
disabled?: boolean;
placeholder?: string;
}
export interface ChangeProps<T> {
value: T;
onChange(value: T): void;
}

View File

@ -20,7 +20,7 @@ function BasicExample() {
<InputGroup.Addon>@</InputGroup.Addon>
<InputGroup.Input
value={value1}
onChange={setValue1}
onChange={(e) => setValue1(e.target.value)}
placeholder="Username"
aria-describedby="basic-addon1"
/>
@ -29,7 +29,7 @@ function BasicExample() {
<InputGroup>
<InputGroup.Input
value={value1}
onChange={setValue1}
onChange={(e) => setValue1(e.target.value)}
placeholder="Recipient's username"
aria-describedby="basic-addon2"
/>
@ -38,9 +38,10 @@ function BasicExample() {
<InputGroup>
<InputGroup.Addon>$</InputGroup.Addon>
<InputGroup.NumberInput
<InputGroup.Input
type="number"
value={valueNumber}
onChange={setValueNumber}
onChange={(e) => setValueNumber(parseInt(e.target.value, 10))}
aria-label="Amount (to the nearest dollar)"
/>
<InputGroup.Addon>.00</InputGroup.Addon>
@ -51,7 +52,7 @@ function BasicExample() {
<InputGroup.Addon>https://example.com/users/</InputGroup.Addon>
<InputGroup.Input
value={value1}
onChange={setValue1}
onChange={(e) => setValue1(e.target.value)}
id="basic-url"
aria-describedby="basic-addon3"
/>
@ -72,12 +73,18 @@ function Addons() {
Go!
</button>
</InputGroup.ButtonWrapper>
<InputGroup.Input value={value1} onChange={setValue1} />
<InputGroup.Input
value={value1}
onChange={(e) => setValue1(e.target.value)}
/>
</InputGroup>
</div>
<div className="col-lg-6">
<InputGroup>
<InputGroup.Input value={value2} onChange={setValue2} />
<InputGroup.Input
value={value2}
onChange={(e) => setValue2(e.target.value)}
/>
<InputGroup.Addon>
<input type="checkbox" />
</InputGroup.Addon>
@ -93,17 +100,26 @@ function Sizing() {
<div className="space-y-8">
<InputGroup size="small">
<InputGroup.Addon>Small</InputGroup.Addon>
<InputGroup.Input value={value} onChange={setValue} />
<InputGroup.Input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</InputGroup>
<InputGroup>
<InputGroup.Addon>Default</InputGroup.Addon>
<InputGroup.Input value={value} onChange={setValue} />
<InputGroup.Input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</InputGroup>
<InputGroup size="large">
<InputGroup.Addon>Large</InputGroup.Addon>
<InputGroup.Input value={value} onChange={setValue} />
<InputGroup.Input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</InputGroup>
</div>
);

View File

@ -1,4 +1,4 @@
import { NumberInput, TextInput } from '../Input';
import { Input } from '../Input';
import { InputGroup as MainComponent } from './InputGroup';
import { InputGroupAddon } from './InputGroupAddon';
@ -7,15 +7,13 @@ import { InputGroupButtonWrapper } from './InputGroupButtonWrapper';
interface InputGroupSubComponents {
Addon: typeof InputGroupAddon;
ButtonWrapper: typeof InputGroupButtonWrapper;
Input: typeof TextInput;
NumberInput: typeof NumberInput;
Input: typeof Input;
}
const InputGroup: typeof MainComponent & InputGroupSubComponents = MainComponent as typeof MainComponent & InputGroupSubComponents;
InputGroup.Addon = InputGroupAddon;
InputGroup.ButtonWrapper = InputGroupButtonWrapper;
InputGroup.Input = TextInput;
InputGroup.NumberInput = NumberInput;
InputGroup.Input = Input;
export { InputGroup };

View File

@ -1,12 +1,12 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { NumberInput, Select } from '../Input';
import { Input, Select } from '../Input';
import { DefaultType, InputList } from './InputList';
const meta: Meta = {
title: 'InputList',
title: 'Components/Form/InputList',
component: InputList,
};
@ -75,12 +75,15 @@ function SelectAndInputItem({
}) {
return (
<div>
<NumberInput
<Input
type="number"
value={item.value}
onChange={(value: number) => onChange({ ...item, value })}
onChange={(e) =>
onChange({ ...item, value: parseInt(e.target.value, 10) })
}
/>
<Select
onChange={(select: string) => onChange({ ...item, select })}
onChange={(e) => onChange({ ...item, select: e.target.value })}
options={[
{ label: 'option1', value: 'option1' },
{ label: 'option2', value: 'option2' },

View File

@ -4,7 +4,7 @@ import clsx from 'clsx';
import { AddButton, Button } from '@/portainer/components/Button';
import { Tooltip } from '@/portainer/components/Tip/Tooltip';
import { TextInput } from '../Input';
import { Input } from '../Input';
import styles from './InputList.module.css';
import { arrayMove } from './utils';
@ -174,9 +174,9 @@ function defaultItemBuilder(): DefaultType {
function DefaultItem({ item, onChange }: ItemProps<DefaultType>) {
return (
<TextInput
<Input
value={item.value}
onChange={(value: string) => onChange({ value })}
onChange={(e) => onChange({ value: e.target.value })}
className={styles.defaultItem}
/>
);

View File

@ -11,6 +11,7 @@ export function UserViewModel(data) {
this.AuthenticationMethod = data.AuthenticationMethod;
this.Checked = false;
this.EndpointAuthorizations = null;
this.PortainerAuthorizations = null;
}
export function UserTokenModel(data) {

View File

@ -0,0 +1,76 @@
import { TeamViewModel } from '@/portainer/models/team';
import { UserViewModel } from '@/portainer/models/user';
export function mockExampleData() {
const teams: TeamViewModel[] = [
{
Id: 3,
Name: 'Team 1',
Checked: false,
},
{
Id: 4,
Name: 'Team 2',
Checked: false,
},
];
const users: UserViewModel[] = [
{
Id: 10,
Username: 'user1',
Role: 2,
UserTheme: '',
EndpointAuthorizations: {},
PortainerAuthorizations: {
PortainerDockerHubInspect: true,
PortainerEndpointExtensionAdd: true,
PortainerEndpointExtensionRemove: true,
PortainerEndpointGroupInspect: true,
PortainerEndpointGroupList: true,
PortainerEndpointInspect: true,
PortainerEndpointList: true,
PortainerMOTD: true,
PortainerRoleList: true,
PortainerTeamList: true,
PortainerTemplateInspect: true,
PortainerTemplateList: true,
PortainerUserInspect: true,
PortainerUserList: true,
PortainerUserMemberships: true,
},
RoleName: 'user',
Checked: false,
AuthenticationMethod: '',
},
{
Id: 13,
Username: 'user2',
Role: 2,
UserTheme: '',
EndpointAuthorizations: {},
PortainerAuthorizations: {
PortainerDockerHubInspect: true,
PortainerEndpointExtensionAdd: true,
PortainerEndpointExtensionRemove: true,
PortainerEndpointGroupInspect: true,
PortainerEndpointGroupList: true,
PortainerEndpointInspect: true,
PortainerEndpointList: true,
PortainerMOTD: true,
PortainerRoleList: true,
PortainerTeamList: true,
PortainerTemplateInspect: true,
PortainerTemplateList: true,
PortainerUserInspect: true,
PortainerUserList: true,
PortainerUserMemberships: true,
},
RoleName: 'user',
Checked: false,
AuthenticationMethod: '',
},
];
return { users, teams };
}

View File

@ -0,0 +1,35 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { CreateTeamForm, FormValues } from './CreateTeamForm';
import { mockExampleData } from './CreateTeamForm.mocks';
const meta: Meta = {
title: 'teams/CreateTeamForm',
component: CreateTeamForm,
};
export default meta;
export function Example() {
const [message, setMessage] = useState('');
const { teams, users } = mockExampleData();
return (
<div>
<CreateTeamForm users={users} teams={teams} onSubmit={handleSubmit} />
<div>{message}</div>
</div>
);
function handleSubmit(values: FormValues) {
return new Promise<void>((resolve) => {
setTimeout(() => {
setMessage(
`created team ${values.name} with ${values.leaders.length} leaders`
);
resolve();
}, 3000);
});
}
}

View File

@ -0,0 +1,28 @@
import userEvent from '@testing-library/user-event';
import { render, waitFor } from '@/react-tools/test-utils';
import { CreateTeamForm } from './CreateTeamForm';
test('filling the name should make the submit button clickable and emptying it should make it disabled', async () => {
const { findByLabelText, findByText } = render(
<CreateTeamForm users={[]} teams={[]} onSubmit={() => {}} />
);
const button = await findByText('Create team');
expect(button).toBeVisible();
const nameField = await findByLabelText('Name');
expect(nameField).toBeVisible();
expect(nameField).toHaveDisplayValue('');
expect(button).toBeDisabled();
const newValue = 'name';
userEvent.type(nameField, newValue);
await waitFor(() => {
expect(nameField).toHaveDisplayValue(newValue);
expect(button).toBeEnabled();
});
});

View File

@ -0,0 +1,114 @@
import { Formik, Field, Form } from 'formik';
import { FormControl } from '@/portainer/components/form-components/FormControl';
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
import { UserViewModel } from '@/portainer/models/user';
import { TeamViewModel } from '@/portainer/models/team';
import { Input } from '@/portainer/components/form-components/Input';
import { UsersSelector } from '@/portainer/components/UsersSelector';
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
import { validationSchema } from './CreateTeamForm.validation';
export interface FormValues {
name: string;
leaders: number[];
}
interface Props {
users: UserViewModel[];
teams: TeamViewModel[];
onSubmit(values: FormValues): void;
}
export function CreateTeamForm({ users, teams, onSubmit }: Props) {
const initialValues = {
name: '',
leaders: [],
};
return (
<div className="row">
<div className="col-lg-12 col-md-12 col-xs-12">
<Widget>
<WidgetTitle icon="fa-plus" title="Add a new team" />
<WidgetBody>
<Formik
initialValues={initialValues}
validationSchema={() => validationSchema(teams)}
onSubmit={onSubmit}
validateOnMount
>
{({
values,
errors,
handleSubmit,
setFieldValue,
isSubmitting,
isValid,
}) => (
<Form
className="form-horizontal"
onSubmit={handleSubmit}
noValidate
>
<FormControl
inputId="team_name"
label="Name"
errors={errors.name}
>
<Field
as={Input}
name="name"
id="team_name"
required
placeholder="e.g. development"
data-cy="team-teamNameInput"
/>
</FormControl>
{users.length > 0 && (
<FormControl
inputId="users-input"
label="Select team leader(s)"
tooltip="You can assign one or more leaders to this team. Team leaders can manage their teams users and resources."
errors={errors.leaders}
>
<UsersSelector
value={values.leaders}
onChange={(leaders) =>
setFieldValue('leaders', leaders)
}
users={users}
dataCy="team-teamLeaderSelect"
inputId="users-input"
placeholder="Select one or more team leaders"
/>
</FormControl>
)}
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
disabled={!isValid}
dataCy="team-createTeamButton"
isLoading={isSubmitting}
loadingText="Creating team..."
>
<i
className="fa fa-plus space-right"
aria-hidden="true"
/>
Create team
</LoadingButton>
</div>
</div>
</Form>
)}
</Formik>
</WidgetBody>
</Widget>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
import { object, string, array, number } from 'yup';
import { TeamViewModel } from '@/portainer/models/team';
export function validationSchema(teams: TeamViewModel[]) {
return object().shape({
name: string()
.required('This field is required.')
.test('is-unique', 'This team already exists.', (name) => !!name && teams.every((team) => team.Name !== name)),
leaders: array().of(number()),
});
}

View File

@ -0,0 +1,7 @@
import { r2a } from '@/react-tools/react2angular';
import { CreateTeamForm } from './CreateTeamForm';
export { CreateTeamForm };
export const CreateTeamFormAngular = r2a(CreateTeamForm, ['users', 'actionInProgress', 'onSubmit', 'teams']);

View File

@ -0,0 +1,8 @@
import angular from 'angular';
import { CreateTeamFormAngular } from './CreateTeamForm';
export default angular
.module('portainer.app.teams', [])
.component('createTeamForm', CreateTeamFormAngular).name;

View File

@ -3,12 +3,12 @@ import { useRouter } from '@uirouter/react';
import { Widget, WidgetBody } from '@/portainer/components/widget';
import { FormControl } from '@/portainer/components/form-components/FormControl';
import { TextInput } from '@/portainer/components/form-components/Input';
import { Button } from '@/portainer/components/Button';
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
import { TextTip } from '@/portainer/components/Tip/TextTip';
import { Code } from '@/portainer/components/Code';
import { CopyButton } from '@/portainer/components/Button/CopyButton';
import { Input } from '@/portainer/components/form-components/Input';
import styles from './CreateAccessToken.module.css';
@ -64,16 +64,15 @@ export function CreateAccessToken({
<WidgetBody>
<div>
<FormControl inputId="input" label="Description" errors={errorText}>
<TextInput
<Input
id="input"
onChange={(value) => setDescription(value)}
type="text"
onChange={(e) => setDescription(e.target.value)}
value={description}
/>
</FormControl>
<Button
disabled={!!errorText || !!accessToken}
onClick={generateAccessToken}
onClick={() => generateAccessToken()}
className={styles.addButton}
>
Add access token

View File

@ -7,87 +7,7 @@
<rd-header-content>Teams management</rd-header-content>
</rd-header>
<div class="row" ng-if="isAdmin">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title-text="Add a new team"> </rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" name="teamCreationForm" ng-submit="addTeam()">
<!-- name-input -->
<div class="form-group">
<label for="team_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
id="team_name"
name="team_name"
ng-model="formValues.Name"
ng-change="checkNameValidity(teamCreationForm)"
placeholder="e.g. development"
auto-focus
required
data-cy="team-teamNameInput"
/>
</div>
</div>
<div class="form-group" ng-show="teamCreationForm.team_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="teamCreationForm.team_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="validName"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This team already exists.</p>
</div>
</div>
</div>
<!-- !name-input -->
<!-- team-leaders -->
<div class="form-group" ng-if="users.length > 0">
<div class="col-sm-12">
<label class="control-label text-left">
Select team leader(s)
<portainer-tooltip
position="bottom"
message="You can assign one or more leaders to this team. Team leaders can manage their teams users and resources."
></portainer-tooltip>
</label>
<span
isteven-multi-select
ng-if="users.length > 0"
input-model="users"
output-model="formValues.Leaders"
button-label="Username"
item-label="Username"
tick-property="ticked"
helper-elements="filter"
search-property="Username"
translation="{nothingSelected: 'Select one or more team leaders', search: 'Search...'}"
style="margin-left: 20px;"
data-cy="team-teamLeaderSelect"
>
</span>
</div>
</div>
<!-- !team-leaders -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !teamCreationForm.$valid"
ng-click="addTeam()"
button-spinner="state.actionInProgress"
data-cy="team-createTeamButton"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create team</span>
<span ng-show="state.actionInProgress">Creating team...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<create-team-form ng-if="isAdmin && users" users="users" action-in-progress="state.actionInProgress" teams="teams" on-submit="(addTeam)"></create-team-form>
<div class="row">
<div class="col-sm-12">

View File

@ -30,15 +30,11 @@ angular.module('portainer.app').controller('TeamsController', [
form.team_name.$setValidity('validName', valid);
};
$scope.addTeam = function () {
var teamName = $scope.formValues.Name;
var leaderIds = [];
angular.forEach($scope.formValues.Leaders, function (user) {
leaderIds.push(user.Id);
});
$scope.addTeam = function (formValues) {
const teamName = formValues.name;
$scope.state.actionInProgress = true;
TeamService.createTeam(teamName, leaderIds)
TeamService.createTeam(teamName, formValues.leaders)
.then(function success() {
Notifications.success('Team successfully created', teamName);
$state.reload();

View File

@ -100,6 +100,7 @@
"fast-json-patch": "^3.1.0",
"filesize": "~3.3.0",
"filesize-parser": "^1.5.0",
"formik": "^2.2.9",
"jquery": "^3.6.0",
"js-base64": "^3.7.2",
"js-yaml": "^3.14.0",
@ -111,6 +112,7 @@
"rc-slider": "^9.7.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-select": "^5.2.1",
"react-tooltip": "^4.2.21",
"sanitize-html": "^2.5.3",
"spinkit": "^2.0.1",
@ -121,7 +123,8 @@
"uuid": "^3.3.2",
"x256": "^0.0.2",
"xterm": "^3.8.0",
"yaml": "^1.10.2"
"yaml": "^1.10.2",
"yup": "^0.32.11"
},
"devDependencies": {
"@apidevtools/swagger-cli": "^4.0.4",
@ -139,6 +142,7 @@
"@storybook/react": "^6.4.9",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/angular": "^1.8.3",
"@types/bootbox": "^5.2.2",
"@types/jest": "^27.0.3",
@ -232,4 +236,4 @@
"pre-commit": "lint-staged"
}
}
}
}

208
yarn.lock
View File

@ -1148,7 +1148,35 @@
core-js-pure "^3.16.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6":
version "7.15.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.0.tgz#e27b977f2e2088ba24748bf99b5e1dece64e4f0b"
integrity sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2":
version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.7":
version "7.16.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.16.3", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.4":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
@ -1258,6 +1286,17 @@
"@emotion/utils" "0.11.3"
"@emotion/weak-memoize" "0.2.5"
"@emotion/cache@^11.4.0", "@emotion/cache@^11.6.0":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.6.0.tgz#65fbdbbe4382f1991d8b20853c38e63ecccec9a1"
integrity sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==
dependencies:
"@emotion/memoize" "^0.7.4"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
stylis "^4.0.10"
"@emotion/core@^10.1.1":
version "10.1.1"
resolved "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz"
@ -1279,7 +1318,7 @@
"@emotion/utils" "0.11.3"
babel-plugin-emotion "^10.0.27"
"@emotion/hash@0.8.0":
"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
@ -1296,6 +1335,24 @@
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@emotion/memoize@^0.7.4":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
"@emotion/react@^11.1.1":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.6.0.tgz#61fcb95c1e01255734c2c721cb9beabcf521eb0f"
integrity sha512-23MnRZFBN9+D1lHXC5pD6z4X9yhPxxtHr6f+iTGz6Fv6Rda0GdefPrsHL7otsEf+//7uqCdT5QtHeRxHCERzuw==
dependencies:
"@babel/runtime" "^7.13.10"
"@emotion/cache" "^11.6.0"
"@emotion/serialize" "^1.0.2"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
version "0.11.16"
resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz"
@ -1307,11 +1364,27 @@
"@emotion/utils" "0.11.3"
csstype "^2.5.7"
"@emotion/serialize@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
dependencies:
"@emotion/hash" "^0.8.0"
"@emotion/memoize" "^0.7.4"
"@emotion/unitless" "^0.7.5"
"@emotion/utils" "^1.0.0"
csstype "^3.0.2"
"@emotion/sheet@0.9.4":
version "0.9.4"
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz"
integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
"@emotion/sheet@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/styled-base@^10.0.27":
version "10.0.31"
resolved "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.0.31.tgz"
@ -1335,7 +1408,7 @@
resolved "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz"
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
"@emotion/unitless@0.7.5":
"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.5":
version "0.7.5"
resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
@ -1345,7 +1418,12 @@
resolved "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz"
integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
"@emotion/weak-memoize@0.2.5":
"@emotion/utils@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
"@emotion/weak-memoize@0.2.5", "@emotion/weak-memoize@^0.2.5":
version "0.2.5"
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
@ -2769,6 +2847,13 @@
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0"
"@testing-library/user-event@^13.5.0":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
dependencies:
"@babel/runtime" "^7.12.5"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz"
@ -3017,6 +3102,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.176.tgz#641150fc1cda36fbfa329de603bbb175d7ee20c0"
integrity sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==
"@types/lodash@^4.14.175":
version "4.14.177"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578"
integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==
"@types/mdast@^3.0.0":
version "3.0.10"
resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz"
@ -3116,6 +3206,13 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@^4.4.0":
version "4.4.4"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
dependencies:
"@types/react" "*"
"@types/react@*":
version "17.0.27"
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.27.tgz"
@ -6619,6 +6716,11 @@ deep-object-diff@^1.1.0:
resolved "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.0.tgz"
integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw==
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz"
@ -6873,6 +6975,14 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz"
@ -8469,6 +8579,19 @@ format@^0.2.0:
resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz"
integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
formik@^2.2.9:
version "2.2.9"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
dependencies:
deepmerge "^2.1.1"
hoist-non-react-statics "^3.3.0"
lodash "^4.17.21"
lodash-es "^4.17.21"
react-fast-compare "^2.0.1"
tiny-warning "^1.0.2"
tslib "^1.10.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz"
@ -9499,7 +9622,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -11948,6 +12071,11 @@ memfs@^3.2.2:
dependencies:
fs-monkey "1.0.3"
memoize-one@^5.0.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memoizerific@^1.11.3:
version "1.11.3"
resolved "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz"
@ -12318,6 +12446,11 @@ nano-time@1.0.0:
dependencies:
big-integer "^1.6.16"
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanocolors@^0.1.0, nanocolors@^0.1.5:
version "0.1.12"
resolved "https://registry.npmjs.org/nanocolors/-/nanocolors-0.1.12.tgz"
@ -14040,7 +14173,7 @@ prompts@^2.4.0:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2:
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -14049,6 +14182,11 @@ prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
property-expr@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
property-information@^5.0.0, property-information@^5.3.0:
version "5.6.0"
resolved "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz"
@ -14372,6 +14510,11 @@ react-error-overlay@^6.0.9:
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fast-compare@^3.0.1, react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz"
@ -14453,6 +14596,19 @@ react-router@6.1.1, react-router@^6.0.0:
dependencies:
history "^5.1.0"
react-select@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.2.1.tgz#416c25c6b79b94687702374e019c4f2ed9d159d6"
integrity sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==
dependencies:
"@babel/runtime" "^7.12.0"
"@emotion/cache" "^11.4.0"
"@emotion/react" "^11.1.1"
"@types/react-transition-group" "^4.4.0"
memoize-one "^5.0.0"
prop-types "^15.6.0"
react-transition-group "^4.3.0"
react-shallow-renderer@^16.13.1:
version "16.14.1"
resolved "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz"
@ -14509,6 +14665,16 @@ react-tooltip@^4.2.21:
prop-types "^15.7.2"
uuid "^7.0.3"
react-transition-group@^4.3.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
@ -16146,6 +16312,11 @@ stylehacks@^4.0.0:
postcss "^7.0.0"
postcss-selector-parser "^3.0.0"
stylis@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
@ -16454,6 +16625,11 @@ timsort@^0.3.0:
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
title-case@^2.1.0:
version "2.1.1"
resolved "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz"
@ -16545,6 +16721,11 @@ toidentifier@1.0.0:
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
tough-cookie@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz"
@ -16628,7 +16809,7 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
tslib@^1.11.1, tslib@^1.8.1:
tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@ -17828,6 +18009,19 @@ yocto-queue@^0.1.0:
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.32.11:
version "0.32.11"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"
integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/lodash" "^4.14.175"
lodash "^4.17.21"
lodash-es "^4.17.21"
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"
z-schema@^5.0.1:
version "5.0.2"
resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.2.tgz#f410394b2c9fcb9edaf6a7511491c0bb4e89a504"