mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +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-plugin-svgr": "2.2.0",
|
||||||
"vite-tsconfig-paths": "3.5.0",
|
"vite-tsconfig-paths": "3.5.0",
|
||||||
"vitest": "0.16.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": {
|
"jest": {
|
||||||
"moduleNameMapper": {
|
"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`, () => {
|
test(`Date component is able to parse midnight when it's 00`, () => {
|
||||||
let f = parseDateValue('2022-03-15T12:27');
|
let f = parseDateValue('2022-03-15T12:27');
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
|
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
|
||||||
import { format, isValid } from 'date-fns';
|
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
|
import { parseDateValue, parseValidDate } from 'component/common/util';
|
||||||
interface IDateSingleValueProps {
|
interface IDateSingleValueProps {
|
||||||
setValue: (value: string) => void;
|
setValue: (value: string) => void;
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -8,11 +8,6 @@ interface IDateSingleValueProps {
|
|||||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
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 = ({
|
export const DateSingleValue = ({
|
||||||
setValue,
|
setValue,
|
||||||
value,
|
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;
|
hasFilters?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
getSearchContext?: () => IGetSearchContextOutput;
|
getSearchContext?: () => IGetSearchContextOutput;
|
||||||
|
containerStyles?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Search = ({
|
export const Search = ({
|
||||||
@ -27,6 +28,7 @@ export const Search = ({
|
|||||||
hasFilters,
|
hasFilters,
|
||||||
disabled,
|
disabled,
|
||||||
getSearchContext,
|
getSearchContext,
|
||||||
|
containerStyles,
|
||||||
}: ISearchProps) => {
|
}: ISearchProps) => {
|
||||||
const ref = useRef<HTMLInputElement>();
|
const ref = useRef<HTMLInputElement>();
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
@ -59,7 +61,7 @@ export const Search = ({
|
|||||||
const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
|
const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container} style={containerStyles}>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
styles.search,
|
styles.search,
|
||||||
|
@ -8,6 +8,7 @@ export const useStyles = makeStyles<{ lineClamp?: number }>()(
|
|||||||
overflow: lineClamp ? 'hidden' : 'auto',
|
overflow: lineClamp ? 'hidden' : 'auto',
|
||||||
WebkitLineClamp: lineClamp ? lineClamp : 'none',
|
WebkitLineClamp: lineClamp ? lineClamp : 'none',
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
|
wordBreak: 'break-all',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVaria
|
|||||||
import { IFlags } from 'interfaces/uiConfig';
|
import { IFlags } from 'interfaces/uiConfig';
|
||||||
import { IRoute } from 'interfaces/route';
|
import { IRoute } from 'interfaces/route';
|
||||||
import { IFeatureVariant } from 'interfaces/featureToggle';
|
import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||||
|
import { format, isValid } from 'date-fns';
|
||||||
|
|
||||||
export const filterByFlags = (flags: IFlags) => (r: IRoute) => {
|
export const filterByFlags = (flags: IFlags) => (r: IRoute) => {
|
||||||
if (!r.flag) {
|
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) {
|
export function updateWeight(variants: IFeatureVariant[], totalWeight: number) {
|
||||||
if (variants.length === 0) {
|
if (variants.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { AddVariant } from './AddFeatureVariant/AddFeatureVariant';
|
import { AddVariant } from './AddFeatureVariant/AddFeatureVariant';
|
||||||
|
|
||||||
import { useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
import { useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
@ -20,7 +19,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
|||||||
import { IFeatureVariant } from 'interfaces/featureToggle';
|
import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { updateWeight } from 'component/common/util';
|
import { calculateVariantWeight, updateWeight } from 'component/common/util';
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import useDeleteVariantMarkup from './useDeleteVariantMarkup';
|
import useDeleteVariantMarkup from './useDeleteVariantMarkup';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
@ -141,7 +140,7 @@ export const FeatureVariantsList = () => {
|
|||||||
}: any) => {
|
}: any) => {
|
||||||
return (
|
return (
|
||||||
<TextCell data-testid={`VARIANT_WEIGHT_${name}`}>
|
<TextCell data-testid={`VARIANT_WEIGHT_${name}`}>
|
||||||
{weight / 10.0} %
|
{calculateVariantWeight(weight)} %
|
||||||
</TextCell>
|
</TextCell>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import { FormEventHandler, useEffect, useState, VFC } from 'react';
|
import { FormEventHandler, useEffect, useState, VFC } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import { Box, Paper, useMediaQuery, useTheme } from '@mui/material';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/PlaygroundConnectionFieldset';
|
|
||||||
import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset';
|
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable';
|
import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable';
|
||||||
import { ContextBanner } from './PlaygroundResultsTable/ContextBanner/ContextBanner';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 { 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<{}> = () => {
|
||||||
|
const { environments } = useEnvironments();
|
||||||
export const Playground: VFC<IPlaygroundProps> = () => {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [environment, onSetEnvironment] = useState<string>('');
|
const matches = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [projects, onSetProjects] = useState<string[]>([]);
|
|
||||||
|
const [environment, setEnvironment] = useState<string>('');
|
||||||
|
const [projects, setProjects] = useState<string[]>([]);
|
||||||
const [context, setContext] = useState<string>();
|
const [context, setContext] = useState<string>();
|
||||||
const [results, setResults] = useState<
|
const [results, setResults] = useState<
|
||||||
PlaygroundResponseSchema | undefined
|
PlaygroundResponseSchema | undefined
|
||||||
@ -34,21 +34,42 @@ export const Playground: VFC<IPlaygroundProps> = () => {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { evaluatePlayground, loading } = usePlaygroundApi();
|
const { evaluatePlayground, loading } = usePlaygroundApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEnvironment(resolveDefaultEnvironment(environments));
|
||||||
|
}, [environments]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load initial values from URL
|
// Load initial values from URL
|
||||||
try {
|
try {
|
||||||
const environmentFromUrl = searchParams.get('environment');
|
const environmentFromUrl = searchParams.get('environment');
|
||||||
if (environmentFromUrl) {
|
if (environmentFromUrl) {
|
||||||
onSetEnvironment(environmentFromUrl);
|
setEnvironment(environmentFromUrl);
|
||||||
}
|
}
|
||||||
const projectsFromUrl = searchParams.get('projects');
|
|
||||||
|
let projectsArray: string[];
|
||||||
|
let projectsFromUrl = searchParams.get('projects');
|
||||||
if (projectsFromUrl) {
|
if (projectsFromUrl) {
|
||||||
onSetProjects(projectsFromUrl.split(','));
|
projectsArray = projectsFromUrl.split(',');
|
||||||
|
setProjects(projectsArray);
|
||||||
}
|
}
|
||||||
const contextFromUrl = searchParams.get('context');
|
|
||||||
|
let contextFromUrl = searchParams.get('context');
|
||||||
if (contextFromUrl) {
|
if (contextFromUrl) {
|
||||||
setContext(decodeURI(contextFromUrl));
|
contextFromUrl = decodeURI(contextFromUrl);
|
||||||
|
setContext(contextFromUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const makePlaygroundRequest = async () => {
|
||||||
|
if (environmentFromUrl && contextFromUrl) {
|
||||||
|
await evaluatePlaygroundContext(
|
||||||
|
environmentFromUrl,
|
||||||
|
projectsArray || '*',
|
||||||
|
contextFromUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
makePlaygroundRequest();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -60,40 +81,27 @@ export const Playground: VFC<IPlaygroundProps> = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {
|
const evaluatePlaygroundContext = async (
|
||||||
event.preventDefault();
|
environment: string,
|
||||||
|
projects: string[] | string,
|
||||||
|
context: string | undefined,
|
||||||
|
action?: () => void
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const parsedContext = JSON.parse(context || '{}');
|
const parsedContext = JSON.parse(context || '{}');
|
||||||
const response = await evaluatePlayground({
|
const response = await evaluatePlayground({
|
||||||
environment,
|
environment,
|
||||||
projects:
|
projects: resolveProjects(projects),
|
||||||
!projects ||
|
|
||||||
projects.length === 0 ||
|
|
||||||
(projects.length === 1 && projects[0] === '*')
|
|
||||||
? '*'
|
|
||||||
: projects,
|
|
||||||
context: {
|
context: {
|
||||||
appName: 'playground',
|
appName: 'playground',
|
||||||
...parsedContext,
|
...parsedContext,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set URL search parameters
|
if (action && typeof action === 'function') {
|
||||||
searchParams.set('context', encodeURI(context || '')); // always set because of native validation
|
action();
|
||||||
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);
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
setResults(response);
|
setResults(response);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastData({
|
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 (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
header={<PageHeader title="Unleash playground" />}
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title="Unleash playground"
|
||||||
|
actions={<PlaygroundGuidancePopper />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
disableLoading
|
disableLoading
|
||||||
bodyClass={'no-padding'}
|
bodyClass={'no-padding'}
|
||||||
>
|
>
|
||||||
<Paper
|
<Box
|
||||||
elevation={0}
|
|
||||||
sx={{
|
sx={{
|
||||||
px: 4,
|
display: 'flex',
|
||||||
py: 3,
|
flexDirection: !matches ? 'row' : 'column',
|
||||||
mb: 4,
|
|
||||||
m: 4,
|
|
||||||
background: theme.palette.grey[200],
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box component="form" onSubmit={onSubmit}>
|
<Box
|
||||||
<Typography
|
sx={{
|
||||||
|
background: theme.palette.grey[200],
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
sx={{
|
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
|
<PlaygroundForm
|
||||||
</Typography>
|
onSubmit={onSubmit}
|
||||||
<PlaygroundConnectionFieldset
|
context={context}
|
||||||
environment={environment}
|
setContext={setContext}
|
||||||
projects={projects}
|
environments={environments}
|
||||||
setEnvironment={onSetEnvironment}
|
projects={projects}
|
||||||
setProjects={onSetProjects}
|
environment={environment}
|
||||||
/>
|
setProjects={setProjects}
|
||||||
<Divider
|
setEnvironment={setEnvironment}
|
||||||
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
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</Paper>
|
||||||
}
|
</Box>
|
||||||
/>
|
<Box
|
||||||
|
sx={theme => ({
|
||||||
<PlaygroundResultsTable
|
width: resultsWidth,
|
||||||
loading={loading}
|
transition: 'width 0.4s ease',
|
||||||
features={results?.features}
|
padding: theme.spacing(4, 2),
|
||||||
/>
|
})}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(results)}
|
||||||
|
show={
|
||||||
|
<PlaygroundResultsTable
|
||||||
|
loading={loading}
|
||||||
|
features={results?.features}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={<PlaygroundGuidance />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</PageContent>
|
</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';
|
} from '@mui/material';
|
||||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
|
import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator';
|
||||||
|
|
||||||
interface IPlaygroundConnectionFieldsetProps {
|
interface IPlaygroundConnectionFieldsetProps {
|
||||||
environment: string;
|
environment: string;
|
||||||
projects: string[];
|
projects: string[];
|
||||||
setProjects: (projects: string[]) => void;
|
setProjects: (projects: string[]) => void;
|
||||||
setEnvironment: (environment: string) => void;
|
setEnvironment: (environment: string) => void;
|
||||||
|
environmentOptions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IOption {
|
interface IOption {
|
||||||
@ -25,13 +27,14 @@ const allOption: IOption = { label: 'ALL', id: '*' };
|
|||||||
|
|
||||||
export const PlaygroundConnectionFieldset: VFC<
|
export const PlaygroundConnectionFieldset: VFC<
|
||||||
IPlaygroundConnectionFieldsetProps
|
IPlaygroundConnectionFieldsetProps
|
||||||
> = ({ environment, projects, setProjects, setEnvironment }) => {
|
> = ({
|
||||||
|
environment,
|
||||||
|
projects,
|
||||||
|
setProjects,
|
||||||
|
setEnvironment,
|
||||||
|
environmentOptions,
|
||||||
|
}) => {
|
||||||
const theme = useTheme();
|
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 { projects: availableProjects = [] } = useProjects();
|
||||||
const projectsOptions = [
|
const projectsOptions = [
|
||||||
@ -74,19 +77,22 @@ export const PlaygroundConnectionFieldset: VFC<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ pb: 2 }}>
|
<Box sx={{ pb: 2 }}>
|
||||||
<Typography
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
variant="body2"
|
<GuidanceIndicator type="secondary">1</GuidanceIndicator>
|
||||||
sx={{ mb: 2 }}
|
<Typography
|
||||||
color={theme.palette.text.secondary}
|
variant="body2"
|
||||||
>
|
color={theme.palette.text.secondary}
|
||||||
Access configuration
|
sx={{ ml: 1 }}
|
||||||
</Typography>
|
>
|
||||||
|
Access configuration
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
id="environment"
|
id="environment"
|
||||||
options={environmentOptions}
|
options={environmentOptions}
|
||||||
sx={{ width: 300, maxWidth: '100%' }}
|
sx={{ width: 200, maxWidth: '100%' }}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField {...params} label="Environment" required />
|
<TextField {...params} label="Environment" required />
|
||||||
)}
|
)}
|
||||||
@ -99,7 +105,7 @@ export const PlaygroundConnectionFieldset: VFC<
|
|||||||
id="projects"
|
id="projects"
|
||||||
multiple={!isAllProjects}
|
multiple={!isAllProjects}
|
||||||
options={projectsOptions}
|
options={projectsOptions}
|
||||||
sx={{ width: 300, maxWidth: '100%' }}
|
sx={{ width: 200, maxWidth: '100%' }}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField {...params} label="Projects" />
|
<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 { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { SortingRule, useGlobalFilter, useSortBy, useTable } from 'react-table';
|
import { SortingRule, useGlobalFilter, useSortBy, useTable } from 'react-table';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import {
|
import {
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
Table,
|
Table,
|
||||||
@ -21,6 +20,10 @@ import { useSearch } from 'hooks/useSearch';
|
|||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell';
|
import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell';
|
||||||
import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
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 defaultSort: SortingRule<string> = { id: 'name' };
|
||||||
const { value, setValue } = createLocalStorage(
|
const { value, setValue } = createLocalStorage(
|
||||||
@ -38,10 +41,13 @@ export const PlaygroundResultsTable = ({
|
|||||||
loading,
|
loading,
|
||||||
}: IPlaygroundResultsTableProps) => {
|
}: IPlaygroundResultsTableProps) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const ref = useLoading(loading);
|
||||||
const [searchValue, setSearchValue] = useState(
|
const [searchValue, setSearchValue] = useState(
|
||||||
searchParams.get('search') || ''
|
searchParams.get('search') || ''
|
||||||
);
|
);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: searchedData,
|
data: searchedData,
|
||||||
@ -54,7 +60,7 @@ export const PlaygroundResultsTable = ({
|
|||||||
? Array(5).fill({
|
? Array(5).fill({
|
||||||
name: 'Feature name',
|
name: 'Feature name',
|
||||||
projectId: 'FeatureProject',
|
projectId: 'FeatureProject',
|
||||||
variant: { name: 'FeatureVariant' },
|
variant: { name: 'FeatureVariant', variants: [] },
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
: searchedData;
|
: searchedData;
|
||||||
@ -78,6 +84,7 @@ export const PlaygroundResultsTable = ({
|
|||||||
state: { sortBy },
|
state: { sortBy },
|
||||||
rows,
|
rows,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
|
setHiddenColumns,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
initialState,
|
initialState,
|
||||||
@ -95,6 +102,17 @@ export const PlaygroundResultsTable = ({
|
|||||||
useSortBy
|
useSortBy
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hiddenColumns = [];
|
||||||
|
if (isSmallScreen) {
|
||||||
|
hiddenColumns.push('projectId');
|
||||||
|
}
|
||||||
|
if (isExtraSmallScreen) {
|
||||||
|
hiddenColumns.push('variant');
|
||||||
|
}
|
||||||
|
setHiddenColumns(hiddenColumns);
|
||||||
|
}, [setHiddenColumns, isExtraSmallScreen, isSmallScreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
@ -122,33 +140,37 @@ export const PlaygroundResultsTable = ({
|
|||||||
}, [loading, sortBy, searchValue]);
|
}, [loading, sortBy, searchValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<>
|
||||||
header={
|
<Box
|
||||||
<PageHeader
|
sx={{
|
||||||
titleElement={
|
display: 'flex',
|
||||||
features !== undefined
|
justifyContent: 'space-between',
|
||||||
? `Results (${
|
alignItems: 'center',
|
||||||
rows.length < data.length
|
mb: 3,
|
||||||
? `${rows.length} of ${data.length}`
|
}}
|
||||||
: data.length
|
>
|
||||||
})`
|
<Typography variant="subtitle1" sx={{ ml: 1 }}>
|
||||||
: 'Results'
|
{features !== undefined && !loading
|
||||||
}
|
? `Results (${
|
||||||
actions={
|
rows.length < data.length
|
||||||
<Search
|
? `${rows.length} of ${data.length}`
|
||||||
initialValue={searchValue}
|
: data.length
|
||||||
onChange={setSearchValue}
|
})`
|
||||||
hasFilters
|
: 'Results'}
|
||||||
getSearchContext={getSearchContext}
|
</Typography>
|
||||||
disabled={loading}
|
|
||||||
/>
|
<Search
|
||||||
}
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
disabled={loading}
|
||||||
|
containerStyles={{ marginLeft: '1rem', maxWidth: '400px' }}
|
||||||
/>
|
/>
|
||||||
}
|
</Box>
|
||||||
isLoading={loading}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!loading && (!data || data.length === 0)}
|
condition={!loading && !data}
|
||||||
show={() => (
|
show={() => (
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
{data === undefined
|
{data === undefined
|
||||||
@ -157,7 +179,7 @@ export const PlaygroundResultsTable = ({
|
|||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
)}
|
)}
|
||||||
elseShow={() => (
|
elseShow={() => (
|
||||||
<>
|
<Box ref={ref}>
|
||||||
<SearchHighlightProvider
|
<SearchHighlightProvider
|
||||||
value={getSearchText(searchValue)}
|
value={getSearchText(searchValue)}
|
||||||
>
|
>
|
||||||
@ -187,7 +209,9 @@ export const PlaygroundResultsTable = ({
|
|||||||
</Table>
|
</Table>
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={searchValue?.length > 0}
|
condition={
|
||||||
|
data.length === 0 && searchValue?.length > 0
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
No feature toggles found matching “
|
No feature toggles found matching “
|
||||||
@ -195,10 +219,21 @@ export const PlaygroundResultsTable = ({
|
|||||||
</TablePlaceholder>
|
</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',
|
Header: 'Name',
|
||||||
accessor: 'name',
|
accessor: 'name',
|
||||||
searchable: true,
|
searchable: true,
|
||||||
width: '60%',
|
minWidth: 160,
|
||||||
Cell: ({ value, row: { original } }: any) => (
|
Cell: ({ value, row: { original } }: any) => (
|
||||||
<LinkCell
|
<LinkCell
|
||||||
title={value}
|
title={value}
|
||||||
@ -233,18 +268,26 @@ const COLUMNS = [
|
|||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
filterName: 'variant',
|
filterName: 'variant',
|
||||||
searchable: true,
|
searchable: true,
|
||||||
maxWidth: 170,
|
width: 200,
|
||||||
Cell: ({
|
Cell: ({
|
||||||
value,
|
value,
|
||||||
row: {
|
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',
|
Header: 'isEnabled',
|
||||||
accessor: 'isEnabled',
|
accessor: 'isEnabled',
|
||||||
maxWidth: 170,
|
filterName: 'isEnabled',
|
||||||
|
filterParsing: (value: boolean) => (value ? 'true' : 'false'),
|
||||||
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
|
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
|
||||||
sortType: 'boolean',
|
sortType: 'boolean',
|
||||||
sortInverted: true,
|
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,
|
PlaygroundResponseSchema,
|
||||||
} from './playground.model';
|
} from './playground.model';
|
||||||
|
|
||||||
const usePlaygroundApi = () => {
|
export const usePlaygroundApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
@ -33,5 +33,3 @@ const usePlaygroundApi = () => {
|
|||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePlaygroundApi;
|
|
||||||
|
@ -91,6 +91,11 @@ export default createTheme({
|
|||||||
dark: colors.grey[800],
|
dark: colors.grey[800],
|
||||||
border: colors.grey[500],
|
border: colors.grey[500],
|
||||||
},
|
},
|
||||||
|
tertiary: {
|
||||||
|
light: colors.grey[200],
|
||||||
|
main: colors.grey[400],
|
||||||
|
dark: colors.grey[600],
|
||||||
|
},
|
||||||
divider: colors.grey[300],
|
divider: colors.grey[300],
|
||||||
dividerAlternative: colors.grey[400],
|
dividerAlternative: colors.grey[400],
|
||||||
tableHeaderHover: colors.grey[400],
|
tableHeaderHover: colors.grey[400],
|
||||||
@ -98,10 +103,12 @@ export default createTheme({
|
|||||||
secondaryContainer: colors.grey[200],
|
secondaryContainer: colors.grey[200],
|
||||||
sidebarContainer: 'rgba(32,32,33, 0.2)',
|
sidebarContainer: 'rgba(32,32,33, 0.2)',
|
||||||
grey: colors.grey,
|
grey: colors.grey,
|
||||||
|
lightBorder: colors.grey[400],
|
||||||
text: {
|
text: {
|
||||||
primary: colors.grey[900],
|
primary: colors.grey[900],
|
||||||
secondary: colors.grey[800],
|
secondary: colors.grey[800],
|
||||||
disabled: colors.grey[600],
|
disabled: colors.grey[600],
|
||||||
|
tertiaryContrast: '#fff',
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
main: '#0b8c8f',
|
main: '#0b8c8f',
|
||||||
|
@ -79,6 +79,20 @@ declare module '@mui/material/styles' {
|
|||||||
* and not with `import YourIcon from "@mui/icons/YourIcon"`.
|
* and not with `import YourIcon from "@mui/icons/YourIcon"`.
|
||||||
*/
|
*/
|
||||||
inactiveIcon: string;
|
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 {}
|
interface Theme extends CustomTheme {}
|
||||||
@ -87,6 +101,8 @@ declare module '@mui/material/styles' {
|
|||||||
interface Palette extends CustomPalette {}
|
interface Palette extends CustomPalette {}
|
||||||
interface PaletteOptions extends CustomPalette {}
|
interface PaletteOptions extends CustomPalette {}
|
||||||
|
|
||||||
|
interface TypeText extends CustomTypeText {}
|
||||||
|
|
||||||
interface PaletteColor {
|
interface PaletteColor {
|
||||||
light: string;
|
light: string;
|
||||||
main: string;
|
main: string;
|
||||||
|
@ -1023,6 +1023,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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":
|
"@babel/template@^7.16.7":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
||||||
@ -1080,6 +1087,88 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.16.7"
|
"@babel/helper-validator-identifier" "^7.16.7"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@colors/colors@1.5.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||||
@ -1350,6 +1439,33 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.0.3"
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@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":
|
"@mswjs/cookies@^0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23"
|
resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23"
|
||||||
@ -2092,6 +2208,29 @@
|
|||||||
"@typescript-eslint/types" "5.23.0"
|
"@typescript-eslint/types" "5.23.0"
|
||||||
eslint-visitor-keys "^3.0.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":
|
"@vitejs/plugin-react@1.3.2":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz#2fcf0b6ce9bcdcd4cec5c760c199779d5657ece1"
|
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"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
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:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
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"
|
path-type "^4.0.0"
|
||||||
yaml "^1.10.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:
|
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
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"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
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:
|
stylis@4.0.13:
|
||||||
version "4.0.13"
|
version "4.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
|
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
|
||||||
@ -6140,6 +6302,11 @@ w3c-hr-time@^1.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
browser-process-hrtime "^1.0.0"
|
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:
|
w3c-xmlserializer@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923"
|
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923"
|
||||||
|
Loading…
Reference in New Issue
Block a user