mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +02:00
Refine playground form (#1136)
* integrate results table with playground form * fix playground api integration * fix: playground loading state from api
This commit is contained in:
parent
1f931c1ecc
commit
9d74fd976d
5
frontend/src/assets/icons/isenabled-false.svg
Normal file
5
frontend/src/assets/icons/isenabled-false.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M8.84835 0.184159C8.56384 -0.0789547 8.12358 -0.0575747 7.86499 0.231912C7.6064 0.521399 7.62741 0.96937 7.91192 1.23248C9.4831 2.68551 10.6078 4.74934 10.6078 7C10.6078 7.40738 10.5709 7.80863 10.5012 8.20125L11.6597 9.35972C11.8783 8.60937 12 7.81877 12 7C12 4.26298 10.6397 1.84083 8.84835 0.184159ZM9.35443 6.99995L9.35418 7.0542L7.63252 5.33255C7.30901 4.52372 6.76993 3.80029 6.11728 3.23173C5.82523 2.97731 5.79118 2.53017 6.04122 2.23302C6.29127 1.93586 6.73072 1.90121 7.02276 2.15563C8.33164 3.29586 9.35443 5.02309 9.35443 6.99995ZM7.57827 8.79752L6.48307 7.70233C6.17606 8.92745 5.08437 9.83342 3.78471 9.83342C2.24685 9.83342 1.00017 8.56492 1.00017 7.00015C1.00017 5.69492 1.8676 4.59583 3.04789 4.26714L0.458225 1.67748C0.172794 1.39205 0.178843 0.923223 0.471736 0.63033C0.764629 0.337437 1.23345 0.331388 1.51889 0.616819L13.2132 12.3111C13.4986 12.5965 13.4926 13.0654 13.1997 13.3583C12.9068 13.6511 12.4379 13.6572 12.1525 13.3718L10.556 11.7753C10.0694 12.539 9.48623 13.2259 8.84835 13.8158C8.56384 14.079 8.12358 14.0576 7.86499 13.7681C7.6064 13.4786 7.62741 13.0306 7.91192 12.7675C8.54021 12.1865 9.0971 11.5078 9.53922 10.7585L8.62356 9.84281C8.20906 10.6195 7.64861 11.2989 7.02276 11.8441C6.73072 12.0986 6.29127 12.0639 6.04122 11.7668C5.79118 11.4696 5.82523 11.0225 6.11728 10.768C6.73471 10.2302 7.25048 9.55374 7.57827 8.79752Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
5
frontend/src/assets/icons/isenabled-true.svg
Normal file
5
frontend/src/assets/icons/isenabled-true.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M8.84818 0.184159C8.56367 -0.0789547 8.1234 -0.0575747 7.86482 0.231912C7.60623 0.521399 7.62724 0.96937 7.91175 1.23248C9.48292 2.68551 10.6076 4.74934 10.6076 7C10.6076 9.25066 9.48292 11.3145 7.91175 12.7675C7.62724 13.0306 7.60623 13.4786 7.86482 13.7681C8.1234 14.0576 8.56367 14.079 8.84818 13.8158C10.6396 12.1592 11.9998 9.73702 11.9998 7C11.9998 4.26298 10.6396 1.84083 8.84818 0.184159ZM6.04105 2.23302C6.29109 1.93586 6.73054 1.90121 7.02259 2.15563C8.33146 3.29586 9.35426 5.02309 9.35426 6.99995C9.35426 8.97682 8.33145 10.7039 7.02259 11.8441C6.73054 12.0986 6.29109 12.0639 6.04105 11.7668C5.79101 11.4696 5.82506 11.0225 6.11711 10.768C7.19425 9.82969 7.96199 8.46967 7.96199 6.99995C7.96199 5.53021 7.19423 4.17007 6.11711 3.23173C5.82506 2.97731 5.79101 2.53017 6.04105 2.23302ZM6.56908 7.00015C6.56908 8.56492 5.3224 9.83342 3.78454 9.83342C2.24668 9.83342 1 8.56492 1 7.00015C1 5.43539 2.24668 4.16689 3.78454 4.16689C5.3224 4.16689 6.56908 5.43539 6.56908 7.00015Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -17,9 +17,23 @@ interface IPageContentProps extends PaperProps {
|
||||
* @deprecated fix feature event log and remove
|
||||
*/
|
||||
disableBorder?: boolean;
|
||||
disableLoading?: boolean;
|
||||
bodyClass?: string;
|
||||
}
|
||||
|
||||
const PageContentLoading: FC<{ isLoading: boolean }> = ({
|
||||
children,
|
||||
isLoading,
|
||||
}) => {
|
||||
const ref = useLoading(isLoading);
|
||||
|
||||
return (
|
||||
<div ref={ref} aria-busy={isLoading} aria-live="polite">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageContent: FC<IPageContentProps> = ({
|
||||
children,
|
||||
header,
|
||||
@ -27,11 +41,11 @@ export const PageContent: FC<IPageContentProps> = ({
|
||||
disableBorder = false,
|
||||
bodyClass = '',
|
||||
isLoading = false,
|
||||
disableLoading = false,
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const ref = useLoading(isLoading);
|
||||
|
||||
const headerClasses = classnames(styles.headerContainer, {
|
||||
[styles.paddingDisabled]: disablePadding,
|
||||
@ -48,27 +62,33 @@ export const PageContent: FC<IPageContentProps> = ({
|
||||
|
||||
const paperProps = disableBorder ? { elevation: 0 } : {};
|
||||
|
||||
const content = (
|
||||
<Paper
|
||||
{...rest}
|
||||
{...paperProps}
|
||||
className={classnames(styles.container, className)}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(header)}
|
||||
show={
|
||||
<div className={headerClasses}>
|
||||
<ConditionallyRender
|
||||
condition={typeof header === 'string'}
|
||||
show={<PageHeader title={header as string} />}
|
||||
elseShow={header}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className={bodyClasses}>{children}</div>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
if (disableLoading) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} aria-busy={isLoading} aria-live="polite">
|
||||
<Paper
|
||||
{...rest}
|
||||
{...paperProps}
|
||||
className={classnames(styles.container, className)}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(header)}
|
||||
show={
|
||||
<div className={headerClasses}>
|
||||
<ConditionallyRender
|
||||
condition={typeof header === 'string'}
|
||||
show={<PageHeader title={header as string} />}
|
||||
elseShow={header}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className={bodyClasses}>{children}</div>
|
||||
</Paper>
|
||||
</div>
|
||||
<PageContentLoading isLoading={isLoading}>{content}</PageContentLoading>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ interface ISearchProps {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
hasFilters?: boolean;
|
||||
disabled?: boolean;
|
||||
getSearchContext?: () => IGetSearchContextOutput;
|
||||
}
|
||||
|
||||
@ -24,6 +25,7 @@ export const Search = ({
|
||||
className,
|
||||
placeholder: customPlaceholder,
|
||||
hasFilters,
|
||||
disabled,
|
||||
getSearchContext,
|
||||
}: ISearchProps) => {
|
||||
const ref = useRef<HTMLInputElement>();
|
||||
@ -79,6 +81,7 @@ export const Search = ({
|
||||
onChange={e => onSearchChange(e.target.value)}
|
||||
onFocus={() => setShowSuggestions(true)}
|
||||
onBlur={() => setShowSuggestions(false)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div
|
||||
className={classnames(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FormEventHandler, useState, VFC } from 'react';
|
||||
import { FormEventHandler, useEffect, useState, VFC } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@ -13,22 +14,87 @@ import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/Pla
|
||||
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 { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
||||
|
||||
interface IPlaygroundProps {}
|
||||
|
||||
export const Playground: VFC<IPlaygroundProps> = () => {
|
||||
const theme = useTheme();
|
||||
const [environment, onSetEnvironment] = useState<string>('');
|
||||
const [projects, onSetProjects] = useState<string[]>([]);
|
||||
const [context, setContext] = useState<string>();
|
||||
const [contextObject, setContextObject] = useState<string>();
|
||||
const [results, setResults] = useState<
|
||||
PlaygroundResponseSchema | undefined
|
||||
>();
|
||||
const { setToastData } = useToast();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { evaluatePlayground, loading } = usePlaygroundApi();
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = event => {
|
||||
useEffect(() => {
|
||||
// Load initial values from URL
|
||||
try {
|
||||
const environmentFromUrl = searchParams.get('environment');
|
||||
if (environmentFromUrl) {
|
||||
onSetEnvironment(environmentFromUrl);
|
||||
}
|
||||
const projectsFromUrl = searchParams.get('projects');
|
||||
if (projectsFromUrl) {
|
||||
onSetProjects(projectsFromUrl.split(','));
|
||||
}
|
||||
const contextFromUrl = searchParams.get('context');
|
||||
if (contextFromUrl) {
|
||||
setContext(decodeURI(contextFromUrl));
|
||||
}
|
||||
} catch (error) {
|
||||
setToastData({
|
||||
type: 'error',
|
||||
title: `Failed to parse URL parameters: ${formatUnknownError(
|
||||
error
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
setContextObject(
|
||||
JSON.stringify(JSON.parse(context || '{}'), null, 2)
|
||||
);
|
||||
const parsedContext = JSON.parse(context || '{}');
|
||||
const response = await evaluatePlayground({
|
||||
environment,
|
||||
projects:
|
||||
!projects ||
|
||||
projects.length === 0 ||
|
||||
(projects.length === 1 && projects[0] === '*')
|
||||
? '*'
|
||||
: 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');
|
||||
}
|
||||
setSearchParams(searchParams);
|
||||
|
||||
// Display results
|
||||
setResults(response);
|
||||
} catch (error: unknown) {
|
||||
setToastData({
|
||||
type: 'error',
|
||||
@ -38,12 +104,18 @@ export const Playground: VFC<IPlaygroundProps> = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent header={<PageHeader title="Unleash playground" />}>
|
||||
<PageContent
|
||||
header={<PageHeader title="Unleash playground" />}
|
||||
disableLoading
|
||||
bodyClass={'no-padding'}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
px: 4,
|
||||
py: 3,
|
||||
mb: 4,
|
||||
m: 4,
|
||||
background: theme.palette.grey[200],
|
||||
}}
|
||||
>
|
||||
@ -55,7 +127,12 @@ export const Playground: VFC<IPlaygroundProps> = () => {
|
||||
>
|
||||
Configure playground
|
||||
</Typography>
|
||||
<PlaygroundConnectionFieldset />
|
||||
<PlaygroundConnectionFieldset
|
||||
environment={environment}
|
||||
projects={projects}
|
||||
setEnvironment={onSetEnvironment}
|
||||
setProjects={onSetProjects}
|
||||
/>
|
||||
<Divider
|
||||
variant="fullWidth"
|
||||
sx={{
|
||||
@ -81,12 +158,33 @@ export const Playground: VFC<IPlaygroundProps> = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
{Boolean(contextObject) && (
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Typography>TODO: Request</Typography>
|
||||
<pre>{contextObject}</pre>
|
||||
</Box>
|
||||
)}
|
||||
<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
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<PlaygroundResultsTable
|
||||
loading={loading}
|
||||
features={results?.features}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
@ -33,12 +33,15 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { setToastData } = useToast();
|
||||
const { context } = useUnleashContext();
|
||||
const contextOptions = context
|
||||
const { context: contextData } = useUnleashContext();
|
||||
const contextOptions = contextData
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map(({ name }) => name);
|
||||
const [error, setError] = useState<string>();
|
||||
const debounceSetError = useMemo(
|
||||
const [fieldExist, setFieldExist] = useState<boolean>(false);
|
||||
const [contextField, setContextField] = useState<string>('');
|
||||
const [contextValue, setContextValue] = useState<string>('');
|
||||
const debounceJsonParsing = useMemo(
|
||||
() =>
|
||||
debounce((input?: string) => {
|
||||
if (!input) {
|
||||
@ -46,22 +49,22 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(input);
|
||||
const contextValue = JSON.parse(input);
|
||||
|
||||
setFieldExist(contextValue[contextField] !== undefined);
|
||||
} catch (error: unknown) {
|
||||
return setError(formatUnknownError(error));
|
||||
}
|
||||
|
||||
return setError(undefined);
|
||||
}, 250),
|
||||
[setError]
|
||||
[setError, contextField, setFieldExist]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
debounceSetError(value);
|
||||
}, [debounceSetError, value]);
|
||||
debounceJsonParsing(value);
|
||||
}, [debounceJsonParsing, value]);
|
||||
|
||||
const [contextField, setContextField] = useState<string>('');
|
||||
const [contextValue, setContextValue] = useState<string>('');
|
||||
const onAddField = () => {
|
||||
try {
|
||||
const currentValue = JSON.parse(value || '{}');
|
||||
@ -75,6 +78,7 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
|
||||
2
|
||||
)
|
||||
);
|
||||
setContextValue('');
|
||||
} catch (error) {
|
||||
setToastData({
|
||||
type: 'error',
|
||||
@ -154,7 +158,11 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
|
||||
disabled={!contextField || Boolean(error)}
|
||||
onClick={onAddField}
|
||||
>
|
||||
Add context field
|
||||
{`${
|
||||
!fieldExist
|
||||
? 'Add context field'
|
||||
: 'Replace context field value'
|
||||
} `}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ComponentProps, useState, VFC } from 'react';
|
||||
import { ComponentProps, VFC } from 'react';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
@ -9,7 +9,12 @@ import {
|
||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||
|
||||
interface IPlaygroundConnectionFieldsetProps {}
|
||||
interface IPlaygroundConnectionFieldsetProps {
|
||||
environment: string;
|
||||
projects: string[];
|
||||
setProjects: (projects: string[]) => void;
|
||||
setEnvironment: (environment: string) => void;
|
||||
}
|
||||
|
||||
interface IOption {
|
||||
label: string;
|
||||
@ -20,7 +25,7 @@ const allOption: IOption = { label: 'ALL', id: '*' };
|
||||
|
||||
export const PlaygroundConnectionFieldset: VFC<
|
||||
IPlaygroundConnectionFieldsetProps
|
||||
> = () => {
|
||||
> = ({ environment, projects, setProjects, setEnvironment }) => {
|
||||
const theme = useTheme();
|
||||
const { environments } = useEnvironments();
|
||||
const environmentOptions = environments
|
||||
@ -36,7 +41,6 @@ export const PlaygroundConnectionFieldset: VFC<
|
||||
id,
|
||||
})),
|
||||
];
|
||||
const [projects, setProjects] = useState<IOption | IOption[]>(allOption);
|
||||
|
||||
const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = (
|
||||
event,
|
||||
@ -45,26 +49,29 @@ export const PlaygroundConnectionFieldset: VFC<
|
||||
) => {
|
||||
const newProjects = value as IOption | IOption[];
|
||||
if (reason === 'clear' || newProjects === null) {
|
||||
return setProjects(allOption);
|
||||
return setProjects([allOption.id]);
|
||||
}
|
||||
if (Array.isArray(newProjects)) {
|
||||
if (newProjects.length === 0) {
|
||||
return setProjects(allOption);
|
||||
return setProjects([allOption.id]);
|
||||
}
|
||||
if (
|
||||
newProjects.find(({ id }) => id === allOption.id) !== undefined
|
||||
) {
|
||||
return setProjects(allOption);
|
||||
return setProjects([allOption.id]);
|
||||
}
|
||||
return setProjects(newProjects);
|
||||
return setProjects(newProjects.map(({ id }) => id));
|
||||
}
|
||||
if (newProjects.id === allOption.id) {
|
||||
return setProjects(allOption);
|
||||
return setProjects([allOption.id]);
|
||||
}
|
||||
|
||||
return setProjects([newProjects]);
|
||||
return setProjects([newProjects.id]);
|
||||
};
|
||||
|
||||
const isAllProjects =
|
||||
projects.length === 0 || (projects.length === 1 && projects[0] === '*');
|
||||
|
||||
return (
|
||||
<Box sx={{ pb: 2 }}>
|
||||
<Typography
|
||||
@ -83,19 +90,27 @@ export const PlaygroundConnectionFieldset: VFC<
|
||||
renderInput={params => (
|
||||
<TextField {...params} label="Environment" required />
|
||||
)}
|
||||
value={environment}
|
||||
onChange={(event, value) => setEnvironment(value || '')}
|
||||
size="small"
|
||||
/>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
id="projects"
|
||||
multiple={Array.isArray(projects)}
|
||||
multiple={!isAllProjects}
|
||||
options={projectsOptions}
|
||||
sx={{ width: 300, maxWidth: '100%' }}
|
||||
renderInput={params => (
|
||||
<TextField {...params} label="Projects" />
|
||||
)}
|
||||
size="small"
|
||||
value={projects}
|
||||
value={
|
||||
isAllProjects
|
||||
? allOption
|
||||
: projectsOptions.filter(({ id }) =>
|
||||
projects.includes(id)
|
||||
)
|
||||
}
|
||||
onChange={onProjectsChange}
|
||||
/>
|
||||
</Box>
|
||||
|
@ -1,24 +1,37 @@
|
||||
import { colors } from 'themes/colors';
|
||||
import { Alert, styled } from '@mui/material';
|
||||
import { SdkContextSchema } from '../../playground.model';
|
||||
import { SdkContextSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
||||
|
||||
interface IContextBannerProps {
|
||||
environment: string;
|
||||
projects?: string | string[];
|
||||
context: SdkContextSchema;
|
||||
}
|
||||
|
||||
const StyledContextFieldList = styled('ul')(({ theme }) => ({
|
||||
color: colors.black,
|
||||
listStyleType: 'none',
|
||||
paddingInlineStart: theme.spacing(16),
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const ContextBanner = ({ context }: IContextBannerProps) => {
|
||||
export const ContextBanner = ({
|
||||
environment,
|
||||
projects = [],
|
||||
context,
|
||||
}: IContextBannerProps) => {
|
||||
return (
|
||||
<Alert severity="info" sx={{ my: 2 }}>
|
||||
Your results are generated based on this configuration
|
||||
<Alert severity="info" sx={{ mt: 4, mb: 2, mx: 4 }}>
|
||||
Your results are generated based on this configuration:
|
||||
<StyledContextFieldList>
|
||||
<li>environment: {environment}</li>
|
||||
<li>
|
||||
projects:{' '}
|
||||
{Array.isArray(projects) ? projects.join(', ') : projects}
|
||||
</li>
|
||||
{Object.entries(context).map(([key, value]) => (
|
||||
<li key={key}>{`${key}: ${value}`}</li>
|
||||
<li key={key}>
|
||||
<span>{key}:</span> {value}
|
||||
</li>
|
||||
))}
|
||||
</StyledContextFieldList>
|
||||
</Alert>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { colors } from 'themes/colors';
|
||||
import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg';
|
||||
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg';
|
||||
import { Chip, styled, useTheme } from '@mui/material';
|
||||
import { Box, Chip, styled, useTheme } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IFeatureStatusCellProps {
|
||||
@ -36,6 +35,16 @@ const StyledTrueChip = styled(Chip)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledCellBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1, 2),
|
||||
}));
|
||||
|
||||
const StyledChipWrapper = styled(Box)(() => ({
|
||||
marginRight: 'auto',
|
||||
}));
|
||||
|
||||
export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
|
||||
const theme = useTheme();
|
||||
const icon = (
|
||||
@ -43,13 +52,13 @@ export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
|
||||
condition={enabled}
|
||||
show={
|
||||
<FeatureEnabledIcon
|
||||
stroke={theme.palette.success.main}
|
||||
color={theme.palette.success.main}
|
||||
strokeWidth="0.25"
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<FeatureDisabledIcon
|
||||
stroke={theme.palette.error.main}
|
||||
color={theme.palette.error.main}
|
||||
strokeWidth="0.25"
|
||||
/>
|
||||
}
|
||||
@ -59,12 +68,14 @@ export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
|
||||
const label = enabled ? 'True' : 'False';
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<ConditionallyRender
|
||||
condition={enabled}
|
||||
show={<StyledTrueChip icon={icon} label={label} />}
|
||||
elseShow={<StyledFalseChip icon={icon} label={label} />}
|
||||
/>
|
||||
</TextCell>
|
||||
<StyledCellBox>
|
||||
<StyledChipWrapper data-loading>
|
||||
<ConditionallyRender
|
||||
condition={enabled}
|
||||
show={<StyledTrueChip icon={icon} label={label} />}
|
||||
elseShow={<StyledFalseChip icon={icon} label={label} />}
|
||||
/>
|
||||
</StyledChipWrapper>
|
||||
</StyledCellBox>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell';
|
||||
import { PlaygroundFeatureSchema } from '../playground.model';
|
||||
import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playground.model';
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'name' };
|
||||
const { value, setValue } = createLocalStorage(
|
||||
@ -53,9 +53,9 @@ export const PlaygroundResultsTable = ({
|
||||
return loading
|
||||
? Array(5).fill({
|
||||
name: 'Feature name',
|
||||
project: 'Feature Project',
|
||||
variant: 'Feature variant',
|
||||
enabled: 'Feature state',
|
||||
projectId: 'FeatureProject',
|
||||
variant: { name: 'FeatureVariant' },
|
||||
enabled: true,
|
||||
})
|
||||
: searchedData;
|
||||
}, [searchedData, loading]);
|
||||
@ -109,6 +109,8 @@ export const PlaygroundResultsTable = ({
|
||||
}
|
||||
if (searchValue) {
|
||||
tableState.search = searchValue;
|
||||
} else {
|
||||
delete tableState.search;
|
||||
}
|
||||
|
||||
setSearchParams(tableState, {
|
||||
@ -138,6 +140,7 @@ export const PlaygroundResultsTable = ({
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
disabled={loading}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -145,10 +148,12 @@ export const PlaygroundResultsTable = ({
|
||||
isLoading={loading}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={!loading && data.length === 0}
|
||||
condition={!loading && (!data || data.length === 0)}
|
||||
show={() => (
|
||||
<TablePlaceholder>
|
||||
None of the feature toggles were evaluated yet.
|
||||
{data === undefined
|
||||
? 'None of the feature toggles were evaluated yet.'
|
||||
: 'No results found.'}
|
||||
</TablePlaceholder>
|
||||
)}
|
||||
elseShow={() => (
|
||||
@ -203,8 +208,11 @@ const COLUMNS = [
|
||||
accessor: 'name',
|
||||
searchable: true,
|
||||
width: '60%',
|
||||
Cell: ({ value }: any) => (
|
||||
<LinkCell title={value} to={`/feature/${value}`} />
|
||||
Cell: ({ value, row: { original } }: any) => (
|
||||
<LinkCell
|
||||
title={value}
|
||||
to={`/projects/${original?.projectId}/features/${value}`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -226,7 +234,12 @@ const COLUMNS = [
|
||||
filterName: 'variant',
|
||||
searchable: true,
|
||||
maxWidth: 170,
|
||||
Cell: ({ value }: any) => <HighlightCell value={value} />,
|
||||
Cell: ({
|
||||
value,
|
||||
row: {
|
||||
original: { variant },
|
||||
},
|
||||
}: any) => <HighlightCell value={variant?.enabled ? value : ''} />,
|
||||
},
|
||||
{
|
||||
Header: 'isEnabled',
|
||||
@ -234,5 +247,6 @@ const COLUMNS = [
|
||||
maxWidth: 170,
|
||||
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
|
||||
sortType: 'boolean',
|
||||
sortInverted: true,
|
||||
},
|
||||
];
|
||||
|
@ -1,3 +1,5 @@
|
||||
// TODO: replace with auto-generated openapi code
|
||||
|
||||
export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum {
|
||||
Json = 'json',
|
||||
Csv = 'csv',
|
@ -0,0 +1,37 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
import {
|
||||
PlaygroundRequestSchema,
|
||||
PlaygroundResponseSchema,
|
||||
} from './playground.model';
|
||||
|
||||
const usePlaygroundApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const URI = 'api/admin/playground';
|
||||
|
||||
const evaluatePlayground = async (payload: PlaygroundRequestSchema) => {
|
||||
const path = URI;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res.json() as Promise<PlaygroundResponseSchema>;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
evaluatePlayground,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePlaygroundApi;
|
@ -112,7 +112,7 @@ export const getColumnValues = (column: any, row: any) => {
|
||||
: column.accessor.includes('.')
|
||||
? column.accessor
|
||||
.split('.')
|
||||
.reduce((object: any, key: string) => object[key], row)
|
||||
.reduce((object: any, key: string) => object?.[key], row)
|
||||
: row[column.accessor];
|
||||
|
||||
if (column.filterParsing) {
|
||||
|
@ -15,7 +15,7 @@ export const sortTypes = {
|
||||
return a === b ? 0 : a ? 1 : -1;
|
||||
},
|
||||
alphanumeric: (a: any, b: any, id: string) =>
|
||||
a?.values?.[id]
|
||||
(a?.values?.[id] || '')
|
||||
?.toLowerCase()
|
||||
.localeCompare(b?.values?.[id]?.toLowerCase()),
|
||||
.localeCompare(b?.values?.[id]?.toLowerCase() || ''),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user