diff --git a/frontend/package.json b/frontend/package.json index d748544fef..673075095a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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": { diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.test.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.test.tsx index 29d46117e2..ed6f46a166 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.test.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.test.tsx @@ -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'); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.tsx index 1fd8b86531..39d1f900c0 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue.tsx @@ -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>; } -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; - } -}; diff --git a/frontend/src/component/common/GuidanceIndicator/GuidanceIndicator.tsx b/frontend/src/component/common/GuidanceIndicator/GuidanceIndicator.tsx new file mode 100644 index 0000000000..167fdef936 --- /dev/null +++ b/frontend/src/component/common/GuidanceIndicator/GuidanceIndicator.tsx @@ -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 = ({ + style, + children, + type, +}) => { + const theme = useTheme(); + + const defaults = { backgroundColor: theme.palette.primary.main }; + if (type === 'secondary') { + defaults.backgroundColor = theme.palette.tertiary.dark; + } + + return ( + + {children} + + ); +}; diff --git a/frontend/src/component/common/Search/Search.tsx b/frontend/src/component/common/Search/Search.tsx index 544f49e6ca..fdfc5c0af7 100644 --- a/frontend/src/component/common/Search/Search.tsx +++ b/frontend/src/component/common/Search/Search.tsx @@ -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(); const { classes: styles } = useStyles(); @@ -59,7 +61,7 @@ export const Search = ({ const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; return ( -
+
()( overflow: lineClamp ? 'hidden' : 'auto', WebkitLineClamp: lineClamp ? lineClamp : 'none', WebkitBoxOrient: 'vertical', + wordBreak: 'break-all', }, }) ); diff --git a/frontend/src/component/common/util.ts b/frontend/src/component/common/util.ts index 1340140a9e..5cbb0d7b8f 100644 --- a/frontend/src/component/common/util.ts +++ b/frontend/src/component/common/util.ts @@ -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 []; diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx index 871d276367..807367591a 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx @@ -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 ( - {weight / 10.0} % + {calculateVariantWeight(weight)} % ); }, diff --git a/frontend/src/component/playground/Playground/Playground.tsx b/frontend/src/component/playground/Playground/Playground.tsx index c0549a0077..2f6943abdd 100644 --- a/frontend/src/component/playground/Playground/Playground.tsx +++ b/frontend/src/component/playground/Playground/Playground.tsx @@ -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 = () => { +export const Playground: VFC<{}> = () => { + const { environments } = useEnvironments(); const theme = useTheme(); - const [environment, onSetEnvironment] = useState(''); - const [projects, onSetProjects] = useState([]); + const matches = useMediaQuery(theme.breakpoints.down('lg')); + + const [environment, setEnvironment] = useState(''); + const [projects, setProjects] = useState([]); const [context, setContext] = useState(); const [results, setResults] = useState< PlaygroundResponseSchema | undefined @@ -34,21 +34,42 @@ export const Playground: VFC = () => { 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 = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onSubmit: FormEventHandler = 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 = () => { } }; + const onSubmit: FormEventHandler = 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 ( } + header={ + } + /> + } disableLoading bodyClass={'no-padding'} > - - - + - Configure playground - - - - - - - - - - - - - } - /> - - + + + ({ + width: resultsWidth, + transition: 'width 0.4s ease', + padding: theme.spacing(4, 2), + })} + > + + } + elseShow={} + /> + + ); }; diff --git a/frontend/src/component/playground/Playground/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx b/frontend/src/component/playground/Playground/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx deleted file mode 100644 index 1388dd4f1b..0000000000 --- a/frontend/src/component/playground/Playground/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx +++ /dev/null @@ -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>; -} - -export const PlaygroundCodeFieldset: VFC = ({ - 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(); - const [fieldExist, setFieldExist] = useState(false); - const [contextField, setContextField] = useState(''); - const [contextValue, setContextValue] = useState(''); - 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 ( - - - Unleash context - - setValue(event.target.value)} - /> - - - - Context field - - - - - setContextValue(event.target.value || '') - } - /> - - - - ); -}; diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx new file mode 100644 index 0000000000..2912dc803b --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx @@ -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>; +} + +export const PlaygroundCodeFieldset: VFC = ({ + 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(); + const [fieldExist, setFieldExist] = useState(false); + const [contextField, setContextField] = useState(''); + const [contextValue, setContextValue] = useState(''); + 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 ( + { + 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 ( + { + if (typeof newValue === 'string') { + return setContextValue(newValue); + } + }} + options={options} + sx={{ width: 200, maxWidth: '100%' }} + renderInput={(params: any) => ( + + )} + /> + ); + } + + return ( + setContextValue(event.target.value || '')} + /> + ); + }; + + return ( + + + 2 + + Unleash context + + + + + + + Context field + + + + {resolveInput()} + + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx new file mode 100644 index 0000000000..9fae5b6bf7 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx @@ -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>; + 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 ( + + + + ); +}; + +const EditorStatusError = () => { + const theme = useTheme(); + + return ( + + + + ); +}; + +export const PlaygroundEditor: VFC = ({ + context, + setContext, + error, +}) => { + const theme = useTheme(); + const onCodeFieldChange = useCallback( + context => { + setContext(context); + }, + [setContext] + ); + + return ( + + + JSON + ({ + display: 'flex', + alignItems: 'center', + })} + > + {error} + + + } + elseShow={} + /> + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx similarity index 81% rename from frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx rename to frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx index aa368906cd..a066c7219d 100644 --- a/frontend/src/component/playground/Playground/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx @@ -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 ( - - Access configuration - + + 1 + + Access configuration + + ( )} @@ -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 => ( )} diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx new file mode 100644 index 0000000000..ba410d06d5 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx @@ -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) => void; + environment: string; + projects: string[]; + setProjects: React.Dispatch>; + setEnvironment: React.Dispatch>; + context: string | undefined; + setContext: React.Dispatch>; +} + +export const PlaygroundForm: VFC = ({ + environments, + environment, + onSubmit, + projects, + setProjects, + setEnvironment, + context, + setContext, +}) => { + const theme = useTheme(); + + return ( + + + + + + + + 3 + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundGuidance/PlaygroundGuidance.tsx b/frontend/src/component/playground/Playground/PlaygroundGuidance/PlaygroundGuidance.tsx new file mode 100644 index 0000000000..ed1e36a7f7 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundGuidance/PlaygroundGuidance.tsx @@ -0,0 +1,40 @@ +import { Typography, Box, Divider } from '@mui/material'; +import { PlaygroundGuidanceSection } from './PlaygroundGuidanceSection/PlaygroundGuidanceSection'; + +export const PlaygroundGuidance = () => { + return ( + + + 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. + + + + + + What you need to do is: + + + + + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundGuidance/PlaygroundGuidanceSection/PlaygroundGuidanceSection.tsx b/frontend/src/component/playground/Playground/PlaygroundGuidance/PlaygroundGuidanceSection/PlaygroundGuidanceSection.tsx new file mode 100644 index 0000000000..b2e13cca05 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundGuidance/PlaygroundGuidanceSection/PlaygroundGuidanceSection.tsx @@ -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 ( + + + + {sectionNumber} + + + + {headerText} + + + {bodyText} + + } + /> + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundGuidancePopper/PlaygroundGuidancePopper.tsx b/frontend/src/component/playground/Playground/PlaygroundGuidancePopper/PlaygroundGuidancePopper.tsx new file mode 100644 index 0000000000..ba48b82884 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundGuidancePopper/PlaygroundGuidancePopper.tsx @@ -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); + + const onOpen = (event: React.FormEvent) => + setAnchorEl(event.currentTarget); + + const onClose = () => setAnchorEl(null); + + const open = Boolean(anchor); + + const id = 'playground-guidance-popper'; + + return ( + + + + + + ({ zIndex: theme.zIndex.tooltip })} + > + ({ + padding: theme.spacing(8, 4), + maxWidth: '500px', + borderRadius: theme.shape.borderRadiusExtraLarge, + })} + > + + + + + + + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx index b86dcc3c08..5f7137c5fb 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultsTable.tsx @@ -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 = { 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 ( - - } + <> + + + {features !== undefined && !loading + ? `Results (${ + rows.length < data.length + ? `${rows.length} of ${data.length}` + : data.length + })` + : 'Results'} + + + - } - isLoading={loading} - > + + ( {data === undefined @@ -157,7 +179,7 @@ export const PlaygroundResultsTable = ({ )} elseShow={() => ( - <> + @@ -187,7 +209,9 @@ export const PlaygroundResultsTable = ({ 0} + condition={ + data.length === 0 && searchValue?.length > 0 + } show={ No feature toggles found matching “ @@ -195,10 +219,21 @@ export const PlaygroundResultsTable = ({ } /> - + + + No features toggles to display + + } + /> + )} /> - + ); }; @@ -207,7 +242,7 @@ const COLUMNS = [ Header: 'Name', accessor: 'name', searchable: true, - width: '60%', + minWidth: 160, Cell: ({ value, row: { original } }: any) => ( , + }: any) => ( + + ), }, { Header: 'isEnabled', accessor: 'isEnabled', - maxWidth: 170, + filterName: 'isEnabled', + filterParsing: (value: boolean) => (value ? 'true' : 'false'), Cell: ({ value }: any) => , sortType: 'boolean', sortInverted: true, diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/VariantCell/VariantCell.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/VariantCell/VariantCell.tsx new file mode 100644 index 0000000000..a9e81f000f --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/VariantCell/VariantCell.tsx @@ -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 = ({ + variant, + variants, + feature, + isEnabled, +}) => { + const theme = useTheme(); + const [anchor, setAnchorEl] = useState(null); + + const onOpen = (event: React.FormEvent) => + setAnchorEl(event.currentTarget); + + const onClose = () => setAnchorEl(null); + + const open = Boolean(anchor); + + return ( + + {variant} + 0 && isEnabled + } + show={ + <> + + + + + + + + + } + /> + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/VariantCell/VariantInformation/VariantInformation.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/VariantCell/VariantInformation/VariantInformation.tsx new file mode 100644 index 0000000000..92bf305480 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/VariantCell/VariantInformation/VariantInformation.tsx @@ -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 = ({ + 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 ( + + + Variant Information + + + + The following table shows the variants defined on this feature + toggle and the variant result based on your context + configuration. + + + + 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. + + + + + + {rows.map((row: any) => { + let styles = {} as { [key: string]: string }; + + if (!row.original.selected) { + styles.color = theme.palette.text.secondary; + } + + prepareRow(row); + return ( + + {row.cells.map((cell: any) => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
+ ); +}; + +const COLUMNS = [ + { + id: 'Icon', + Cell: ({ + row: { + original: { selected }, + }, + }: any) => ( + <> + } />} + /> + + ), + maxWidth: 25, + disableGlobalFilter: true, + }, + { + Header: 'Name', + accessor: 'name', + searchable: true, + Cell: ({ + row: { + original: { name }, + }, + }: any) => {name}, + maxWidth: 175, + width: 175, + }, + { + Header: 'Weight', + accessor: 'weight', + sortType: 'alphanumeric', + searchable: true, + maxWidth: 75, + Cell: ({ + row: { + original: { weight }, + }, + }: any) => {weight}, + }, +]; diff --git a/frontend/src/component/playground/Playground/playground.utils.ts b/frontend/src/component/playground/Playground/playground.utils.ts new file mode 100644 index 0000000000..f27aa6d483 --- /dev/null +++ b/frontend/src/component/playground/Playground/playground.utils.ts @@ -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%'; +}; diff --git a/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts b/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts index 5351b673ec..fd23b83358 100644 --- a/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts +++ b/frontend/src/hooks/api/actions/usePlayground/usePlayground.ts @@ -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; diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index ce9eca54d9..0e9b54d8ba 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -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', diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts index 1be1b05800..cb59be8a6e 100644 --- a/frontend/src/themes/themeTypes.ts +++ b/frontend/src/themes/themeTypes.ts @@ -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; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8e83fc4cd4..fc3af17b73 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"