mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
Merge remote-tracking branch 'origin/task/constraint_card_adjustmnets' into task/constraint_card_adjustmnets
This commit is contained in:
commit
9a13c5c489
@ -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