mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into task/constraint_card_adjustmnets
This commit is contained in:
		
						commit
						6d405c9af5
					
				| @ -101,7 +101,11 @@ | ||||
|     "vite-plugin-svgr": "2.2.0", | ||||
|     "vite-tsconfig-paths": "3.5.0", | ||||
|     "vitest": "0.16.0", | ||||
|     "whatwg-fetch": "^3.6.2" | ||||
|     "whatwg-fetch": "^3.6.2", | ||||
|     "@codemirror/lang-json": "^6.0.0", | ||||
|     "@codemirror/state": "^6.1.0", | ||||
|     "@uiw/react-codemirror": "^4.11.4", | ||||
|     "codemirror": "^6.0.1" | ||||
|   }, | ||||
|   "jest": { | ||||
|     "moduleNameMapper": { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { parseDateValue } from 'component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue'; | ||||
| import { parseDateValue } from 'component/common/util'; | ||||
| 
 | ||||
| test(`Date component is able to parse midnight when it's 00`, () => { | ||||
|     let f = parseDateValue('2022-03-15T12:27'); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader'; | ||||
| import { format, isValid } from 'date-fns'; | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import { parseDateValue, parseValidDate } from 'component/common/util'; | ||||
| interface IDateSingleValueProps { | ||||
|     setValue: (value: string) => void; | ||||
|     value?: string; | ||||
| @ -8,11 +8,6 @@ interface IDateSingleValueProps { | ||||
|     setError: React.Dispatch<React.SetStateAction<string>>; | ||||
| } | ||||
| 
 | ||||
| export const parseDateValue = (value: string) => { | ||||
|     const date = new Date(value); | ||||
|     return format(date, 'yyyy-MM-dd') + 'T' + format(date, 'HH:mm'); | ||||
| }; | ||||
| 
 | ||||
| export const DateSingleValue = ({ | ||||
|     setValue, | ||||
|     value, | ||||
| @ -45,11 +40,3 @@ export const DateSingleValue = ({ | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const parseValidDate = (value: string): Date | undefined => { | ||||
|     const parsed = new Date(value); | ||||
| 
 | ||||
|     if (isValid(parsed)) { | ||||
|         return parsed; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,40 @@ | ||||
| import { styled, useTheme } from '@mui/material'; | ||||
| import { FC } from 'react'; | ||||
| 
 | ||||
| const StyledIndicator = styled('div')(({ style, theme }) => ({ | ||||
|     width: '25px', | ||||
|     height: '25px', | ||||
|     borderRadius: '50%', | ||||
|     color: theme.palette.text.tertiaryContrast, | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     justifyContent: 'center', | ||||
|     fontWeight: 'bold', | ||||
|     ...style, | ||||
| })); | ||||
| 
 | ||||
| interface IGuidanceIndicatorProps { | ||||
|     style?: React.CSSProperties; | ||||
|     type?: guidanceIndicatorType; | ||||
| } | ||||
| 
 | ||||
| type guidanceIndicatorType = 'primary' | 'secondary'; | ||||
| 
 | ||||
| export const GuidanceIndicator: FC<IGuidanceIndicatorProps> = ({ | ||||
|     style, | ||||
|     children, | ||||
|     type, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     const defaults = { backgroundColor: theme.palette.primary.main }; | ||||
|     if (type === 'secondary') { | ||||
|         defaults.backgroundColor = theme.palette.tertiary.dark; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledIndicator style={{ ...defaults, ...style }}> | ||||
|             {children} | ||||
|         </StyledIndicator> | ||||
|     ); | ||||
| }; | ||||
| @ -17,6 +17,7 @@ interface ISearchProps { | ||||
|     hasFilters?: boolean; | ||||
|     disabled?: boolean; | ||||
|     getSearchContext?: () => IGetSearchContextOutput; | ||||
|     containerStyles?: React.CSSProperties; | ||||
| } | ||||
| 
 | ||||
| export const Search = ({ | ||||
| @ -27,6 +28,7 @@ export const Search = ({ | ||||
|     hasFilters, | ||||
|     disabled, | ||||
|     getSearchContext, | ||||
|     containerStyles, | ||||
| }: ISearchProps) => { | ||||
|     const ref = useRef<HTMLInputElement>(); | ||||
|     const { classes: styles } = useStyles(); | ||||
| @ -59,7 +61,7 @@ export const Search = ({ | ||||
|     const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; | ||||
| 
 | ||||
|     return ( | ||||
|         <div className={styles.container}> | ||||
|         <div className={styles.container} style={containerStyles}> | ||||
|             <div | ||||
|                 className={classnames( | ||||
|                     styles.search, | ||||
|  | ||||
| @ -8,6 +8,7 @@ export const useStyles = makeStyles<{ lineClamp?: number }>()( | ||||
|             overflow: lineClamp ? 'hidden' : 'auto', | ||||
|             WebkitLineClamp: lineClamp ? lineClamp : 'none', | ||||
|             WebkitBoxOrient: 'vertical', | ||||
|             wordBreak: 'break-all', | ||||
|         }, | ||||
|     }) | ||||
| ); | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVaria | ||||
| import { IFlags } from 'interfaces/uiConfig'; | ||||
| import { IRoute } from 'interfaces/route'; | ||||
| import { IFeatureVariant } from 'interfaces/featureToggle'; | ||||
| import { format, isValid } from 'date-fns'; | ||||
| 
 | ||||
| export const filterByFlags = (flags: IFlags) => (r: IRoute) => { | ||||
|     if (!r.flag) { | ||||
| @ -23,6 +24,23 @@ export const trim = (value: string): string => { | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const parseDateValue = (value: string) => { | ||||
|     const date = new Date(value); | ||||
|     return format(date, 'yyyy-MM-dd') + 'T' + format(date, 'HH:mm'); | ||||
| }; | ||||
| 
 | ||||
| export const parseValidDate = (value: string): Date | undefined => { | ||||
|     const parsed = new Date(value); | ||||
| 
 | ||||
|     if (isValid(parsed)) { | ||||
|         return parsed; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const calculateVariantWeight = (weight: number) => { | ||||
|     return weight / 10.0; | ||||
| }; | ||||
| 
 | ||||
| export function updateWeight(variants: IFeatureVariant[], totalWeight: number) { | ||||
|     if (variants.length === 0) { | ||||
|         return []; | ||||
|  | ||||
| @ -9,7 +9,6 @@ import { | ||||
|     useMediaQuery, | ||||
| } from '@mui/material'; | ||||
| import { AddVariant } from './AddFeatureVariant/AddFeatureVariant'; | ||||
| 
 | ||||
| import { useContext, useEffect, useState, useMemo, useCallback } from 'react'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| @ -20,7 +19,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; | ||||
| import { IFeatureVariant } from 'interfaces/featureToggle'; | ||||
| import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { updateWeight } from 'component/common/util'; | ||||
| import { calculateVariantWeight, updateWeight } from 'component/common/util'; | ||||
| import cloneDeep from 'lodash.clonedeep'; | ||||
| import useDeleteVariantMarkup from './useDeleteVariantMarkup'; | ||||
| import PermissionButton from 'component/common/PermissionButton/PermissionButton'; | ||||
| @ -141,7 +140,7 @@ export const FeatureVariantsList = () => { | ||||
|                 }: any) => { | ||||
|                     return ( | ||||
|                         <TextCell data-testid={`VARIANT_WEIGHT_${name}`}> | ||||
|                             {weight / 10.0} % | ||||
|                             {calculateVariantWeight(weight)} % | ||||
|                         </TextCell> | ||||
|                     ); | ||||
|                 }, | ||||
|  | ||||
| @ -1,31 +1,31 @@ | ||||
| import { FormEventHandler, useEffect, useState, VFC } from 'react'; | ||||
| import { useSearchParams } from 'react-router-dom'; | ||||
| import { | ||||
|     Box, | ||||
|     Button, | ||||
|     Divider, | ||||
|     Paper, | ||||
|     Typography, | ||||
|     useTheme, | ||||
| } from '@mui/material'; | ||||
| import { Box, Paper, useMediaQuery, useTheme } from '@mui/material'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/PlaygroundConnectionFieldset'; | ||||
| import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable'; | ||||
| import { ContextBanner } from './PlaygroundResultsTable/ContextBanner/ContextBanner'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import usePlaygroundApi from 'hooks/api/actions/usePlayground/usePlayground'; | ||||
| import { usePlaygroundApi } from 'hooks/api/actions/usePlayground/usePlayground'; | ||||
| import { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model'; | ||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||
| import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm'; | ||||
| import { | ||||
|     resolveDefaultEnvironment, | ||||
|     resolveProjects, | ||||
|     resolveResultsWidth, | ||||
| } from './playground.utils'; | ||||
| import { PlaygroundGuidance } from './PlaygroundGuidance/PlaygroundGuidance'; | ||||
| import { PlaygroundGuidancePopper } from './PlaygroundGuidancePopper/PlaygroundGuidancePopper'; | ||||
| 
 | ||||
| interface IPlaygroundProps {} | ||||
| 
 | ||||
| export const Playground: VFC<IPlaygroundProps> = () => { | ||||
| export const Playground: VFC<{}> = () => { | ||||
|     const { environments } = useEnvironments(); | ||||
|     const theme = useTheme(); | ||||
|     const [environment, onSetEnvironment] = useState<string>(''); | ||||
|     const [projects, onSetProjects] = useState<string[]>([]); | ||||
|     const matches = useMediaQuery(theme.breakpoints.down('lg')); | ||||
| 
 | ||||
|     const [environment, setEnvironment] = useState<string>(''); | ||||
|     const [projects, setProjects] = useState<string[]>([]); | ||||
|     const [context, setContext] = useState<string>(); | ||||
|     const [results, setResults] = useState< | ||||
|         PlaygroundResponseSchema | undefined | ||||
| @ -34,21 +34,42 @@ export const Playground: VFC<IPlaygroundProps> = () => { | ||||
|     const [searchParams, setSearchParams] = useSearchParams(); | ||||
|     const { evaluatePlayground, loading } = usePlaygroundApi(); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setEnvironment(resolveDefaultEnvironment(environments)); | ||||
|     }, [environments]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         // Load initial values from URL
 | ||||
|         try { | ||||
|             const environmentFromUrl = searchParams.get('environment'); | ||||
|             if (environmentFromUrl) { | ||||
|                 onSetEnvironment(environmentFromUrl); | ||||
|                 setEnvironment(environmentFromUrl); | ||||
|             } | ||||
|             const projectsFromUrl = searchParams.get('projects'); | ||||
| 
 | ||||
|             let projectsArray: string[]; | ||||
|             let projectsFromUrl = searchParams.get('projects'); | ||||
|             if (projectsFromUrl) { | ||||
|                 onSetProjects(projectsFromUrl.split(',')); | ||||
|                 projectsArray = projectsFromUrl.split(','); | ||||
|                 setProjects(projectsArray); | ||||
|             } | ||||
|             const contextFromUrl = searchParams.get('context'); | ||||
| 
 | ||||
|             let contextFromUrl = searchParams.get('context'); | ||||
|             if (contextFromUrl) { | ||||
|                 setContext(decodeURI(contextFromUrl)); | ||||
|                 contextFromUrl = decodeURI(contextFromUrl); | ||||
|                 setContext(contextFromUrl); | ||||
|             } | ||||
| 
 | ||||
|             const makePlaygroundRequest = async () => { | ||||
|                 if (environmentFromUrl && contextFromUrl) { | ||||
|                     await evaluatePlaygroundContext( | ||||
|                         environmentFromUrl, | ||||
|                         projectsArray || '*', | ||||
|                         contextFromUrl | ||||
|                     ); | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             makePlaygroundRequest(); | ||||
|         } catch (error) { | ||||
|             setToastData({ | ||||
|                 type: 'error', | ||||
| @ -60,40 +81,27 @@ export const Playground: VFC<IPlaygroundProps> = () => { | ||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|     }, []); | ||||
| 
 | ||||
|     const onSubmit: FormEventHandler<HTMLFormElement> = async event => { | ||||
|         event.preventDefault(); | ||||
| 
 | ||||
|     const evaluatePlaygroundContext = async ( | ||||
|         environment: string, | ||||
|         projects: string[] | string, | ||||
|         context: string | undefined, | ||||
|         action?: () => void | ||||
|     ) => { | ||||
|         try { | ||||
|             const parsedContext = JSON.parse(context || '{}'); | ||||
|             const response = await evaluatePlayground({ | ||||
|                 environment, | ||||
|                 projects: | ||||
|                     !projects || | ||||
|                     projects.length === 0 || | ||||
|                     (projects.length === 1 && projects[0] === '*') | ||||
|                         ? '*' | ||||
|                         : projects, | ||||
|                 projects: resolveProjects(projects), | ||||
|                 context: { | ||||
|                     appName: 'playground', | ||||
|                     ...parsedContext, | ||||
|                 }, | ||||
|             }); | ||||
| 
 | ||||
|             // Set URL search parameters
 | ||||
|             searchParams.set('context', encodeURI(context || '')); // always set because of native validation
 | ||||
|             searchParams.set('environment', environment); | ||||
|             if ( | ||||
|                 Array.isArray(projects) && | ||||
|                 projects.length > 0 && | ||||
|                 !(projects.length === 1 && projects[0] === '*') | ||||
|             ) { | ||||
|                 searchParams.set('projects', projects.join(',')); | ||||
|             } else { | ||||
|                 searchParams.delete('projects'); | ||||
|             if (action && typeof action === 'function') { | ||||
|                 action(); | ||||
|             } | ||||
|             setSearchParams(searchParams); | ||||
| 
 | ||||
|             // Display results
 | ||||
|             setResults(response); | ||||
|         } catch (error: unknown) { | ||||
|             setToastData({ | ||||
| @ -103,88 +111,104 @@ export const Playground: VFC<IPlaygroundProps> = () => { | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onSubmit: FormEventHandler<HTMLFormElement> = async event => { | ||||
|         event.preventDefault(); | ||||
| 
 | ||||
|         await evaluatePlaygroundContext( | ||||
|             environment, | ||||
|             projects, | ||||
|             context, | ||||
|             setURLParameters | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     const setURLParameters = () => { | ||||
|         searchParams.set('context', encodeURI(context || '')); // always set because of native validation
 | ||||
|         searchParams.set('environment', environment); | ||||
|         if ( | ||||
|             Array.isArray(projects) && | ||||
|             projects.length > 0 && | ||||
|             !(projects.length === 1 && projects[0] === '*') | ||||
|         ) { | ||||
|             searchParams.set('projects', projects.join(',')); | ||||
|         } else { | ||||
|             searchParams.delete('projects'); | ||||
|         } | ||||
|         setSearchParams(searchParams); | ||||
|     }; | ||||
| 
 | ||||
|     const formWidth = results && !matches ? '35%' : 'auto'; | ||||
|     const resultsWidth = resolveResultsWidth(matches, results); | ||||
| 
 | ||||
|     return ( | ||||
|         <PageContent | ||||
|             header={<PageHeader title="Unleash playground" />} | ||||
|             header={ | ||||
|                 <PageHeader | ||||
|                     title="Unleash playground" | ||||
|                     actions={<PlaygroundGuidancePopper />} | ||||
|                 /> | ||||
|             } | ||||
|             disableLoading | ||||
|             bodyClass={'no-padding'} | ||||
|         > | ||||
|             <Paper | ||||
|                 elevation={0} | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     px: 4, | ||||
|                     py: 3, | ||||
|                     mb: 4, | ||||
|                     m: 4, | ||||
|                     background: theme.palette.grey[200], | ||||
|                     display: 'flex', | ||||
|                     flexDirection: !matches ? 'row' : 'column', | ||||
|                 }} | ||||
|             > | ||||
|                 <Box component="form" onSubmit={onSubmit}> | ||||
|                     <Typography | ||||
|                 <Box | ||||
|                     sx={{ | ||||
|                         background: theme.palette.grey[200], | ||||
|                         borderBottomLeftRadius: theme.shape.borderRadiusMedium, | ||||
|                     }} | ||||
|                 > | ||||
|                     <Paper | ||||
|                         elevation={0} | ||||
|                         sx={{ | ||||
|                             mb: 3, | ||||
|                             px: 4, | ||||
|                             py: 3, | ||||
|                             mb: 4, | ||||
|                             mt: 2, | ||||
|                             background: theme.palette.grey[200], | ||||
|                             transition: 'width 0.4s ease', | ||||
|                             minWidth: matches ? 'auto' : '500px', | ||||
|                             width: formWidth, | ||||
|                             position: 'sticky', | ||||
|                             top: 0, | ||||
|                         }} | ||||
|                     > | ||||
|                         Configure playground | ||||
|                     </Typography> | ||||
|                     <PlaygroundConnectionFieldset | ||||
|                         environment={environment} | ||||
|                         projects={projects} | ||||
|                         setEnvironment={onSetEnvironment} | ||||
|                         setProjects={onSetProjects} | ||||
|                     /> | ||||
|                     <Divider | ||||
|                         variant="fullWidth" | ||||
|                         sx={{ | ||||
|                             mb: 2, | ||||
|                             borderColor: theme.palette.dividerAlternative, | ||||
|                             borderStyle: 'dashed', | ||||
|                         }} | ||||
|                     /> | ||||
|                     <PlaygroundCodeFieldset | ||||
|                         value={context} | ||||
|                         setValue={setContext} | ||||
|                     /> | ||||
|                     <Divider | ||||
|                         variant="fullWidth" | ||||
|                         sx={{ | ||||
|                             mt: 3, | ||||
|                             mb: 2, | ||||
|                             borderColor: theme.palette.dividerAlternative, | ||||
|                         }} | ||||
|                     /> | ||||
|                     <Button variant="contained" size="large" type="submit"> | ||||
|                         Try configuration | ||||
|                     </Button> | ||||
|                 </Box> | ||||
|             </Paper> | ||||
|             <ConditionallyRender | ||||
|                 condition={Boolean(results)} | ||||
|                 show={ | ||||
|                     <> | ||||
|                         <Divider /> | ||||
|                         <ContextBanner | ||||
|                             environment={ | ||||
|                                 (results as PlaygroundResponseSchema)?.input | ||||
|                                     ?.environment | ||||
|                             } | ||||
|                             projects={ | ||||
|                                 (results as PlaygroundResponseSchema)?.input | ||||
|                                     ?.projects | ||||
|                             } | ||||
|                             context={ | ||||
|                                 (results as PlaygroundResponseSchema)?.input | ||||
|                                     ?.context | ||||
|                             } | ||||
|                         <PlaygroundForm | ||||
|                             onSubmit={onSubmit} | ||||
|                             context={context} | ||||
|                             setContext={setContext} | ||||
|                             environments={environments} | ||||
|                             projects={projects} | ||||
|                             environment={environment} | ||||
|                             setProjects={setProjects} | ||||
|                             setEnvironment={setEnvironment} | ||||
|                         /> | ||||
|                     </> | ||||
|                 } | ||||
|             /> | ||||
| 
 | ||||
|             <PlaygroundResultsTable | ||||
|                 loading={loading} | ||||
|                 features={results?.features} | ||||
|             /> | ||||
|                     </Paper> | ||||
|                 </Box> | ||||
|                 <Box | ||||
|                     sx={theme => ({ | ||||
|                         width: resultsWidth, | ||||
|                         transition: 'width 0.4s ease', | ||||
|                         padding: theme.spacing(4, 2), | ||||
|                     })} | ||||
|                 > | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(results)} | ||||
|                         show={ | ||||
|                             <PlaygroundResultsTable | ||||
|                                 loading={loading} | ||||
|                                 features={results?.features} | ||||
|                             /> | ||||
|                         } | ||||
|                         elseShow={<PlaygroundGuidance />} | ||||
|                     /> | ||||
|                 </Box> | ||||
|             </Box> | ||||
|         </PageContent> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -1,170 +0,0 @@ | ||||
| import { | ||||
|     Dispatch, | ||||
|     SetStateAction, | ||||
|     useEffect, | ||||
|     useMemo, | ||||
|     useState, | ||||
|     VFC, | ||||
| } from 'react'; | ||||
| import { | ||||
|     Box, | ||||
|     Button, | ||||
|     FormControl, | ||||
|     InputLabel, | ||||
|     MenuItem, | ||||
|     Select, | ||||
|     TextField, | ||||
|     Typography, | ||||
|     useTheme, | ||||
| } from '@mui/material'; | ||||
| import { debounce } from 'debounce'; | ||||
| import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| 
 | ||||
| interface IPlaygroundCodeFieldsetProps { | ||||
|     value: string | undefined; | ||||
|     setValue: Dispatch<SetStateAction<string | undefined>>; | ||||
| } | ||||
| 
 | ||||
| export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({ | ||||
|     value, | ||||
|     setValue, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const { setToastData } = useToast(); | ||||
|     const { context: contextData } = useUnleashContext(); | ||||
|     const contextOptions = contextData | ||||
|         .sort((a, b) => a.sortOrder - b.sortOrder) | ||||
|         .map(({ name }) => name); | ||||
|     const [error, setError] = useState<string>(); | ||||
|     const [fieldExist, setFieldExist] = useState<boolean>(false); | ||||
|     const [contextField, setContextField] = useState<string>(''); | ||||
|     const [contextValue, setContextValue] = useState<string>(''); | ||||
|     const debounceJsonParsing = useMemo( | ||||
|         () => | ||||
|             debounce((input?: string) => { | ||||
|                 if (!input) { | ||||
|                     return setError(undefined); | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     const contextValue = JSON.parse(input); | ||||
| 
 | ||||
|                     setFieldExist(contextValue[contextField] !== undefined); | ||||
|                 } catch (error: unknown) { | ||||
|                     return setError(formatUnknownError(error)); | ||||
|                 } | ||||
| 
 | ||||
|                 return setError(undefined); | ||||
|             }, 250), | ||||
|         [setError, contextField, setFieldExist] | ||||
|     ); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         debounceJsonParsing(value); | ||||
|     }, [debounceJsonParsing, value]); | ||||
| 
 | ||||
|     const onAddField = () => { | ||||
|         try { | ||||
|             const currentValue = JSON.parse(value || '{}'); | ||||
|             setValue( | ||||
|                 JSON.stringify( | ||||
|                     { | ||||
|                         ...currentValue, | ||||
|                         [contextField]: contextValue, | ||||
|                     }, | ||||
|                     null, | ||||
|                     2 | ||||
|                 ) | ||||
|             ); | ||||
|             setContextValue(''); | ||||
|         } catch (error) { | ||||
|             setToastData({ | ||||
|                 type: 'error', | ||||
|                 title: `Error parsing context: ${formatUnknownError(error)}`, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Box> | ||||
|             <Typography | ||||
|                 variant="body2" | ||||
|                 sx={{ mb: 2 }} | ||||
|                 color={theme.palette.text.secondary} | ||||
|             > | ||||
|                 Unleash context | ||||
|             </Typography> | ||||
|             <TextField | ||||
|                 error={Boolean(error)} | ||||
|                 helperText={error} | ||||
|                 autoCorrect="off" | ||||
|                 spellCheck={false} | ||||
|                 multiline | ||||
|                 label="JSON" | ||||
|                 placeholder={JSON.stringify( | ||||
|                     { | ||||
|                         currentTime: '2022-07-04T14:13:03.929Z', | ||||
|                         appName: 'playground', | ||||
|                         userId: 'test', | ||||
|                         remoteAddress: '127.0.0.1', | ||||
|                     }, | ||||
|                     null, | ||||
|                     2 | ||||
|                 )} | ||||
|                 fullWidth | ||||
|                 InputLabelProps={{ shrink: true }} | ||||
|                 InputProps={{ minRows: 5 }} | ||||
|                 value={value} | ||||
|                 onChange={event => setValue(event.target.value)} | ||||
|             /> | ||||
|             <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}> | ||||
|                 <FormControl> | ||||
|                     <InputLabel id="context-field-label" size="small"> | ||||
|                         Context field | ||||
|                     </InputLabel> | ||||
|                     <Select | ||||
|                         label="Context field" | ||||
|                         labelId="context-field-label" | ||||
|                         id="context-field" | ||||
|                         value={contextField} | ||||
|                         onChange={event => | ||||
|                             setContextField(event.target.value || '') | ||||
|                         } | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         sx={{ width: 300, maxWidth: '100%' }} | ||||
|                     > | ||||
|                         {contextOptions.map(option => ( | ||||
|                             <MenuItem key={option} value={option}> | ||||
|                                 {option} | ||||
|                             </MenuItem> | ||||
|                         ))} | ||||
|                     </Select> | ||||
|                 </FormControl> | ||||
|                 <TextField | ||||
|                     label="Value" | ||||
|                     id="context-value" | ||||
|                     sx={{ width: 300, maxWidth: '100%' }} | ||||
|                     size="small" | ||||
|                     value={contextValue} | ||||
|                     onChange={event => | ||||
|                         setContextValue(event.target.value || '') | ||||
|                     } | ||||
|                 /> | ||||
|                 <Button | ||||
|                     variant="outlined" | ||||
|                     disabled={!contextField || Boolean(error)} | ||||
|                     onClick={onAddField} | ||||
|                 > | ||||
|                     {`${ | ||||
|                         !fieldExist | ||||
|                             ? 'Add context field' | ||||
|                             : 'Replace context field value' | ||||
|                     } `}
 | ||||
|                 </Button> | ||||
|             </Box> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,237 @@ | ||||
| import { | ||||
|     Dispatch, | ||||
|     FormEvent, | ||||
|     SetStateAction, | ||||
|     useEffect, | ||||
|     useMemo, | ||||
|     useState, | ||||
|     VFC, | ||||
| } from 'react'; | ||||
| import { | ||||
|     Box, | ||||
|     Button, | ||||
|     FormControl, | ||||
|     InputLabel, | ||||
|     MenuItem, | ||||
|     Select, | ||||
|     TextField, | ||||
|     Typography, | ||||
|     useTheme, | ||||
|     Autocomplete, | ||||
| } from '@mui/material'; | ||||
| 
 | ||||
| import { debounce } from 'debounce'; | ||||
| import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { PlaygroundEditor } from './PlaygroundEditor/PlaygroundEditor'; | ||||
| import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator'; | ||||
| import { parseDateValue, parseValidDate } from 'component/common/util'; | ||||
| interface IPlaygroundCodeFieldsetProps { | ||||
|     context: string | undefined; | ||||
|     setContext: Dispatch<SetStateAction<string | undefined>>; | ||||
| } | ||||
| 
 | ||||
| export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({ | ||||
|     context, | ||||
|     setContext, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const { setToastData } = useToast(); | ||||
|     const { context: contextData } = useUnleashContext(); | ||||
|     const contextOptions = contextData | ||||
|         .sort((a, b) => a.sortOrder - b.sortOrder) | ||||
|         .map(({ name }) => name); | ||||
|     const [error, setError] = useState<string>(); | ||||
|     const [fieldExist, setFieldExist] = useState<boolean>(false); | ||||
|     const [contextField, setContextField] = useState<string>(''); | ||||
|     const [contextValue, setContextValue] = useState<string>(''); | ||||
|     const debounceJsonParsing = useMemo( | ||||
|         () => | ||||
|             debounce((input?: string) => { | ||||
|                 if (!input) { | ||||
|                     return setError(undefined); | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     const contextValue = JSON.parse(input); | ||||
| 
 | ||||
|                     setFieldExist(contextValue[contextField] !== undefined); | ||||
|                 } catch (error: unknown) { | ||||
|                     return setError(formatUnknownError(error)); | ||||
|                 } | ||||
| 
 | ||||
|                 return setError(undefined); | ||||
|             }, 250), | ||||
|         [setError, contextField, setFieldExist] | ||||
|     ); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         debounceJsonParsing(context); | ||||
|     }, [debounceJsonParsing, context]); | ||||
| 
 | ||||
|     const onAddField = () => { | ||||
|         try { | ||||
|             const currentValue = JSON.parse(context || '{}'); | ||||
|             setContext( | ||||
|                 JSON.stringify( | ||||
|                     { | ||||
|                         ...currentValue, | ||||
|                         [contextField]: contextValue, | ||||
|                     }, | ||||
|                     null, | ||||
|                     2 | ||||
|                 ) | ||||
|             ); | ||||
| 
 | ||||
|             const foundContext = contextData.find( | ||||
|                 context => context.name === contextField | ||||
|             ); | ||||
| 
 | ||||
|             if ( | ||||
|                 (foundContext?.legalValues && | ||||
|                     foundContext.legalValues.length > 0) || | ||||
|                 contextField === 'currentTime' | ||||
|             ) | ||||
|                 return; | ||||
|             setContextValue(''); | ||||
|         } catch (error) { | ||||
|             setToastData({ | ||||
|                 type: 'error', | ||||
|                 title: `Error parsing context: ${formatUnknownError(error)}`, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const resolveInput = () => { | ||||
|         if (contextField === 'currentTime') { | ||||
|             const validDate = parseValidDate(contextValue); | ||||
|             const now = new Date(); | ||||
| 
 | ||||
|             const value = validDate | ||||
|                 ? parseDateValue(validDate.toISOString()) | ||||
|                 : parseDateValue(now.toISOString()); | ||||
| 
 | ||||
|             return ( | ||||
|                 <TextField | ||||
|                     id="date" | ||||
|                     label="Date" | ||||
|                     size="small" | ||||
|                     type="datetime-local" | ||||
|                     value={value} | ||||
|                     sx={{ width: 200, maxWidth: '100%' }} | ||||
|                     onChange={e => { | ||||
|                         const parsedDate = parseValidDate(e.target.value); | ||||
|                         const dateString = parsedDate?.toISOString(); | ||||
|                         dateString && setContextValue(dateString); | ||||
|                     }} | ||||
|                     InputLabelProps={{ | ||||
|                         shrink: true, | ||||
|                     }} | ||||
|                     required | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
|         const foundField = contextData.find( | ||||
|             contextData => contextData.name === contextField | ||||
|         ); | ||||
|         if ( | ||||
|             foundField && | ||||
|             foundField.legalValues && | ||||
|             foundField.legalValues.length > 0 | ||||
|         ) { | ||||
|             const options = foundField.legalValues.map(({ value }) => value); | ||||
|             return ( | ||||
|                 <Autocomplete | ||||
|                     disablePortal | ||||
|                     id="context-legal-values" | ||||
|                     size="small" | ||||
|                     onChange={(e: FormEvent, newValue) => { | ||||
|                         if (typeof newValue === 'string') { | ||||
|                             return setContextValue(newValue); | ||||
|                         } | ||||
|                     }} | ||||
|                     options={options} | ||||
|                     sx={{ width: 200, maxWidth: '100%' }} | ||||
|                     renderInput={(params: any) => ( | ||||
|                         <TextField {...params} label="Value" /> | ||||
|                     )} | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <TextField | ||||
|                 label="Value" | ||||
|                 id="context-value" | ||||
|                 sx={{ width: 200, maxWidth: '100%' }} | ||||
|                 size="small" | ||||
|                 value={contextValue} | ||||
|                 onChange={event => setContextValue(event.target.value || '')} | ||||
|             /> | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Box> | ||||
|             <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> | ||||
|                 <GuidanceIndicator type="secondary">2</GuidanceIndicator> | ||||
|                 <Typography | ||||
|                     variant="body2" | ||||
|                     color={theme.palette.text.secondary} | ||||
|                     sx={{ ml: 1 }} | ||||
|                 > | ||||
|                     Unleash context | ||||
|                 </Typography> | ||||
|             </Box> | ||||
| 
 | ||||
|             <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }}> | ||||
|                 <FormControl> | ||||
|                     <InputLabel id="context-field-label" size="small"> | ||||
|                         Context field | ||||
|                     </InputLabel> | ||||
|                     <Select | ||||
|                         label="Context field" | ||||
|                         labelId="context-field-label" | ||||
|                         id="context-field" | ||||
|                         value={contextField} | ||||
|                         onChange={event => { | ||||
|                             setContextField(event.target.value || ''); | ||||
| 
 | ||||
|                             if (event.target.value === 'currentTime') { | ||||
|                                 return setContextValue( | ||||
|                                     new Date().toISOString() | ||||
|                                 ); | ||||
|                             } | ||||
|                             setContextValue(''); | ||||
|                         }} | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         sx={{ width: 200, maxWidth: '100%' }} | ||||
|                     > | ||||
|                         {contextOptions.map(option => ( | ||||
|                             <MenuItem key={option} value={option}> | ||||
|                                 {option} | ||||
|                             </MenuItem> | ||||
|                         ))} | ||||
|                     </Select> | ||||
|                 </FormControl> | ||||
|                 {resolveInput()} | ||||
|                 <Button | ||||
|                     variant="outlined" | ||||
|                     disabled={!contextField || Boolean(error)} | ||||
|                     onClick={onAddField} | ||||
|                     sx={{ width: '95px' }} | ||||
|                 > | ||||
|                     {`${!fieldExist ? 'Add' : 'Replace'} `} | ||||
|                 </Button> | ||||
|             </Box> | ||||
| 
 | ||||
|             <PlaygroundEditor | ||||
|                 context={context} | ||||
|                 setContext={setContext} | ||||
|                 error={error} | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,133 @@ | ||||
| import CodeMirror from '@uiw/react-codemirror'; | ||||
| import { json } from '@codemirror/lang-json'; | ||||
| import { Dispatch, SetStateAction, VFC, useCallback } from 'react'; | ||||
| import { styled, useTheme, Box } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import Check from '@mui/icons-material/Check'; | ||||
| import { Error } from '@mui/icons-material'; | ||||
| 
 | ||||
| interface IPlaygroundEditorProps { | ||||
|     context: string | undefined; | ||||
|     setContext: Dispatch<SetStateAction<string | undefined>>; | ||||
|     error: string | undefined; | ||||
| } | ||||
| 
 | ||||
| const StyledEditorHeader = styled('aside')(({ theme }) => ({ | ||||
|     height: '50px', | ||||
|     backgroundColor: theme.palette.grey[100], | ||||
|     borderTopRightRadius: theme.shape.borderRadiusMedium, | ||||
|     borderTopLeftRadius: theme.shape.borderRadiusMedium, | ||||
|     padding: theme.spacing(1, 2), | ||||
|     color: theme.palette.text.primary, | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
|     border: `1px solid ${theme.palette.lightBorder}`, | ||||
|     borderBottom: 'none', | ||||
| })); | ||||
| 
 | ||||
| const StyledEditorStatusContainer = styled('div')(({ theme, style }) => ({ | ||||
|     width: '28px', | ||||
|     height: '28px', | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     justifyContent: 'center', | ||||
|     transition: `background-color 0.5s ease-in-out`, | ||||
|     borderRadius: '50%', | ||||
|     opacity: 0.8, | ||||
|     ...style, | ||||
| })); | ||||
| 
 | ||||
| const StyledErrorSpan = styled('div')(({ theme }) => ({ | ||||
|     fontSize: '0.9rem', | ||||
|     color: theme.palette.error.main, | ||||
|     marginRight: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const EditorStatusOk = () => { | ||||
|     const theme = useTheme(); | ||||
|     return ( | ||||
|         <StyledEditorStatusContainer | ||||
|             style={{ | ||||
|                 color: theme.palette.text.tertiaryContrast, | ||||
|                 backgroundColor: theme.palette.success.main, | ||||
|             }} | ||||
|         > | ||||
|             <Check sx={{ width: '20px', height: '20px' }} /> | ||||
|         </StyledEditorStatusContainer> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const EditorStatusError = () => { | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledEditorStatusContainer | ||||
|             style={{ | ||||
|                 color: theme.palette.text.tertiaryContrast, | ||||
|                 backgroundColor: theme.palette.error.main, | ||||
|             }} | ||||
|         > | ||||
|             <Error /> | ||||
|         </StyledEditorStatusContainer> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const PlaygroundEditor: VFC<IPlaygroundEditorProps> = ({ | ||||
|     context, | ||||
|     setContext, | ||||
|     error, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const onCodeFieldChange = useCallback( | ||||
|         context => { | ||||
|             setContext(context); | ||||
|         }, | ||||
|         [setContext] | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ width: '100%' }}> | ||||
|             <StyledEditorHeader> | ||||
|                 JSON | ||||
|                 <ConditionallyRender | ||||
|                     condition={Boolean(error)} | ||||
|                     show={ | ||||
|                         <Box | ||||
|                             sx={theme => ({ | ||||
|                                 display: 'flex', | ||||
|                                 alignItems: 'center', | ||||
|                             })} | ||||
|                         > | ||||
|                             <StyledErrorSpan>{error}</StyledErrorSpan> | ||||
|                             <EditorStatusError /> | ||||
|                         </Box> | ||||
|                     } | ||||
|                     elseShow={<EditorStatusOk />} | ||||
|                 /> | ||||
|             </StyledEditorHeader> | ||||
|             <CodeMirror | ||||
|                 value={context} | ||||
|                 height="200px" | ||||
|                 extensions={[json()]} | ||||
|                 onChange={onCodeFieldChange} | ||||
|                 style={{ | ||||
|                     border: `1px solid ${theme.palette.lightBorder}`, | ||||
|                     borderTop: 'none', | ||||
|                     borderBottomLeftRadius: theme.shape.borderRadiusMedium, | ||||
|                     borderBottomRightRadius: theme.shape.borderRadiusMedium, | ||||
|                 }} | ||||
|                 placeholder={JSON.stringify( | ||||
|                     { | ||||
|                         currentTime: '2022-07-04T14:13:03.929Z', | ||||
|                         appName: 'playground', | ||||
|                         userId: 'test', | ||||
|                         remoteAddress: '127.0.0.1', | ||||
|                     }, | ||||
|                     null, | ||||
|                     2 | ||||
|                 )} | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -8,12 +8,14 @@ import { | ||||
| } from '@mui/material'; | ||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||
| import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||
| import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator'; | ||||
| 
 | ||||
| interface IPlaygroundConnectionFieldsetProps { | ||||
|     environment: string; | ||||
|     projects: string[]; | ||||
|     setProjects: (projects: string[]) => void; | ||||
|     setEnvironment: (environment: string) => void; | ||||
|     environmentOptions: string[]; | ||||
| } | ||||
| 
 | ||||
| interface IOption { | ||||
| @ -25,13 +27,14 @@ const allOption: IOption = { label: 'ALL', id: '*' }; | ||||
| 
 | ||||
| export const PlaygroundConnectionFieldset: VFC< | ||||
|     IPlaygroundConnectionFieldsetProps | ||||
| > = ({ environment, projects, setProjects, setEnvironment }) => { | ||||
| > = ({ | ||||
|     environment, | ||||
|     projects, | ||||
|     setProjects, | ||||
|     setEnvironment, | ||||
|     environmentOptions, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const { environments } = useEnvironments(); | ||||
|     const environmentOptions = environments | ||||
|         .filter(({ enabled }) => Boolean(enabled)) | ||||
|         .sort((a, b) => a.sortOrder - b.sortOrder) | ||||
|         .map(({ name }) => name); | ||||
| 
 | ||||
|     const { projects: availableProjects = [] } = useProjects(); | ||||
|     const projectsOptions = [ | ||||
| @ -74,19 +77,22 @@ export const PlaygroundConnectionFieldset: VFC< | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ pb: 2 }}> | ||||
|             <Typography | ||||
|                 variant="body2" | ||||
|                 sx={{ mb: 2 }} | ||||
|                 color={theme.palette.text.secondary} | ||||
|             > | ||||
|                 Access configuration | ||||
|             </Typography> | ||||
|             <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> | ||||
|                 <GuidanceIndicator type="secondary">1</GuidanceIndicator> | ||||
|                 <Typography | ||||
|                     variant="body2" | ||||
|                     color={theme.palette.text.secondary} | ||||
|                     sx={{ ml: 1 }} | ||||
|                 > | ||||
|                     Access configuration | ||||
|                 </Typography> | ||||
|             </Box> | ||||
|             <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}> | ||||
|                 <Autocomplete | ||||
|                     disablePortal | ||||
|                     id="environment" | ||||
|                     options={environmentOptions} | ||||
|                     sx={{ width: 300, maxWidth: '100%' }} | ||||
|                     sx={{ width: 200, maxWidth: '100%' }} | ||||
|                     renderInput={params => ( | ||||
|                         <TextField {...params} label="Environment" required /> | ||||
|                     )} | ||||
| @ -99,7 +105,7 @@ export const PlaygroundConnectionFieldset: VFC< | ||||
|                     id="projects" | ||||
|                     multiple={!isAllProjects} | ||||
|                     options={projectsOptions} | ||||
|                     sx={{ width: 300, maxWidth: '100%' }} | ||||
|                     sx={{ width: 200, maxWidth: '100%' }} | ||||
|                     renderInput={params => ( | ||||
|                         <TextField {...params} label="Projects" /> | ||||
|                     )} | ||||
| @ -0,0 +1,86 @@ | ||||
| import { Box, Button, Divider, useTheme } from '@mui/material'; | ||||
| import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator'; | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| import { FormEvent, VFC } from 'react'; | ||||
| import { getEnvironmentOptions } from '../playground.utils'; | ||||
| import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset'; | ||||
| import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/PlaygroundConnectionFieldset'; | ||||
| 
 | ||||
| interface IPlaygroundFormProps { | ||||
|     environments: IEnvironment[]; | ||||
|     onSubmit: (event: FormEvent<HTMLFormElement>) => void; | ||||
|     environment: string; | ||||
|     projects: string[]; | ||||
|     setProjects: React.Dispatch<React.SetStateAction<string[]>>; | ||||
|     setEnvironment: React.Dispatch<React.SetStateAction<string>>; | ||||
|     context: string | undefined; | ||||
|     setContext: React.Dispatch<React.SetStateAction<string | undefined>>; | ||||
| } | ||||
| 
 | ||||
| export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({ | ||||
|     environments, | ||||
|     environment, | ||||
|     onSubmit, | ||||
|     projects, | ||||
|     setProjects, | ||||
|     setEnvironment, | ||||
|     context, | ||||
|     setContext, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box | ||||
|             component="form" | ||||
|             onSubmit={onSubmit} | ||||
|             sx={{ | ||||
|                 display: 'flex', | ||||
|                 flexDirection: 'column', | ||||
|             }} | ||||
|         > | ||||
|             <PlaygroundConnectionFieldset | ||||
|                 environment={environment} | ||||
|                 projects={projects} | ||||
|                 setEnvironment={setEnvironment} | ||||
|                 setProjects={setProjects} | ||||
|                 environmentOptions={getEnvironmentOptions(environments)} | ||||
|             /> | ||||
|             <Divider | ||||
|                 variant="fullWidth" | ||||
|                 sx={{ | ||||
|                     mb: 2, | ||||
|                     borderColor: theme.palette.dividerAlternative, | ||||
|                     borderStyle: 'dashed', | ||||
|                 }} | ||||
|             /> | ||||
|             <PlaygroundCodeFieldset context={context} setContext={setContext} /> | ||||
| 
 | ||||
|             <Divider | ||||
|                 variant="fullWidth" | ||||
|                 sx={{ | ||||
|                     mt: 3, | ||||
|                     mb: 2, | ||||
|                     borderColor: theme.palette.dividerAlternative, | ||||
|                 }} | ||||
|             /> | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     display: 'flex', | ||||
|                     alignItems: 'center', | ||||
|                     justifyContent: 'space-between', | ||||
|                 }} | ||||
|             > | ||||
|                 <GuidanceIndicator type="secondary">3</GuidanceIndicator> | ||||
| 
 | ||||
|                 <Button | ||||
|                     variant="contained" | ||||
|                     size="large" | ||||
|                     type="submit" | ||||
|                     sx={{ marginLeft: 'auto' }} | ||||
|                 > | ||||
|                     Try configuration | ||||
|                 </Button> | ||||
|             </Box> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,40 @@ | ||||
| import { Typography, Box, Divider } from '@mui/material'; | ||||
| import { PlaygroundGuidanceSection } from './PlaygroundGuidanceSection/PlaygroundGuidanceSection'; | ||||
| 
 | ||||
| export const PlaygroundGuidance = () => { | ||||
|     return ( | ||||
|         <Box sx={{ ml: 4 }}> | ||||
|             <Typography variant="body1"> | ||||
|                 Unleash playground is for helping you to undestand how unleash | ||||
|                 works, how feature toggles are evaluated and for you to easily | ||||
|                 debug your feature toggles. | ||||
|             </Typography> | ||||
| 
 | ||||
|             <Divider sx={{ mt: 2, mb: 2 }} /> | ||||
| 
 | ||||
|             <Typography variant="body1" sx={{ mb: 1 }}> | ||||
|                 What you need to do is: | ||||
|             </Typography> | ||||
| 
 | ||||
|             <PlaygroundGuidanceSection | ||||
|                 headerText="Select in which environment you want to test your | ||||
|                             feature toggle configuration" | ||||
|                 bodyText="You can also specify specific projects, or check | ||||
|                             toggles in all projects." | ||||
|                 sectionNumber="1" | ||||
|             /> | ||||
| 
 | ||||
|             <PlaygroundGuidanceSection | ||||
|                 headerText="Select a context field that you'd like to check" | ||||
|                 bodyText="You can configure as many context fields context fields as you want. You can also leave the context empty to test against an empty context." | ||||
|                 sectionNumber="2" | ||||
|             /> | ||||
| 
 | ||||
|             <PlaygroundGuidanceSection | ||||
|                 headerText="Submit the form to try the configuration" | ||||
|                 bodyText="The results of evaluating your feature toggles will appear after you submit the form. Then view the results." | ||||
|                 sectionNumber="3" | ||||
|             /> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,44 @@ | ||||
| import { Box, Typography } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator'; | ||||
| import { VFC } from 'react'; | ||||
| 
 | ||||
| interface IPlaygroundGuidanceSectionProps { | ||||
|     headerText: string; | ||||
|     bodyText?: string; | ||||
|     sectionNumber: string; | ||||
| } | ||||
| 
 | ||||
| export const PlaygroundGuidanceSection: VFC< | ||||
|     IPlaygroundGuidanceSectionProps | ||||
| > = ({ headerText, bodyText, sectionNumber }) => { | ||||
|     return ( | ||||
|         <Box | ||||
|             sx={{ | ||||
|                 display: 'flex', | ||||
|                 alignItems: 'flex-start', | ||||
|                 mt: 2, | ||||
|                 flexDirection: 'column', | ||||
|             }} | ||||
|         > | ||||
|             <Box sx={{ display: 'flex' }}> | ||||
|                 <Box> | ||||
|                     <GuidanceIndicator>{sectionNumber}</GuidanceIndicator> | ||||
|                 </Box> | ||||
|                 <Box sx={{ ml: 2, display: 'flex', flexDirection: 'column' }}> | ||||
|                     <Typography variant="body1" sx={{ fontWeight: 'bold' }}> | ||||
|                         {headerText} | ||||
|                     </Typography> | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(bodyText)} | ||||
|                         show={ | ||||
|                             <Typography variant="body1" sx={{ mt: 1 }}> | ||||
|                                 {bodyText} | ||||
|                             </Typography> | ||||
|                         } | ||||
|                     /> | ||||
|                 </Box> | ||||
|             </Box> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,49 @@ | ||||
| import { useState } from 'react'; | ||||
| 
 | ||||
| import { Close, Help } from '@mui/icons-material'; | ||||
| import { Box, IconButton, Popper, Paper } from '@mui/material'; | ||||
| import { PlaygroundGuidance } from '../PlaygroundGuidance/PlaygroundGuidance'; | ||||
| 
 | ||||
| export const PlaygroundGuidancePopper = () => { | ||||
|     const [anchor, setAnchorEl] = useState<null | Element>(null); | ||||
| 
 | ||||
|     const onOpen = (event: React.FormEvent<HTMLButtonElement>) => | ||||
|         setAnchorEl(event.currentTarget); | ||||
| 
 | ||||
|     const onClose = () => setAnchorEl(null); | ||||
| 
 | ||||
|     const open = Boolean(anchor); | ||||
| 
 | ||||
|     const id = 'playground-guidance-popper'; | ||||
| 
 | ||||
|     return ( | ||||
|         <Box> | ||||
|             <IconButton onClick={onOpen} aria-describedby={id}> | ||||
|                 <Help /> | ||||
|             </IconButton> | ||||
| 
 | ||||
|             <Popper | ||||
|                 id={id} | ||||
|                 open={open} | ||||
|                 anchorEl={anchor} | ||||
|                 sx={theme => ({ zIndex: theme.zIndex.tooltip })} | ||||
|             > | ||||
|                 <Paper | ||||
|                     sx={theme => ({ | ||||
|                         padding: theme.spacing(8, 4), | ||||
|                         maxWidth: '500px', | ||||
|                         borderRadius: theme.shape.borderRadiusExtraLarge, | ||||
|                     })} | ||||
|                 > | ||||
|                     <IconButton | ||||
|                         onClick={onClose} | ||||
|                         sx={{ position: 'absolute', right: 25, top: 15 }} | ||||
|                     > | ||||
|                         <Close /> | ||||
|                     </IconButton> | ||||
|                     <PlaygroundGuidance /> | ||||
|                 </Paper> | ||||
|             </Popper> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -1,8 +1,7 @@ | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
| import { useSearchParams } from 'react-router-dom'; | ||||
| import { SortingRule, useGlobalFilter, useSortBy, useTable } from 'react-table'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| 
 | ||||
| import { | ||||
|     SortableTableHeader, | ||||
|     Table, | ||||
| @ -21,6 +20,10 @@ import { useSearch } from 'hooks/useSearch'; | ||||
| import { createLocalStorage } from 'utils/createLocalStorage'; | ||||
| import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell'; | ||||
| import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playground.model'; | ||||
| import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; | ||||
| import useLoading from 'hooks/useLoading'; | ||||
| import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator'; | ||||
| import { VariantCell } from './VariantCell/VariantCell'; | ||||
| 
 | ||||
| const defaultSort: SortingRule<string> = { id: 'name' }; | ||||
| const { value, setValue } = createLocalStorage( | ||||
| @ -38,10 +41,13 @@ export const PlaygroundResultsTable = ({ | ||||
|     loading, | ||||
| }: IPlaygroundResultsTableProps) => { | ||||
|     const [searchParams, setSearchParams] = useSearchParams(); | ||||
| 
 | ||||
|     const ref = useLoading(loading); | ||||
|     const [searchValue, setSearchValue] = useState( | ||||
|         searchParams.get('search') || '' | ||||
|     ); | ||||
|     const theme = useTheme(); | ||||
|     const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||
| 
 | ||||
|     const { | ||||
|         data: searchedData, | ||||
| @ -54,7 +60,7 @@ export const PlaygroundResultsTable = ({ | ||||
|             ? Array(5).fill({ | ||||
|                   name: 'Feature name', | ||||
|                   projectId: 'FeatureProject', | ||||
|                   variant: { name: 'FeatureVariant' }, | ||||
|                   variant: { name: 'FeatureVariant', variants: [] }, | ||||
|                   enabled: true, | ||||
|               }) | ||||
|             : searchedData; | ||||
| @ -78,6 +84,7 @@ export const PlaygroundResultsTable = ({ | ||||
|         state: { sortBy }, | ||||
|         rows, | ||||
|         prepareRow, | ||||
|         setHiddenColumns, | ||||
|     } = useTable( | ||||
|         { | ||||
|             initialState, | ||||
| @ -95,6 +102,17 @@ export const PlaygroundResultsTable = ({ | ||||
|         useSortBy | ||||
|     ); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         const hiddenColumns = []; | ||||
|         if (isSmallScreen) { | ||||
|             hiddenColumns.push('projectId'); | ||||
|         } | ||||
|         if (isExtraSmallScreen) { | ||||
|             hiddenColumns.push('variant'); | ||||
|         } | ||||
|         setHiddenColumns(hiddenColumns); | ||||
|     }, [setHiddenColumns, isExtraSmallScreen, isSmallScreen]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if (loading) { | ||||
|             return; | ||||
| @ -122,33 +140,37 @@ export const PlaygroundResultsTable = ({ | ||||
|     }, [loading, sortBy, searchValue]); | ||||
| 
 | ||||
|     return ( | ||||
|         <PageContent | ||||
|             header={ | ||||
|                 <PageHeader | ||||
|                     titleElement={ | ||||
|                         features !== undefined | ||||
|                             ? `Results (${ | ||||
|                                   rows.length < data.length | ||||
|                                       ? `${rows.length} of ${data.length}` | ||||
|                                       : data.length | ||||
|                               })` | ||||
|                             : 'Results' | ||||
|                     } | ||||
|                     actions={ | ||||
|                         <Search | ||||
|                             initialValue={searchValue} | ||||
|                             onChange={setSearchValue} | ||||
|                             hasFilters | ||||
|                             getSearchContext={getSearchContext} | ||||
|                             disabled={loading} | ||||
|                         /> | ||||
|                     } | ||||
|         <> | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     display: 'flex', | ||||
|                     justifyContent: 'space-between', | ||||
|                     alignItems: 'center', | ||||
|                     mb: 3, | ||||
|                 }} | ||||
|             > | ||||
|                 <Typography variant="subtitle1" sx={{ ml: 1 }}> | ||||
|                     {features !== undefined && !loading | ||||
|                         ? `Results (${ | ||||
|                               rows.length < data.length | ||||
|                                   ? `${rows.length} of ${data.length}` | ||||
|                                   : data.length | ||||
|                           })` | ||||
|                         : 'Results'} | ||||
|                 </Typography> | ||||
| 
 | ||||
|                 <Search | ||||
|                     initialValue={searchValue} | ||||
|                     onChange={setSearchValue} | ||||
|                     hasFilters | ||||
|                     getSearchContext={getSearchContext} | ||||
|                     disabled={loading} | ||||
|                     containerStyles={{ marginLeft: '1rem', maxWidth: '400px' }} | ||||
|                 /> | ||||
|             } | ||||
|             isLoading={loading} | ||||
|         > | ||||
|             </Box> | ||||
| 
 | ||||
|             <ConditionallyRender | ||||
|                 condition={!loading && (!data || data.length === 0)} | ||||
|                 condition={!loading && !data} | ||||
|                 show={() => ( | ||||
|                     <TablePlaceholder> | ||||
|                         {data === undefined | ||||
| @ -157,7 +179,7 @@ export const PlaygroundResultsTable = ({ | ||||
|                     </TablePlaceholder> | ||||
|                 )} | ||||
|                 elseShow={() => ( | ||||
|                     <> | ||||
|                     <Box ref={ref}> | ||||
|                         <SearchHighlightProvider | ||||
|                             value={getSearchText(searchValue)} | ||||
|                         > | ||||
| @ -187,7 +209,9 @@ export const PlaygroundResultsTable = ({ | ||||
|                             </Table> | ||||
|                         </SearchHighlightProvider> | ||||
|                         <ConditionallyRender | ||||
|                             condition={searchValue?.length > 0} | ||||
|                             condition={ | ||||
|                                 data.length === 0 && searchValue?.length > 0 | ||||
|                             } | ||||
|                             show={ | ||||
|                                 <TablePlaceholder> | ||||
|                                     No feature toggles found matching “ | ||||
| @ -195,10 +219,21 @@ export const PlaygroundResultsTable = ({ | ||||
|                                 </TablePlaceholder> | ||||
|                             } | ||||
|                         /> | ||||
|                     </> | ||||
| 
 | ||||
|                         <ConditionallyRender | ||||
|                             condition={ | ||||
|                                 data && data.length === 0 && !searchValue | ||||
|                             } | ||||
|                             show={ | ||||
|                                 <TablePlaceholder> | ||||
|                                     No features toggles to display | ||||
|                                 </TablePlaceholder> | ||||
|                             } | ||||
|                         /> | ||||
|                     </Box> | ||||
|                 )} | ||||
|             /> | ||||
|         </PageContent> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| @ -207,7 +242,7 @@ const COLUMNS = [ | ||||
|         Header: 'Name', | ||||
|         accessor: 'name', | ||||
|         searchable: true, | ||||
|         width: '60%', | ||||
|         minWidth: 160, | ||||
|         Cell: ({ value, row: { original } }: any) => ( | ||||
|             <LinkCell | ||||
|                 title={value} | ||||
| @ -233,18 +268,26 @@ const COLUMNS = [ | ||||
|         sortType: 'alphanumeric', | ||||
|         filterName: 'variant', | ||||
|         searchable: true, | ||||
|         maxWidth: 170, | ||||
|         width: 200, | ||||
|         Cell: ({ | ||||
|             value, | ||||
|             row: { | ||||
|                 original: { variant }, | ||||
|                 original: { variant, feature, variants, isEnabled }, | ||||
|             }, | ||||
|         }: any) => <HighlightCell value={variant?.enabled ? value : ''} />, | ||||
|         }: any) => ( | ||||
|             <VariantCell | ||||
|                 variant={variant?.enabled ? value : ''} | ||||
|                 variants={variants} | ||||
|                 feature={feature} | ||||
|                 isEnabled={isEnabled} | ||||
|             /> | ||||
|         ), | ||||
|     }, | ||||
|     { | ||||
|         Header: 'isEnabled', | ||||
|         accessor: 'isEnabled', | ||||
|         maxWidth: 170, | ||||
|         filterName: 'isEnabled', | ||||
|         filterParsing: (value: boolean) => (value ? 'true' : 'false'), | ||||
|         Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />, | ||||
|         sortType: 'boolean', | ||||
|         sortInverted: true, | ||||
|  | ||||
| @ -0,0 +1,77 @@ | ||||
| import { InfoOutlined } from '@mui/icons-material'; | ||||
| import { IconButton, Popover, styled, useTheme } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import React, { useState, VFC } from 'react'; | ||||
| import { VariantInformation } from './VariantInformation/VariantInformation'; | ||||
| import { IFeatureVariant } from 'interfaces/featureToggle'; | ||||
| 
 | ||||
| interface IVariantCellProps { | ||||
|     variant: string; | ||||
|     variants: IFeatureVariant[]; | ||||
|     feature: string; | ||||
|     isEnabled: boolean; | ||||
| } | ||||
| 
 | ||||
| const StyledDiv = styled('div')(() => ({ | ||||
|     maxWidth: '100%', | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     wordBreak: 'break-all', | ||||
| })); | ||||
| 
 | ||||
| export const VariantCell: VFC<IVariantCellProps> = ({ | ||||
|     variant, | ||||
|     variants, | ||||
|     feature, | ||||
|     isEnabled, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const [anchor, setAnchorEl] = useState<null | Element>(null); | ||||
| 
 | ||||
|     const onOpen = (event: React.FormEvent<HTMLButtonElement>) => | ||||
|         setAnchorEl(event.currentTarget); | ||||
| 
 | ||||
|     const onClose = () => setAnchorEl(null); | ||||
| 
 | ||||
|     const open = Boolean(anchor); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledDiv> | ||||
|             {variant} | ||||
|             <ConditionallyRender | ||||
|                 condition={ | ||||
|                     Boolean(variants) && variants.length > 0 && isEnabled | ||||
|                 } | ||||
|                 show={ | ||||
|                     <> | ||||
|                         <IconButton onClick={onOpen}> | ||||
|                             <InfoOutlined /> | ||||
|                         </IconButton> | ||||
| 
 | ||||
|                         <Popover | ||||
|                             open={open} | ||||
|                             id={`${feature}-result-variants`} | ||||
|                             PaperProps={{ | ||||
|                                 sx: { | ||||
|                                     borderRadius: | ||||
|                                         theme.shape.borderRadiusExtraLarge, | ||||
|                                 }, | ||||
|                             }} | ||||
|                             onClose={onClose} | ||||
|                             anchorEl={anchor} | ||||
|                             anchorOrigin={{ | ||||
|                                 vertical: 'bottom', | ||||
|                                 horizontal: -320, | ||||
|                             }} | ||||
|                         > | ||||
|                             <VariantInformation | ||||
|                                 variants={variants} | ||||
|                                 selectedVariant={variant} | ||||
|                             /> | ||||
|                         </Popover> | ||||
|                     </> | ||||
|                 } | ||||
|             /> | ||||
|         </StyledDiv> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,159 @@ | ||||
| import { Typography, styled, useTheme } from '@mui/material'; | ||||
| import { Table, TableBody, TableCell, TableRow } from 'component/common/Table'; | ||||
| import { useMemo, VFC } from 'react'; | ||||
| import { IFeatureVariant } from 'interfaces/featureToggle'; | ||||
| import { calculateVariantWeight } from 'component/common/util'; | ||||
| import { useGlobalFilter, useSortBy, useTable } from 'react-table'; | ||||
| import { sortTypes } from 'utils/sortTypes'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import { SortableTableHeader } from 'component/common/Table'; | ||||
| import { CheckCircleOutlined } from '@mui/icons-material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | ||||
| 
 | ||||
| interface IVariantInformationProps { | ||||
|     variants: IFeatureVariant[]; | ||||
|     selectedVariant: string; | ||||
| } | ||||
| 
 | ||||
| const StyledBox = styled('div')(({ theme }) => ({ | ||||
|     padding: theme.spacing(4), | ||||
|     maxWidth: '400px', | ||||
| })); | ||||
| 
 | ||||
| const StyledTypography = styled(Typography)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledCheckIcon = styled(CheckCircleOutlined)(({ theme }) => ({ | ||||
|     color: theme.palette.success.main, | ||||
| })); | ||||
| 
 | ||||
| export const VariantInformation: VFC<IVariantInformationProps> = ({ | ||||
|     variants, | ||||
|     selectedVariant, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const data = useMemo(() => { | ||||
|         return variants.map(variant => { | ||||
|             return { | ||||
|                 name: variant.name, | ||||
|                 weight: `${calculateVariantWeight(variant.weight)}%`, | ||||
|                 selected: variant.name === selectedVariant, | ||||
|             }; | ||||
|         }); | ||||
|     }, [variants, selectedVariant]); | ||||
| 
 | ||||
|     const initialState = useMemo( | ||||
|         () => ({ | ||||
|             sortBy: [{ id: 'name', desc: false }], | ||||
|         }), | ||||
|         [] | ||||
|     ); | ||||
| 
 | ||||
|     const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = | ||||
|         useTable( | ||||
|             { | ||||
|                 initialState, | ||||
|                 columns: COLUMNS as any, | ||||
|                 data: data as any, | ||||
|                 sortTypes, | ||||
|                 autoResetGlobalFilter: false, | ||||
|                 autoResetSortBy: false, | ||||
|                 disableSortRemove: true, | ||||
|             }, | ||||
|             useGlobalFilter, | ||||
|             useSortBy | ||||
|         ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledBox> | ||||
|             <StyledTypography variant="subtitle2"> | ||||
|                 Variant Information | ||||
|             </StyledTypography> | ||||
| 
 | ||||
|             <StyledTypography variant="body2"> | ||||
|                 The following table shows the variants defined on this feature | ||||
|                 toggle and the variant result based on your context | ||||
|                 configuration. | ||||
|             </StyledTypography> | ||||
| 
 | ||||
|             <StyledTypography variant="body2"> | ||||
|                 If you include "userId" or "sessionId" in your context, the | ||||
|                 variant will be the same every time because unleash uses these | ||||
|                 properties to ensure that the user receives the same experience. | ||||
|             </StyledTypography> | ||||
| 
 | ||||
|             <Table {...getTableProps()} rowHeight="dense"> | ||||
|                 <SortableTableHeader headerGroups={headerGroups as any} /> | ||||
|                 <TableBody {...getTableBodyProps()}> | ||||
|                     {rows.map((row: any) => { | ||||
|                         let styles = {} as { [key: string]: string }; | ||||
| 
 | ||||
|                         if (!row.original.selected) { | ||||
|                             styles.color = theme.palette.text.secondary; | ||||
|                         } | ||||
| 
 | ||||
|                         prepareRow(row); | ||||
|                         return ( | ||||
|                             <TableRow hover {...row.getRowProps()}> | ||||
|                                 {row.cells.map((cell: any) => ( | ||||
|                                     <TableCell | ||||
|                                         {...cell.getCellProps()} | ||||
|                                         style={styles} | ||||
|                                     > | ||||
|                                         {cell.render('Cell')} | ||||
|                                     </TableCell> | ||||
|                                 ))} | ||||
|                             </TableRow> | ||||
|                         ); | ||||
|                     })} | ||||
|                 </TableBody> | ||||
|             </Table> | ||||
|         </StyledBox> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const COLUMNS = [ | ||||
|     { | ||||
|         id: 'Icon', | ||||
|         Cell: ({ | ||||
|             row: { | ||||
|                 original: { selected }, | ||||
|             }, | ||||
|         }: any) => ( | ||||
|             <> | ||||
|                 <ConditionallyRender | ||||
|                     condition={selected} | ||||
|                     show={<IconCell icon={<StyledCheckIcon />} />} | ||||
|                 /> | ||||
|             </> | ||||
|         ), | ||||
|         maxWidth: 25, | ||||
|         disableGlobalFilter: true, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Name', | ||||
|         accessor: 'name', | ||||
|         searchable: true, | ||||
|         Cell: ({ | ||||
|             row: { | ||||
|                 original: { name }, | ||||
|             }, | ||||
|         }: any) => <TextCell>{name}</TextCell>, | ||||
|         maxWidth: 175, | ||||
|         width: 175, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Weight', | ||||
|         accessor: 'weight', | ||||
|         sortType: 'alphanumeric', | ||||
|         searchable: true, | ||||
|         maxWidth: 75, | ||||
|         Cell: ({ | ||||
|             row: { | ||||
|                 original: { weight }, | ||||
|             }, | ||||
|         }: any) => <TextCell>{weight}</TextCell>, | ||||
|     }, | ||||
| ]; | ||||
| @ -0,0 +1,44 @@ | ||||
| import { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model'; | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| 
 | ||||
| export const resolveProjects = ( | ||||
|     projects: string[] | string | ||||
| ): string[] | string => { | ||||
|     return !projects || | ||||
|         projects.length === 0 || | ||||
|         (projects.length === 1 && projects[0] === '*') | ||||
|         ? '*' | ||||
|         : projects; | ||||
| }; | ||||
| 
 | ||||
| export const resolveDefaultEnvironment = ( | ||||
|     environmentOptions: IEnvironment[] | ||||
| ) => { | ||||
|     const options = getEnvironmentOptions(environmentOptions); | ||||
|     if (options.length > 0) { | ||||
|         return options[0]; | ||||
|     } | ||||
|     return ''; | ||||
| }; | ||||
| 
 | ||||
| export const getEnvironmentOptions = (environments: IEnvironment[]) => { | ||||
|     return environments | ||||
|         .filter(({ enabled }) => Boolean(enabled)) | ||||
|         .sort((a, b) => a.sortOrder - b.sortOrder) | ||||
|         .map(({ name }) => name); | ||||
| }; | ||||
| 
 | ||||
| export const resolveResultsWidth = ( | ||||
|     matches: boolean, | ||||
|     results: PlaygroundResponseSchema | undefined | ||||
| ) => { | ||||
|     if (matches) { | ||||
|         return '100%'; | ||||
|     } | ||||
| 
 | ||||
|     if (results && !matches) { | ||||
|         return '65%'; | ||||
|     } | ||||
| 
 | ||||
|     return '50%'; | ||||
| }; | ||||
| @ -4,7 +4,7 @@ import { | ||||
|     PlaygroundResponseSchema, | ||||
| } from './playground.model'; | ||||
| 
 | ||||
| const usePlaygroundApi = () => { | ||||
| export const usePlaygroundApi = () => { | ||||
|     const { makeRequest, createRequest, errors, loading } = useAPI({ | ||||
|         propagateErrors: true, | ||||
|     }); | ||||
| @ -33,5 +33,3 @@ const usePlaygroundApi = () => { | ||||
|         loading, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default usePlaygroundApi; | ||||
|  | ||||
| @ -91,6 +91,11 @@ export default createTheme({ | ||||
|             dark: colors.grey[800], | ||||
|             border: colors.grey[500], | ||||
|         }, | ||||
|         tertiary: { | ||||
|             light: colors.grey[200], | ||||
|             main: colors.grey[400], | ||||
|             dark: colors.grey[600], | ||||
|         }, | ||||
|         divider: colors.grey[300], | ||||
|         dividerAlternative: colors.grey[400], | ||||
|         tableHeaderHover: colors.grey[400], | ||||
| @ -98,10 +103,12 @@ export default createTheme({ | ||||
|         secondaryContainer: colors.grey[200], | ||||
|         sidebarContainer: 'rgba(32,32,33, 0.2)', | ||||
|         grey: colors.grey, | ||||
|         lightBorder: colors.grey[400], | ||||
|         text: { | ||||
|             primary: colors.grey[900], | ||||
|             secondary: colors.grey[800], | ||||
|             disabled: colors.grey[600], | ||||
|             tertiaryContrast: '#fff', | ||||
|         }, | ||||
|         code: { | ||||
|             main: '#0b8c8f', | ||||
|  | ||||
| @ -79,6 +79,20 @@ declare module '@mui/material/styles' { | ||||
|          * and not with `import YourIcon from "@mui/icons/YourIcon"`. | ||||
|          */ | ||||
|         inactiveIcon: string; | ||||
| 
 | ||||
|         /** A border color used for contrast between similar backgroundColors **/ | ||||
|         lightBorder: string; | ||||
| 
 | ||||
|         /* Type for tertiary colors */ | ||||
|         tertiary: { | ||||
|             main: string; | ||||
|             light: string; | ||||
|             dark: string; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     interface CustomTypeText { | ||||
|         tertiaryContrast: string; | ||||
|     } | ||||
| 
 | ||||
|     interface Theme extends CustomTheme {} | ||||
| @ -87,6 +101,8 @@ declare module '@mui/material/styles' { | ||||
|     interface Palette extends CustomPalette {} | ||||
|     interface PaletteOptions extends CustomPalette {} | ||||
| 
 | ||||
|     interface TypeText extends CustomTypeText {} | ||||
| 
 | ||||
|     interface PaletteColor { | ||||
|         light: string; | ||||
|         main: string; | ||||
|  | ||||
| @ -1023,6 +1023,13 @@ | ||||
|   dependencies: | ||||
|     regenerator-runtime "^0.13.4" | ||||
| 
 | ||||
| "@babel/runtime@^7.18.6": | ||||
|   version "7.18.9" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" | ||||
|   integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== | ||||
|   dependencies: | ||||
|     regenerator-runtime "^0.13.4" | ||||
| 
 | ||||
| "@babel/template@^7.16.7": | ||||
|   version "7.16.7" | ||||
|   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" | ||||
| @ -1080,6 +1087,88 @@ | ||||
|     "@babel/helper-validator-identifier" "^7.16.7" | ||||
|     to-fast-properties "^2.0.0" | ||||
| 
 | ||||
| "@codemirror/autocomplete@^6.0.0": | ||||
|   version "6.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.0.4.tgz#90a9c81cfddac528b9e9dc07415a7c6554dbe85c" | ||||
|   integrity sha512-uP7UodCRykPNwSAN+wYa/AS9gJI/V47echCAXUYgCgBXy3l19nwO7W/d29COtG/dfAsjBOhMDeh3Ms8Y5VZbrA== | ||||
|   dependencies: | ||||
|     "@codemirror/language" "^6.0.0" | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
|     "@lezer/common" "^1.0.0" | ||||
| 
 | ||||
| "@codemirror/commands@^6.0.0": | ||||
|   version "6.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.0.1.tgz#c005dd2dab2f6d90ad00d4a25bfeaaec2393efa6" | ||||
|   integrity sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ== | ||||
|   dependencies: | ||||
|     "@codemirror/language" "^6.0.0" | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
|     "@lezer/common" "^1.0.0" | ||||
| 
 | ||||
| "@codemirror/lang-json@^6.0.0": | ||||
|   version "6.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.0.tgz#6ac373248c2d44ceab6d5d58879cc543095e503e" | ||||
|   integrity sha512-DvTcYTKLmg2viADXlTdufrT334M9jowe1qO02W28nvm+nejcvhM5vot5mE8/kPrxYw/HJHhwu1z2PyBpnMLCNQ== | ||||
|   dependencies: | ||||
|     "@codemirror/language" "^6.0.0" | ||||
|     "@lezer/json" "^1.0.0" | ||||
| 
 | ||||
| "@codemirror/language@^6.0.0": | ||||
|   version "6.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.2.0.tgz#f8d103927bb61346e93781b1ca7d3f4ac3c9280b" | ||||
|   integrity sha512-tabB0Ef/BflwoEmTB4a//WZ9P90UQyne9qWB9YFsmeS4bnEqSys7UpGk/da1URMXhyfuzWCwp+AQNMhvu8SfnA== | ||||
|   dependencies: | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
|     "@lezer/common" "^1.0.0" | ||||
|     "@lezer/highlight" "^1.0.0" | ||||
|     "@lezer/lr" "^1.0.0" | ||||
|     style-mod "^4.0.0" | ||||
| 
 | ||||
| "@codemirror/lint@^6.0.0": | ||||
|   version "6.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.0.0.tgz#a249b021ac9933b94fe312d994d220f0ef11a157" | ||||
|   integrity sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA== | ||||
|   dependencies: | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
|     crelt "^1.0.5" | ||||
| 
 | ||||
| "@codemirror/search@^6.0.0": | ||||
|   version "6.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.0.0.tgz#43bd6341d9aff18869386d2fce27519850e919e3" | ||||
|   integrity sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ== | ||||
|   dependencies: | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
|     crelt "^1.0.5" | ||||
| 
 | ||||
| "@codemirror/state@^6.0.0", "@codemirror/state@^6.1.0": | ||||
|   version "6.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.1.0.tgz#c0f1d80f61908c9dcf5e2a3fe931e9dd78f3df8a" | ||||
|   integrity sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg== | ||||
| 
 | ||||
| "@codemirror/theme-one-dark@^6.0.0": | ||||
|   version "6.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz#81a999a568217f68522bd8846cbf7210ca2a59df" | ||||
|   integrity sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw== | ||||
|   dependencies: | ||||
|     "@codemirror/language" "^6.0.0" | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
|     "@lezer/highlight" "^1.0.0" | ||||
| 
 | ||||
| "@codemirror/view@^6.0.0": | ||||
|   version "6.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.0.3.tgz#c0f6cf5c66d76cbe64227717708a714338ac76a4" | ||||
|   integrity sha512-1gDBymhbx2DZzwnR/rNUu1LiQqjxBJtFiB+4uLR6tHQ6vKhTIwUsP5uZUQ7SM7JxVx3UihMynnTqjcsC+mczZg== | ||||
|   dependencies: | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     style-mod "^4.0.0" | ||||
|     w3c-keyname "^2.2.4" | ||||
| 
 | ||||
| "@colors/colors@1.5.0": | ||||
|   version "1.5.0" | ||||
|   resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" | ||||
| @ -1350,6 +1439,33 @@ | ||||
|     "@jridgewell/resolve-uri" "^3.0.3" | ||||
|     "@jridgewell/sourcemap-codec" "^1.4.10" | ||||
| 
 | ||||
| "@lezer/common@^1.0.0": | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.0.tgz#1c95ae53ec17706aa3cbcc88b52c23f22ed56096" | ||||
|   integrity sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA== | ||||
| 
 | ||||
| "@lezer/highlight@^1.0.0": | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.0.0.tgz#1dc82300f5d39fbd67ae1194b5519b4c381878d3" | ||||
|   integrity sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA== | ||||
|   dependencies: | ||||
|     "@lezer/common" "^1.0.0" | ||||
| 
 | ||||
| "@lezer/json@^1.0.0": | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.0.tgz#848ad9c2c3e812518eb02897edd5a7f649e9c160" | ||||
|   integrity sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw== | ||||
|   dependencies: | ||||
|     "@lezer/highlight" "^1.0.0" | ||||
|     "@lezer/lr" "^1.0.0" | ||||
| 
 | ||||
| "@lezer/lr@^1.0.0": | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.2.0.tgz#59aecafdbc15be63f918cf777f470dd17562f051" | ||||
|   integrity sha512-TgEpfm9br2SX8JwtwKT8HsQZKuFkLRg6g+IRxObk9nVKQLKnkP3oMh+QGcTBL9GQsfQ2ADtKPbj2iGSMf3ytiA== | ||||
|   dependencies: | ||||
|     "@lezer/common" "^1.0.0" | ||||
| 
 | ||||
| "@mswjs/cookies@^0.2.0": | ||||
|   version "0.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23" | ||||
| @ -2092,6 +2208,29 @@ | ||||
|     "@typescript-eslint/types" "5.23.0" | ||||
|     eslint-visitor-keys "^3.0.0" | ||||
| 
 | ||||
| "@uiw/codemirror-extensions-basic-setup@4.11.4": | ||||
|   version "4.11.4" | ||||
|   resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.11.4.tgz#c749a66980e18ca6651488712ea3239c82a31cdd" | ||||
|   integrity sha512-pc9pQtCQFmAH5nV9UmX37VB0+yzSFQ2kbSvLHBFST9siYnacaR6HxmkBBBbYYXwVK/n9pGZ6A8ZefAUNTFfo/A== | ||||
|   dependencies: | ||||
|     "@codemirror/autocomplete" "^6.0.0" | ||||
|     "@codemirror/commands" "^6.0.0" | ||||
|     "@codemirror/language" "^6.0.0" | ||||
|     "@codemirror/lint" "^6.0.0" | ||||
|     "@codemirror/search" "^6.0.0" | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
| 
 | ||||
| "@uiw/react-codemirror@^4.11.4": | ||||
|   version "4.11.4" | ||||
|   resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.11.4.tgz#76adc757baa0b8b1a9bd30d7081f5622b896d607" | ||||
|   integrity sha512-p7DNBI6kj+DUzTe7MjBJwZ3qo0nSOav7T0MEGRpRNZA9ZO3RnzhPMie6swDA8e3dz1s59l9UdFB1fgyam1vFhQ== | ||||
|   dependencies: | ||||
|     "@babel/runtime" "^7.18.6" | ||||
|     "@codemirror/theme-one-dark" "^6.0.0" | ||||
|     "@uiw/codemirror-extensions-basic-setup" "4.11.4" | ||||
|     codemirror "^6.0.0" | ||||
| 
 | ||||
| "@vitejs/plugin-react@1.3.2": | ||||
|   version "1.3.2" | ||||
|   resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz#2fcf0b6ce9bcdcd4cec5c760c199779d5657ece1" | ||||
| @ -2723,6 +2862,19 @@ clsx@^1.1.1: | ||||
|   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" | ||||
|   integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== | ||||
| 
 | ||||
| codemirror@^6.0.0, codemirror@^6.0.1: | ||||
|   version "6.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" | ||||
|   integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== | ||||
|   dependencies: | ||||
|     "@codemirror/autocomplete" "^6.0.0" | ||||
|     "@codemirror/commands" "^6.0.0" | ||||
|     "@codemirror/language" "^6.0.0" | ||||
|     "@codemirror/lint" "^6.0.0" | ||||
|     "@codemirror/search" "^6.0.0" | ||||
|     "@codemirror/state" "^6.0.0" | ||||
|     "@codemirror/view" "^6.0.0" | ||||
| 
 | ||||
| color-convert@^1.9.0: | ||||
|   version "1.9.3" | ||||
|   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" | ||||
| @ -2874,6 +3026,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: | ||||
|     path-type "^4.0.0" | ||||
|     yaml "^1.10.0" | ||||
| 
 | ||||
| crelt@^1.0.5: | ||||
|   version "1.0.5" | ||||
|   resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94" | ||||
|   integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA== | ||||
| 
 | ||||
| cross-spawn@^7.0.0, cross-spawn@^7.0.2: | ||||
|   version "7.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" | ||||
| @ -5755,6 +5912,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: | ||||
|   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" | ||||
|   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== | ||||
| 
 | ||||
| style-mod@^4.0.0: | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01" | ||||
|   integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw== | ||||
| 
 | ||||
| stylis@4.0.13: | ||||
|   version "4.0.13" | ||||
|   resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" | ||||
| @ -6140,6 +6302,11 @@ w3c-hr-time@^1.0.2: | ||||
|   dependencies: | ||||
|     browser-process-hrtime "^1.0.0" | ||||
| 
 | ||||
| w3c-keyname@^2.2.4: | ||||
|   version "2.2.4" | ||||
|   resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.4.tgz#4ade6916f6290224cdbd1db8ac49eab03d0eef6b" | ||||
|   integrity sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw== | ||||
| 
 | ||||
| w3c-xmlserializer@^3.0.0: | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user