1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-15 17:50:48 +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:
Tymoteusz Czech 2022-07-13 16:35:43 +02:00 committed by GitHub
parent 1f931c1ecc
commit 9d74fd976d
14 changed files with 318 additions and 87 deletions

View 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

View 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

View File

@ -17,9 +17,23 @@ interface IPageContentProps extends PaperProps {
* @deprecated fix feature event log and remove * @deprecated fix feature event log and remove
*/ */
disableBorder?: boolean; disableBorder?: boolean;
disableLoading?: boolean;
bodyClass?: string; 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> = ({ export const PageContent: FC<IPageContentProps> = ({
children, children,
header, header,
@ -27,11 +41,11 @@ export const PageContent: FC<IPageContentProps> = ({
disableBorder = false, disableBorder = false,
bodyClass = '', bodyClass = '',
isLoading = false, isLoading = false,
disableLoading = false,
className, className,
...rest ...rest
}) => { }) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const ref = useLoading(isLoading);
const headerClasses = classnames(styles.headerContainer, { const headerClasses = classnames(styles.headerContainer, {
[styles.paddingDisabled]: disablePadding, [styles.paddingDisabled]: disablePadding,
@ -48,27 +62,33 @@ export const PageContent: FC<IPageContentProps> = ({
const paperProps = disableBorder ? { elevation: 0 } : {}; 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 ( return (
<div ref={ref} aria-busy={isLoading} aria-live="polite"> <PageContentLoading isLoading={isLoading}>{content}</PageContentLoading>
<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>
); );
}; };

View File

@ -15,6 +15,7 @@ interface ISearchProps {
className?: string; className?: string;
placeholder?: string; placeholder?: string;
hasFilters?: boolean; hasFilters?: boolean;
disabled?: boolean;
getSearchContext?: () => IGetSearchContextOutput; getSearchContext?: () => IGetSearchContextOutput;
} }
@ -24,6 +25,7 @@ export const Search = ({
className, className,
placeholder: customPlaceholder, placeholder: customPlaceholder,
hasFilters, hasFilters,
disabled,
getSearchContext, getSearchContext,
}: ISearchProps) => { }: ISearchProps) => {
const ref = useRef<HTMLInputElement>(); const ref = useRef<HTMLInputElement>();
@ -79,6 +81,7 @@ export const Search = ({
onChange={e => onSearchChange(e.target.value)} onChange={e => onSearchChange(e.target.value)}
onFocus={() => setShowSuggestions(true)} onFocus={() => setShowSuggestions(true)}
onBlur={() => setShowSuggestions(false)} onBlur={() => setShowSuggestions(false)}
disabled={disabled}
/> />
<div <div
className={classnames( className={classnames(

View File

@ -1,4 +1,5 @@
import { FormEventHandler, useState, VFC } from 'react'; import { FormEventHandler, useEffect, useState, VFC } from 'react';
import { useSearchParams } from 'react-router-dom';
import { import {
Box, Box,
Button, Button,
@ -13,22 +14,87 @@ import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/Pla
import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset'; 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 { 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 {} interface IPlaygroundProps {}
export const Playground: VFC<IPlaygroundProps> = () => { export const Playground: VFC<IPlaygroundProps> = () => {
const theme = useTheme(); const theme = useTheme();
const [environment, onSetEnvironment] = useState<string>('');
const [projects, onSetProjects] = useState<string[]>([]);
const [context, setContext] = useState<string>(); const [context, setContext] = useState<string>();
const [contextObject, setContextObject] = useState<string>(); const [results, setResults] = useState<
PlaygroundResponseSchema | undefined
>();
const { setToastData } = useToast(); 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(); event.preventDefault();
try { try {
setContextObject( const parsedContext = JSON.parse(context || '{}');
JSON.stringify(JSON.parse(context || '{}'), null, 2) 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) { } catch (error: unknown) {
setToastData({ setToastData({
type: 'error', type: 'error',
@ -38,12 +104,18 @@ export const Playground: VFC<IPlaygroundProps> = () => {
}; };
return ( return (
<PageContent header={<PageHeader title="Unleash playground" />}> <PageContent
header={<PageHeader title="Unleash playground" />}
disableLoading
bodyClass={'no-padding'}
>
<Paper <Paper
elevation={0} elevation={0}
sx={{ sx={{
px: 4, px: 4,
py: 3, py: 3,
mb: 4,
m: 4,
background: theme.palette.grey[200], background: theme.palette.grey[200],
}} }}
> >
@ -55,7 +127,12 @@ export const Playground: VFC<IPlaygroundProps> = () => {
> >
Configure playground Configure playground
</Typography> </Typography>
<PlaygroundConnectionFieldset /> <PlaygroundConnectionFieldset
environment={environment}
projects={projects}
setEnvironment={onSetEnvironment}
setProjects={onSetProjects}
/>
<Divider <Divider
variant="fullWidth" variant="fullWidth"
sx={{ sx={{
@ -81,12 +158,33 @@ export const Playground: VFC<IPlaygroundProps> = () => {
</Button> </Button>
</Box> </Box>
</Paper> </Paper>
{Boolean(contextObject) && ( <ConditionallyRender
<Box sx={{ p: 4 }}> condition={Boolean(results)}
<Typography>TODO: Request</Typography> show={
<pre>{contextObject}</pre> <>
</Box> <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> </PageContent>
); );
}; };

View File

@ -33,12 +33,15 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { setToastData } = useToast(); const { setToastData } = useToast();
const { context } = useUnleashContext(); const { context: contextData } = useUnleashContext();
const contextOptions = context const contextOptions = contextData
.sort((a, b) => a.sortOrder - b.sortOrder) .sort((a, b) => a.sortOrder - b.sortOrder)
.map(({ name }) => name); .map(({ name }) => name);
const [error, setError] = useState<string>(); 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) => { debounce((input?: string) => {
if (!input) { if (!input) {
@ -46,22 +49,22 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
} }
try { try {
JSON.parse(input); const contextValue = JSON.parse(input);
setFieldExist(contextValue[contextField] !== undefined);
} catch (error: unknown) { } catch (error: unknown) {
return setError(formatUnknownError(error)); return setError(formatUnknownError(error));
} }
return setError(undefined); return setError(undefined);
}, 250), }, 250),
[setError] [setError, contextField, setFieldExist]
); );
useEffect(() => { useEffect(() => {
debounceSetError(value); debounceJsonParsing(value);
}, [debounceSetError, value]); }, [debounceJsonParsing, value]);
const [contextField, setContextField] = useState<string>('');
const [contextValue, setContextValue] = useState<string>('');
const onAddField = () => { const onAddField = () => {
try { try {
const currentValue = JSON.parse(value || '{}'); const currentValue = JSON.parse(value || '{}');
@ -75,6 +78,7 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
2 2
) )
); );
setContextValue('');
} catch (error) { } catch (error) {
setToastData({ setToastData({
type: 'error', type: 'error',
@ -154,7 +158,11 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
disabled={!contextField || Boolean(error)} disabled={!contextField || Boolean(error)}
onClick={onAddField} onClick={onAddField}
> >
Add context field {`${
!fieldExist
? 'Add context field'
: 'Replace context field value'
} `}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@ -1,4 +1,4 @@
import { ComponentProps, useState, VFC } from 'react'; import { ComponentProps, VFC } from 'react';
import { import {
Autocomplete, Autocomplete,
Box, Box,
@ -9,7 +9,12 @@ import {
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';
interface IPlaygroundConnectionFieldsetProps {} interface IPlaygroundConnectionFieldsetProps {
environment: string;
projects: string[];
setProjects: (projects: string[]) => void;
setEnvironment: (environment: string) => void;
}
interface IOption { interface IOption {
label: string; label: string;
@ -20,7 +25,7 @@ const allOption: IOption = { label: 'ALL', id: '*' };
export const PlaygroundConnectionFieldset: VFC< export const PlaygroundConnectionFieldset: VFC<
IPlaygroundConnectionFieldsetProps IPlaygroundConnectionFieldsetProps
> = () => { > = ({ environment, projects, setProjects, setEnvironment }) => {
const theme = useTheme(); const theme = useTheme();
const { environments } = useEnvironments(); const { environments } = useEnvironments();
const environmentOptions = environments const environmentOptions = environments
@ -36,7 +41,6 @@ export const PlaygroundConnectionFieldset: VFC<
id, id,
})), })),
]; ];
const [projects, setProjects] = useState<IOption | IOption[]>(allOption);
const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = ( const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = (
event, event,
@ -45,26 +49,29 @@ export const PlaygroundConnectionFieldset: VFC<
) => { ) => {
const newProjects = value as IOption | IOption[]; const newProjects = value as IOption | IOption[];
if (reason === 'clear' || newProjects === null) { if (reason === 'clear' || newProjects === null) {
return setProjects(allOption); return setProjects([allOption.id]);
} }
if (Array.isArray(newProjects)) { if (Array.isArray(newProjects)) {
if (newProjects.length === 0) { if (newProjects.length === 0) {
return setProjects(allOption); return setProjects([allOption.id]);
} }
if ( if (
newProjects.find(({ id }) => id === allOption.id) !== undefined 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) { 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 ( return (
<Box sx={{ pb: 2 }}> <Box sx={{ pb: 2 }}>
<Typography <Typography
@ -83,19 +90,27 @@ export const PlaygroundConnectionFieldset: VFC<
renderInput={params => ( renderInput={params => (
<TextField {...params} label="Environment" required /> <TextField {...params} label="Environment" required />
)} )}
value={environment}
onChange={(event, value) => setEnvironment(value || '')}
size="small" size="small"
/> />
<Autocomplete <Autocomplete
disablePortal disablePortal
id="projects" id="projects"
multiple={Array.isArray(projects)} multiple={!isAllProjects}
options={projectsOptions} options={projectsOptions}
sx={{ width: 300, maxWidth: '100%' }} sx={{ width: 300, maxWidth: '100%' }}
renderInput={params => ( renderInput={params => (
<TextField {...params} label="Projects" /> <TextField {...params} label="Projects" />
)} )}
size="small" size="small"
value={projects} value={
isAllProjects
? allOption
: projectsOptions.filter(({ id }) =>
projects.includes(id)
)
}
onChange={onProjectsChange} onChange={onProjectsChange}
/> />
</Box> </Box>

View File

@ -1,24 +1,37 @@
import { colors } from 'themes/colors'; import { colors } from 'themes/colors';
import { Alert, styled } from '@mui/material'; import { Alert, styled } from '@mui/material';
import { SdkContextSchema } from '../../playground.model'; import { SdkContextSchema } from 'hooks/api/actions/usePlayground/playground.model';
interface IContextBannerProps { interface IContextBannerProps {
environment: string;
projects?: string | string[];
context: SdkContextSchema; context: SdkContextSchema;
} }
const StyledContextFieldList = styled('ul')(({ theme }) => ({ const StyledContextFieldList = styled('ul')(({ theme }) => ({
color: colors.black, color: colors.black,
listStyleType: 'none', listStyleType: 'none',
paddingInlineStart: theme.spacing(16), padding: theme.spacing(2),
})); }));
export const ContextBanner = ({ context }: IContextBannerProps) => { export const ContextBanner = ({
environment,
projects = [],
context,
}: IContextBannerProps) => {
return ( return (
<Alert severity="info" sx={{ my: 2 }}> <Alert severity="info" sx={{ mt: 4, mb: 2, mx: 4 }}>
Your results are generated based on this configuration Your results are generated based on this configuration:
<StyledContextFieldList> <StyledContextFieldList>
<li>environment: {environment}</li>
<li>
projects:{' '}
{Array.isArray(projects) ? projects.join(', ') : projects}
</li>
{Object.entries(context).map(([key, value]) => ( {Object.entries(context).map(([key, value]) => (
<li key={key}>{`${key}: ${value}`}</li> <li key={key}>
<span>{key}:</span> {value}
</li>
))} ))}
</StyledContextFieldList> </StyledContextFieldList>
</Alert> </Alert>

View File

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { colors } from 'themes/colors'; import { colors } from 'themes/colors';
import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg'; import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg';
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.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'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IFeatureStatusCellProps { 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) => { export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
const theme = useTheme(); const theme = useTheme();
const icon = ( const icon = (
@ -43,13 +52,13 @@ export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
condition={enabled} condition={enabled}
show={ show={
<FeatureEnabledIcon <FeatureEnabledIcon
stroke={theme.palette.success.main} color={theme.palette.success.main}
strokeWidth="0.25" strokeWidth="0.25"
/> />
} }
elseShow={ elseShow={
<FeatureDisabledIcon <FeatureDisabledIcon
stroke={theme.palette.error.main} color={theme.palette.error.main}
strokeWidth="0.25" strokeWidth="0.25"
/> />
} }
@ -59,12 +68,14 @@ export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
const label = enabled ? 'True' : 'False'; const label = enabled ? 'True' : 'False';
return ( return (
<TextCell> <StyledCellBox>
<ConditionallyRender <StyledChipWrapper data-loading>
condition={enabled} <ConditionallyRender
show={<StyledTrueChip icon={icon} label={label} />} condition={enabled}
elseShow={<StyledFalseChip icon={icon} label={label} />} show={<StyledTrueChip icon={icon} label={label} />}
/> elseShow={<StyledFalseChip icon={icon} label={label} />}
</TextCell> />
</StyledChipWrapper>
</StyledCellBox>
); );
}; };

View File

@ -20,7 +20,7 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { useSearch } from 'hooks/useSearch'; 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 '../playground.model'; import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playground.model';
const defaultSort: SortingRule<string> = { id: 'name' }; const defaultSort: SortingRule<string> = { id: 'name' };
const { value, setValue } = createLocalStorage( const { value, setValue } = createLocalStorage(
@ -53,9 +53,9 @@ export const PlaygroundResultsTable = ({
return loading return loading
? Array(5).fill({ ? Array(5).fill({
name: 'Feature name', name: 'Feature name',
project: 'Feature Project', projectId: 'FeatureProject',
variant: 'Feature variant', variant: { name: 'FeatureVariant' },
enabled: 'Feature state', enabled: true,
}) })
: searchedData; : searchedData;
}, [searchedData, loading]); }, [searchedData, loading]);
@ -109,6 +109,8 @@ export const PlaygroundResultsTable = ({
} }
if (searchValue) { if (searchValue) {
tableState.search = searchValue; tableState.search = searchValue;
} else {
delete tableState.search;
} }
setSearchParams(tableState, { setSearchParams(tableState, {
@ -138,6 +140,7 @@ export const PlaygroundResultsTable = ({
onChange={setSearchValue} onChange={setSearchValue}
hasFilters hasFilters
getSearchContext={getSearchContext} getSearchContext={getSearchContext}
disabled={loading}
/> />
} }
/> />
@ -145,10 +148,12 @@ export const PlaygroundResultsTable = ({
isLoading={loading} isLoading={loading}
> >
<ConditionallyRender <ConditionallyRender
condition={!loading && data.length === 0} condition={!loading && (!data || data.length === 0)}
show={() => ( show={() => (
<TablePlaceholder> <TablePlaceholder>
None of the feature toggles were evaluated yet. {data === undefined
? 'None of the feature toggles were evaluated yet.'
: 'No results found.'}
</TablePlaceholder> </TablePlaceholder>
)} )}
elseShow={() => ( elseShow={() => (
@ -203,8 +208,11 @@ const COLUMNS = [
accessor: 'name', accessor: 'name',
searchable: true, searchable: true,
width: '60%', width: '60%',
Cell: ({ value }: any) => ( Cell: ({ value, row: { original } }: any) => (
<LinkCell title={value} to={`/feature/${value}`} /> <LinkCell
title={value}
to={`/projects/${original?.projectId}/features/${value}`}
/>
), ),
}, },
{ {
@ -226,7 +234,12 @@ const COLUMNS = [
filterName: 'variant', filterName: 'variant',
searchable: true, searchable: true,
maxWidth: 170, maxWidth: 170,
Cell: ({ value }: any) => <HighlightCell value={value} />, Cell: ({
value,
row: {
original: { variant },
},
}: any) => <HighlightCell value={variant?.enabled ? value : ''} />,
}, },
{ {
Header: 'isEnabled', Header: 'isEnabled',
@ -234,5 +247,6 @@ const COLUMNS = [
maxWidth: 170, maxWidth: 170,
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />, Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
sortType: 'boolean', sortType: 'boolean',
sortInverted: true,
}, },
]; ];

View File

@ -1,3 +1,5 @@
// TODO: replace with auto-generated openapi code
export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum { export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum {
Json = 'json', Json = 'json',
Csv = 'csv', Csv = 'csv',

View File

@ -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;

View File

@ -112,7 +112,7 @@ export const getColumnValues = (column: any, row: any) => {
: column.accessor.includes('.') : column.accessor.includes('.')
? column.accessor ? column.accessor
.split('.') .split('.')
.reduce((object: any, key: string) => object[key], row) .reduce((object: any, key: string) => object?.[key], row)
: row[column.accessor]; : row[column.accessor];
if (column.filterParsing) { if (column.filterParsing) {

View File

@ -15,7 +15,7 @@ export const sortTypes = {
return a === b ? 0 : a ? 1 : -1; return a === b ? 0 : a ? 1 : -1;
}, },
alphanumeric: (a: any, b: any, id: string) => alphanumeric: (a: any, b: any, id: string) =>
a?.values?.[id] (a?.values?.[id] || '')
?.toLowerCase() ?.toLowerCase()
.localeCompare(b?.values?.[id]?.toLowerCase()), .localeCompare(b?.values?.[id]?.toLowerCase() || ''),
}; };