1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Merge remote-tracking branch 'origin/archive_table' into archive_table

This commit is contained in:
andreas-unleash 2022-06-08 14:37:05 +03:00
commit 88f2bef1be
16 changed files with 172 additions and 167 deletions

View File

@ -74,7 +74,7 @@ describe('feature', () => {
cy.get("[data-testid='CF_DESC_ID'").type('hello-world');
cy.get("[data-testid='CF_CREATE_BTN_ID']").click();
cy.get("[data-testid='INPUT_ERROR_TEXT']").contains(
'A feature with this name already exists'
'A toggle with that name already exists'
);
});

View File

@ -39,9 +39,9 @@
"devDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "5.8.2",
"@mui/lab": "5.0.0-alpha.84",
"@mui/material": "5.8.2",
"@mui/icons-material": "5.8.3",
"@mui/lab": "5.0.0-alpha.85",
"@mui/material": "5.8.3",
"@openapitools/openapi-generator-cli": "2.5.1",
"@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.4",
@ -78,11 +78,11 @@
"lodash.clonedeep": "4.5.0",
"msw": "0.42.0",
"pkginfo": "^0.4.1",
"plausible-tracker": "0.3.7",
"plausible-tracker": "0.3.8",
"prettier": "2.6.2",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-chartjs-2": "4.1.0",
"react-chartjs-2": "4.2.0",
"react-dom": "17.0.2",
"react-hooks-global-state": "1.0.2",
"react-router-dom": "6.3.0",

View File

@ -3,6 +3,7 @@ import { IPermission } from 'interfaces/project';
import cloneDeep from 'lodash.clonedeep';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import { formatUnknownError } from 'utils/formatUnknownError';
export interface ICheckedPermission {
[key: string]: IPermission;
@ -203,21 +204,14 @@ const useProjectRoleForm = (
permissions,
};
};
const NAME_EXISTS_ERROR =
'BadRequestError: There already exists a role with the name';
const validateNameUniqueness = async () => {
const payload = getProjectRolePayload();
try {
await validateRole(payload);
} catch (e) {
// @ts-expect-error
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
name: 'There already exists a role with this role name',
}));
}
} catch (error: unknown) {
setErrors(prev => ({ ...prev, name: formatUnknownError(error) }));
}
};

View File

@ -3,13 +3,11 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
constraintIconContainer: {
backgroundColor: theme.palette.primary.main,
height: '28px',
width: '28px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '2rem',
marginRight: theme.spacing(2),
[theme.breakpoints.down(650)]: {
marginBottom: '1rem',
marginRight: 0,
@ -17,8 +15,6 @@ export const useStyles = makeStyles()(theme => ({
},
constraintIcon: {
fill: '#fff',
width: '26px',
height: '26px',
},
accordion: {
border: `1px solid ${theme.palette.grey[300]}`,
@ -44,7 +40,7 @@ export const useStyles = makeStyles()(theme => ({
display: 'flex',
alignItems: 'center',
width: '100%',
[theme.breakpoints.down(650)]: {
[theme.breakpoints.down(710)]: {
flexDirection: 'column',
alignItems: 'center',
position: 'relative',
@ -56,10 +52,13 @@ export const useStyles = makeStyles()(theme => ({
},
headerValues: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.primary.main,
},
headerValuesExpand: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.primary.dark,
[theme.breakpoints.down(710)]: {
textAlign: 'center',
},
},
headerConstraintContainer: {
minWidth: '220px',
@ -69,11 +68,6 @@ export const useStyles = makeStyles()(theme => ({
paddingRight: 0,
},
},
headerViewValuesContainer: {
[theme.breakpoints.down(990)]: {
display: 'none',
},
},
editingBadge: {
borderRadius: theme.shape.borderRadiusExtraLarge,
padding: '0.25rem 0.5rem',
@ -122,9 +116,8 @@ export const useStyles = makeStyles()(theme => ({
headerActions: {
marginLeft: 'auto',
whiteSpace: 'nowrap',
[theme.breakpoints.down(660)]: {
marginLeft: '0',
marginTop: '0.5rem',
[theme.breakpoints.down(710)]: {
display: 'none',
},
},
accordionDetails: {
@ -140,9 +133,8 @@ export const useStyles = makeStyles()(theme => ({
summary: {
border: 'none',
padding: '0.25rem 1rem',
height: '85px',
[theme.breakpoints.down(770)]: {
height: '200px',
'&:hover .valuesExpandLabel': {
textDecoration: 'underline',
},
},
settingsParagraph: {

View File

@ -70,15 +70,20 @@ export const FreeTextInput = ({
};
const addValues = () => {
if (inputValues.length === 0) {
setError('values can not be empty');
return;
const newValues = uniqueValues([
...values,
...parseParameterStrings(inputValues),
]);
if (newValues.length === 0) {
setError('values cannot be empty');
} else if (newValues.some(v => v.length > 100)) {
setError('values cannot be longer than 100 characters');
} else {
setError('');
setInputValues('');
setValues(newValues);
}
setError('');
setValues(
uniqueValues([...values, ...parseParameterStrings(inputValues)])
);
setInputValues('');
};
return (

View File

@ -2,6 +2,7 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
width: '100%',
display: 'grid',
gap: '1rem',
},

View File

@ -72,9 +72,9 @@ const SingleValue = ({ value, operator }: ISingleValueProps) => {
<Chip
label={
<StringTruncator
maxWidth="200"
maxWidth="400"
text={value}
maxLength={25}
maxLength={50}
/>
}
className={styles.chip}
@ -95,7 +95,15 @@ const MultipleValues = ({ values }: IMultipleValuesProps) => {
return (
<>
<ConstraintValueSearch filter={filter} setFilter={setFilter} />
<ConditionallyRender
condition={values.length > 20}
show={
<ConstraintValueSearch
filter={filter}
setFilter={setFilter}
/>
}
/>
{values
.filter(value => value.includes(filter))
.map((value, index) => (
@ -103,9 +111,9 @@ const MultipleValues = ({ values }: IMultipleValuesProps) => {
key={`${value}-${index}`}
label={
<StringTruncator
maxWidth="200"
maxWidth="400"
text={value}
maxLength={25}
maxLength={50}
className={styles.chipValue}
/>
}

View File

@ -1,5 +1,4 @@
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { Chip, useMediaQuery, IconButton, Tooltip } from '@mui/material';
import { Chip, IconButton, Tooltip, styled } from '@mui/material';
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
import { Delete, Edit } from '@mui/icons-material';
import { IConstraint } from 'interfaces/strategy';
@ -10,6 +9,44 @@ import React from 'react';
import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { ConstraintOperator } from 'component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator';
import classnames from 'classnames';
const StyledHeaderText = styled('span')(({ theme }) => ({
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
maxWidth: '100px',
minWidth: '100px',
marginRight: '10px',
wordBreak: 'break-word',
fontSize: theme.fontSizes.smallBody,
[theme.breakpoints.down(710)]: {
textAlign: 'center',
padding: theme.spacing(1, 0),
marginRight: 'inherit',
maxWidth: 'inherit',
},
}));
const StyledValuesSpan = styled('span')(({ theme }) => ({
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
wordBreak: 'break-word',
fontSize: theme.fontSizes.smallBody,
[theme.breakpoints.down(710)]: {
margin: theme.spacing(1, 0),
textAlign: 'center',
},
}));
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
[theme.breakpoints.down(710)]: {
margin: theme.spacing(1, 0),
},
}));
interface IConstraintAccordionViewHeaderProps {
compact: boolean;
@ -28,9 +65,6 @@ export const ConstraintAccordionViewHeader = ({
}: IConstraintAccordionViewHeaderProps) => {
const { classes: styles } = useStyles();
const { locationSettings } = useLocationSettings();
const smallScreen = useMediaQuery(`(max-width:${790}px)`);
const minWidthHeader = compact || smallScreen ? '100px' : '175px';
const onEditClick =
onEdit &&
@ -50,42 +84,43 @@ export const ConstraintAccordionViewHeader = ({
<div className={styles.headerContainer}>
<ConstraintIcon />
<div className={styles.headerMetaInfo}>
<div style={{ minWidth: minWidthHeader }}>
<StringTruncator
text={constraint.contextName}
maxWidth="175px"
maxLength={25}
/>
</div>
<Tooltip title={constraint.contextName} arrow>
<StyledHeaderText>
{constraint.contextName}
</StyledHeaderText>
</Tooltip>
<div className={styles.headerConstraintContainer}>
<ConstraintOperator constraint={constraint} />
</div>
<div className={styles.headerViewValuesContainer}>
<ConditionallyRender
condition={singleValue}
show={
<Chip
label={formatConstraintValue(
constraint,
locationSettings
<ConditionallyRender
condition={singleValue}
show={
<StyledSingleValueChip
label={formatConstraintValue(
constraint,
locationSettings
)}
/>
}
elseShow={
<div className={styles.headerValuesContainer}>
<StyledValuesSpan>
{constraint?.values
?.map(value => value)
.join(', ')}
</StyledValuesSpan>
<p
className={classnames(
styles.headerValuesExpand,
'valuesExpandLabel'
)}
/>
}
elseShow={
<div className={styles.headerValuesContainer}>
<p className={styles.headerValues}>
{constraint?.values?.length}{' '}
{constraint?.values?.length === 1
? 'value'
: 'values'}
</p>
<p className={styles.headerValuesExpand}>
Expand to view
</p>
</div>
}
/>
</div>
>
Expand to view all ({constraint?.values?.length}
)
</p>
</div>
}
/>
</div>
<div className={styles.headerActions}>
<ConditionallyRender

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi';
import { ILegalValue } from 'interfaces/context';
import { formatUnknownError } from 'utils/formatUnknownError';
export const useContextForm = (
initialContextName = '',
@ -41,8 +42,6 @@ export const useContextForm = (
};
};
const NAME_EXISTS_ERROR = 'A context field with that name already exist';
const validateContext = async () => {
if (contextName.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
@ -51,18 +50,8 @@ export const useContextForm = (
try {
await validateContextName(contextName);
return true;
} catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
name: 'A context field with that name already exist',
}));
} else {
setErrors(prev => ({
...prev,
name: e.toString(),
}));
}
} catch (error: unknown) {
setErrors(prev => ({ ...prev, name: formatUnknownError(error) }));
return false;
}
};

View File

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import { formatUnknownError } from 'utils/formatUnknownError';
const useEnvironmentForm = (initialName = '', initialType = 'development') => {
const NAME_EXISTS_ERROR = 'Error: Environment';
const [name, setName] = useState(initialName);
const [type, setType] = useState(initialType);
const [errors, setErrors] = useState({});
@ -35,16 +35,11 @@ const useEnvironmentForm = (initialName = '', initialType = 'development') => {
try {
await validateEnvName(name);
} catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
name: 'Name already exists',
}));
}
return true;
} catch (error: unknown) {
setErrors(prev => ({ ...prev, name: formatUnknownError(error) }));
return false;
}
return true;
};
const clearErrors = () => {

View File

@ -6,6 +6,7 @@ export const useStyles = makeStyles()(theme => ({
margin: '0.25rem',
},
paragraph: {
display: 'inline',
margin: '0.25rem 0',
maxWidth: '95%',
textAlign: 'center',

View File

@ -13,6 +13,8 @@ export const useStyles = makeStyles()(theme => ({
link: {
textDecoration: 'none',
fontWeight: theme.fontWeight.bold,
color: theme.palette.primary.main,
'&:hover': {
textDecoration: 'underline',
},
},
}));

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useQueryParams from 'hooks/useQueryParams';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { formatUnknownError } from 'utils/formatUnknownError';
const useFeatureForm = (
initialName = '',
@ -55,8 +56,6 @@ const useFeatureForm = (
};
};
const NAME_EXISTS_ERROR = 'Error: A toggle with that name already exists';
const validateToggleName = async () => {
if (name.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
@ -65,18 +64,8 @@ const useFeatureForm = (
try {
await validateFeatureToggleName(name);
return true;
} catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
name: 'A feature with this name already exists',
}));
} else {
setErrors(prev => ({
...prev,
name: e.toString(),
}));
}
} catch (error: unknown) {
setErrors(prev => ({ ...prev, name: formatUnknownError(error) }));
return false;
}
};

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import { formatUnknownError } from 'utils/formatUnknownError';
const useProjectForm = (
initialProjectId = '',
@ -31,7 +32,6 @@ const useProjectForm = (
description: projectDesc,
};
};
const NAME_EXISTS_ERROR = 'Error: A project with this id already exists.';
const validateProjectId = async () => {
if (projectId.length === 0) {
@ -41,18 +41,8 @@ const useProjectForm = (
try {
await validateId(getProjectPayload());
return true;
} catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
id: 'A project with this id already exists',
}));
} else {
setErrors(prev => ({
...prev,
id: e.toString(),
}));
}
} catch (error: unknown) {
setErrors(prev => ({ ...prev, id: formatUnknownError(error) }));
return false;
}
};

View File

@ -14,6 +14,10 @@ export const useStyles = makeStyles()(theme => ({
[theme.breakpoints.down('sm')]: {
justifyContent: 'center',
},
'&:hover': {
transition: 'background-color 0.2s ease-in-out',
backgroundColor: theme.palette.grey[100],
},
},
header: {
display: 'flex',

View File

@ -1336,10 +1336,10 @@
outvariant "^1.2.1"
strict-event-emitter "^0.2.4"
"@mui/base@5.0.0-alpha.83":
version "5.0.0-alpha.83"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.83.tgz#8ac60dc7315f8ae001233e8e10398e31833625bd"
integrity sha512-/bFcjiI36R2Epf2Y3BkZOIdxrz5uMLqOU4cRai4igJ8DHTRMZDeKbOff0SdvwJNwg8r6oPUyoeOpsWkaOOX9/g==
"@mui/base@5.0.0-alpha.84":
version "5.0.0-alpha.84"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.84.tgz#83c580c9b04b4e4efe3fb39572720470b0d7cc29"
integrity sha512-uDx+wGVytS+ZHiWHyzUyijY83GSIXJpzSJ0PGc/8/s+8nBzeHvaPKrAyJz15ASLr52hYRA6PQGqn0eRAsB7syQ==
dependencies:
"@babel/runtime" "^7.17.2"
"@emotion/is-prop-valid" "^1.1.2"
@ -1350,21 +1350,21 @@
prop-types "^15.8.1"
react-is "^17.0.2"
"@mui/icons-material@5.8.2":
version "5.8.2"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.8.2.tgz#211ac2a041ece952749d39c3c26071226993be14"
integrity sha512-fP6KUCCZZjc2rdbMSmkNmBHDskLkmP0uCox57cbVXvomU6BOPrCxnr5YXsSsQrZB8fchx7hfH0bkAgvMZ5KM0Q==
"@mui/icons-material@5.8.3":
version "5.8.3"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.8.3.tgz#75c8bde42e6ba71e3871439a2b751be987343493"
integrity sha512-dAdhimSLKOV0Q8FR7AYGEaCrTUh9OV7zU4Ueo5REoUt4cC3Vy+UBKDjZk66x5ezaYb63AFgQIFwtnZj3B/QDbQ==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/lab@5.0.0-alpha.84":
version "5.0.0-alpha.84"
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.84.tgz#e96644fefc0745b9a50cba6c18ca53d581a863b8"
integrity sha512-HLYD6E3PAlzKMGZkkpiPI7trHP3WYDvrjQstEsFwdaGy9AMWPmyTxhwUyfB4VVHOx3zcj4p/a36kECDtEOAJ+g==
"@mui/lab@5.0.0-alpha.85":
version "5.0.0-alpha.85"
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.85.tgz#e7f5f2530b66151d508dc23d4d9848953e6e5b1b"
integrity sha512-GaPl5azVXr9dbwZe1DiKr3GO9Bg3nbZ48oRTDZoMxWYMB8dm4f73GrY2Sv1Sf03z19YzlD7Ixskr6rGcKGPWlw==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/base" "5.0.0-alpha.83"
"@mui/system" "^5.8.2"
"@mui/base" "5.0.0-alpha.84"
"@mui/system" "^5.8.3"
"@mui/utils" "^5.8.0"
"@mui/x-date-pickers" "5.0.0-alpha.1"
clsx "^1.1.1"
@ -1373,14 +1373,14 @@
react-transition-group "^4.4.2"
rifm "^0.12.1"
"@mui/material@5.8.2":
version "5.8.2"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.8.2.tgz#13547df6bb6991e064f42ee71ce17c30629e263a"
integrity sha512-w/A1KG9Czf42uTyJOiRU5U1VullOz1R3xcsBvv3BtKCCWdVP+D6v/Yb8v0tJpIixMEbjeWzWGjotQBU0nd+yNA==
"@mui/material@5.8.3":
version "5.8.3"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.8.3.tgz#86681d14c1a119d1d9b6b981c864736d075d095f"
integrity sha512-8UecY/W9SMtEZm5PMCUcMbujajVP6fobu0BgBPiIWwwWRblZVEzqprY6v1P2me7qCyrve4L4V/rqAKPKhVHOSg==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/base" "5.0.0-alpha.83"
"@mui/system" "^5.8.2"
"@mui/base" "5.0.0-alpha.84"
"@mui/system" "^5.8.3"
"@mui/types" "^7.1.3"
"@mui/utils" "^5.8.0"
"@types/react-transition-group" "^4.4.4"
@ -1409,10 +1409,10 @@
"@emotion/cache" "^11.7.1"
prop-types "^15.8.1"
"@mui/system@^5.8.2":
version "5.8.2"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.8.2.tgz#b6d889051caec1efe31a87f0bb96d52f003e517d"
integrity sha512-N74gDNKM+MnWvKTMmCPvCVLH4f0ZzakP1bcMDaPctrHwcyxNcEmtTGNpIiVk0Iu7vtThZAFL3DjHpINPGF7+cg==
"@mui/system@^5.8.3":
version "5.8.3"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.8.3.tgz#66db174f1b5c244eb73dbc48527509782a22ec0a"
integrity sha512-/tyGQcYqZT0nl98qV9XnGiedTO+V7VHc28k4POfhMJNedB1CRrwWRm767DeEdc5f/8CU2See3WD16ikP6pYiOA==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/private-theming" "^5.8.0"
@ -5041,10 +5041,10 @@ pkginfo@^0.4.1:
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=
plausible-tracker@0.3.7:
version "0.3.7"
resolved "https://registry.yarnpkg.com/plausible-tracker/-/plausible-tracker-0.3.7.tgz#e93d241a1c46bf70f05317a6f177feff6c4865d5"
integrity sha512-yhM3VJekqMIEDbvx/PqratMyHpF4T/skTO4owzxSo3YMB/ZVmAYwh9c2iKRuJnkE3b4NwsMMW9b0Vw5VD5Gpyw==
plausible-tracker@0.3.8:
version "0.3.8"
resolved "https://registry.yarnpkg.com/plausible-tracker/-/plausible-tracker-0.3.8.tgz#9b8b322cc41e0e1d6473869ef234deea365a5a40"
integrity sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==
postcss@^8.4.13:
version "8.4.13"
@ -5126,10 +5126,10 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
react-chartjs-2@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz#2a123df16d3a987c54eb4e810ed766d3c03adf8d"
integrity sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==
react-chartjs-2@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.2.0.tgz#bc5693a8b161f125301cf28ab0fe980d7dce54aa"
integrity sha512-9Vm9Sg9XAKiR579/FnBkesofjW9goaaFLfS7XlGTzUJlWFZGSE6A/pBI6+i/bP3pobKZoFcWJdFnjShytToqXw==
react-dom@17.0.2:
version "17.0.2"