mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: Advanced playground table (#3978)
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> Implements the Advanced Playground Table ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # [1-1007](https://linear.app/unleash/issue/1-1007/env-aware-results-table) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ![Screenshot 2023-06-14 at 15 04 08](https://github.com/Unleash/unleash/assets/104830839/2f76d6f5-f92b-4586-bb4b-265f26eeb836) --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
a066d7888d
commit
650f6cc857
@ -133,14 +133,7 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": {
|
"component": [Function],
|
||||||
"$$typeof": Symbol(react.lazy),
|
|
||||||
"_init": [Function],
|
|
||||||
"_payload": {
|
|
||||||
"_result": [Function],
|
|
||||||
"_status": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"menu": {
|
"menu": {
|
||||||
"mobile": true,
|
"mobile": true,
|
||||||
|
@ -0,0 +1,246 @@
|
|||||||
|
import { FormEventHandler, useEffect, useState, VFC } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Box, Paper, useMediaQuery, useTheme } from '@mui/material';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { usePlaygroundApi } from 'hooks/api/actions/usePlayground/usePlayground';
|
||||||
|
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
|
import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm';
|
||||||
|
import {
|
||||||
|
resolveDefaultEnvironment,
|
||||||
|
resolveEnvironments,
|
||||||
|
resolveProjects,
|
||||||
|
resolveResultsWidth,
|
||||||
|
} from './playground.utils';
|
||||||
|
import { PlaygroundGuidance } from './PlaygroundGuidance/PlaygroundGuidance';
|
||||||
|
import { PlaygroundGuidancePopper } from './PlaygroundGuidancePopper/PlaygroundGuidancePopper';
|
||||||
|
import Loader from '../../common/Loader/Loader';
|
||||||
|
import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable';
|
||||||
|
import { AdvancedPlaygroundResponseSchema } from 'openapi';
|
||||||
|
|
||||||
|
export const AdvancedPlayground: VFC<{}> = () => {
|
||||||
|
const { environments: availableEnvironments } = useEnvironments();
|
||||||
|
const theme = useTheme();
|
||||||
|
const matches = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
|
const [environments, setEnvironments] = useState<string[]>([]);
|
||||||
|
const [projects, setProjects] = useState<string[]>([]);
|
||||||
|
const [context, setContext] = useState<string>();
|
||||||
|
const [results, setResults] = useState<
|
||||||
|
AdvancedPlaygroundResponseSchema | undefined
|
||||||
|
>();
|
||||||
|
const { setToastData } = useToast();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const { evaluateAdvancedPlayground, loading } = usePlaygroundApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEnvironments([resolveDefaultEnvironment(availableEnvironments)]);
|
||||||
|
}, [availableEnvironments]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadInitialValuesFromUrl();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadInitialValuesFromUrl = () => {
|
||||||
|
try {
|
||||||
|
const environments = resolveEnvironmentsFromUrl();
|
||||||
|
const projects = resolveProjectsFromUrl();
|
||||||
|
const context = resolveContextFromUrl();
|
||||||
|
const makePlaygroundRequest = async () => {
|
||||||
|
if (environments && context) {
|
||||||
|
await evaluatePlaygroundContext(
|
||||||
|
environments || [],
|
||||||
|
projects || '*',
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
makePlaygroundRequest();
|
||||||
|
} catch (error) {
|
||||||
|
setToastData({
|
||||||
|
type: 'error',
|
||||||
|
title: `Failed to parse URL parameters: ${formatUnknownError(
|
||||||
|
error
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveEnvironmentsFromUrl = (): string[] | null => {
|
||||||
|
let environmentArray: string[] | null = null;
|
||||||
|
const environmentsFromUrl = searchParams.get('environments');
|
||||||
|
if (environmentsFromUrl) {
|
||||||
|
environmentArray = environmentsFromUrl.split(',');
|
||||||
|
setEnvironments(environmentArray);
|
||||||
|
}
|
||||||
|
return environmentArray;
|
||||||
|
};
|
||||||
|
const resolveProjectsFromUrl = (): string[] | null => {
|
||||||
|
let projectsArray: string[] | null = null;
|
||||||
|
let projectsFromUrl = searchParams.get('projects');
|
||||||
|
if (projectsFromUrl) {
|
||||||
|
projectsArray = projectsFromUrl.split(',');
|
||||||
|
setProjects(projectsArray);
|
||||||
|
}
|
||||||
|
return projectsArray;
|
||||||
|
};
|
||||||
|
const resolveContextFromUrl = () => {
|
||||||
|
let contextFromUrl = searchParams.get('context');
|
||||||
|
if (contextFromUrl) {
|
||||||
|
contextFromUrl = decodeURI(contextFromUrl);
|
||||||
|
setContext(contextFromUrl);
|
||||||
|
}
|
||||||
|
return contextFromUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const evaluatePlaygroundContext = async (
|
||||||
|
environments: string[] | string,
|
||||||
|
projects: string[] | string,
|
||||||
|
context: string | undefined,
|
||||||
|
action?: () => void
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const parsedContext = JSON.parse(context || '{}');
|
||||||
|
const response = await evaluateAdvancedPlayground({
|
||||||
|
environments: resolveEnvironments(environments),
|
||||||
|
projects: resolveProjects(projects),
|
||||||
|
context: {
|
||||||
|
appName: 'playground',
|
||||||
|
...parsedContext,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (action && typeof action === 'function') {
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
setResults(response);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastData({
|
||||||
|
type: 'error',
|
||||||
|
title: `Error parsing context: ${formatUnknownError(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
await evaluatePlaygroundContext(
|
||||||
|
environments,
|
||||||
|
projects,
|
||||||
|
context,
|
||||||
|
setURLParameters
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setURLParameters = () => {
|
||||||
|
searchParams.set('context', encodeURI(context || '')); // always set because of native validation
|
||||||
|
if (
|
||||||
|
Array.isArray(environments) &&
|
||||||
|
environments.length > 0 &&
|
||||||
|
!(environments.length === 1 && environments[0] === '*')
|
||||||
|
) {
|
||||||
|
searchParams.set('environments', environments.join(','));
|
||||||
|
} else {
|
||||||
|
searchParams.delete('projects');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Array.isArray(projects) &&
|
||||||
|
projects.length > 0 &&
|
||||||
|
!(projects.length === 1 && projects[0] === '*')
|
||||||
|
) {
|
||||||
|
searchParams.set('projects', projects.join(','));
|
||||||
|
} else {
|
||||||
|
searchParams.delete('projects');
|
||||||
|
}
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formWidth = results && !matches ? '35%' : 'auto';
|
||||||
|
const resultsWidth = resolveResultsWidth(matches, results);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title="Unleash playground"
|
||||||
|
actions={<PlaygroundGuidancePopper />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disableLoading
|
||||||
|
bodyClass={'no-padding'}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: !matches ? 'row' : 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
background: theme.palette.background.elevation2,
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
px: 4,
|
||||||
|
py: 3,
|
||||||
|
mb: 4,
|
||||||
|
mt: 2,
|
||||||
|
background: theme.palette.background.elevation2,
|
||||||
|
transition: 'width 0.4s ease',
|
||||||
|
minWidth: matches ? 'auto' : '500px',
|
||||||
|
width: formWidth,
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlaygroundForm
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
context={context}
|
||||||
|
setContext={setContext}
|
||||||
|
availableEnvironments={availableEnvironments}
|
||||||
|
projects={projects}
|
||||||
|
environments={environments}
|
||||||
|
setProjects={setProjects}
|
||||||
|
setEnvironments={setEnvironments}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={theme => ({
|
||||||
|
width: resultsWidth,
|
||||||
|
transition: 'width 0.4s ease',
|
||||||
|
padding: theme.spacing(4, 2),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={loading}
|
||||||
|
show={<Loader />}
|
||||||
|
elseShow={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(results)}
|
||||||
|
show={
|
||||||
|
<AdvancedPlaygroundResultsTable
|
||||||
|
loading={loading}
|
||||||
|
features={results?.features}
|
||||||
|
input={results?.input}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={<PlaygroundGuidance />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedPlayground;
|
@ -0,0 +1,95 @@
|
|||||||
|
import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Box, IconButton, Popover, styled, useTheme } from '@mui/material';
|
||||||
|
import { flexRow } from '../../../../../themes/themeStyles';
|
||||||
|
import { PlaygroundResultChip } from '../../PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip';
|
||||||
|
import { InfoOutlined } from '@mui/icons-material';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AdvancedPlaygroundEnvironmentFeatureSchema } from 'openapi';
|
||||||
|
import { PlaygroundEnvironmentTable } from '../../PlaygroundEnvironmentTable/PlaygroundEnvironmentTable';
|
||||||
|
|
||||||
|
const StyledContainer = styled(
|
||||||
|
'div',
|
||||||
|
{}
|
||||||
|
)(({ theme }) => ({
|
||||||
|
flexGrow: 0,
|
||||||
|
...flexRow,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
margin: theme.spacing(0, 1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledPlaygroundChipContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface IAdvancedPlaygroundEnvironmentCellProps {
|
||||||
|
value: AdvancedPlaygroundEnvironmentFeatureSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdvancedPlaygroundEnvironmentCell = ({
|
||||||
|
value,
|
||||||
|
}: IAdvancedPlaygroundEnvironmentCellProps) => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
const enabled = (value || []).filter(evaluation => evaluation.isEnabled);
|
||||||
|
const disabled = (value || []).filter(evaluation => !evaluation.isEnabled);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledPlaygroundChipContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={enabled.length > 0}
|
||||||
|
show={
|
||||||
|
<PlaygroundResultChip
|
||||||
|
enabled={true}
|
||||||
|
label={`${enabled.length}`}
|
||||||
|
showIcon={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={disabled.length > 0}
|
||||||
|
show={
|
||||||
|
<PlaygroundResultChip
|
||||||
|
enabled={false}
|
||||||
|
label={`${disabled.length}`}
|
||||||
|
showIcon={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledPlaygroundChipContainer>
|
||||||
|
<>
|
||||||
|
<IconButton onClick={onOpen}>
|
||||||
|
<InfoOutlined />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
id={`${value}-result-details`}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClose={onClose}
|
||||||
|
anchorEl={anchor}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: -320,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlaygroundEnvironmentTable features={value} />
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,286 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
SortingRule,
|
||||||
|
useFlexLayout,
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy,
|
||||||
|
useTable,
|
||||||
|
} from 'react-table';
|
||||||
|
|
||||||
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Link,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
import { AdvancedPlaygroundEnvironmentCell } from './AdvancedPlaygroundEnvironmentCell/AdvancedPlaygroundEnvironmentCell';
|
||||||
|
import {
|
||||||
|
AdvancedPlaygroundRequestSchema,
|
||||||
|
AdvancedPlaygroundFeatureSchema,
|
||||||
|
} from 'openapi';
|
||||||
|
import { capitalizeFirst } from 'utils/capitalizeFirst';
|
||||||
|
|
||||||
|
const defaultSort: SortingRule<string> = { id: 'name' };
|
||||||
|
const { value, setValue } = createLocalStorage(
|
||||||
|
'AdvancedPlaygroundResultsTable:v1',
|
||||||
|
defaultSort
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledButton = styled(Link)(({ theme }) => ({
|
||||||
|
textAlign: 'left',
|
||||||
|
textDecorationStyle: 'dotted',
|
||||||
|
textUnderlineOffset: theme.spacing(0.75),
|
||||||
|
color: theme.palette.neutral.dark,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IAdvancedPlaygroundResultsTableProps {
|
||||||
|
features?: AdvancedPlaygroundFeatureSchema[];
|
||||||
|
input?: AdvancedPlaygroundRequestSchema;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdvancedPlaygroundResultsTable = ({
|
||||||
|
features,
|
||||||
|
input,
|
||||||
|
loading,
|
||||||
|
}: IAdvancedPlaygroundResultsTableProps) => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
const [searchValue, setSearchValue] = useState(
|
||||||
|
searchParams.get('search') || ''
|
||||||
|
);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const COLUMNS = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
searchable: true,
|
||||||
|
minWidth: 160,
|
||||||
|
Cell: ({ value, row: { original } }: any) => (
|
||||||
|
<LinkCell
|
||||||
|
title={value}
|
||||||
|
to={`/projects/${original?.projectId}/features/${value}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Project ID',
|
||||||
|
accessor: 'projectId',
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
filterName: 'projectId',
|
||||||
|
searchable: true,
|
||||||
|
minWidth: 150,
|
||||||
|
Cell: ({ value }: any) => (
|
||||||
|
<LinkCell title={value} to={`/projects/${value}`} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...(input?.environments?.map((name: string) => {
|
||||||
|
return {
|
||||||
|
Header: loading ? () => '' : capitalizeFirst(name),
|
||||||
|
maxWidth: 140,
|
||||||
|
id: `environments.${name}`,
|
||||||
|
align: 'flex-start',
|
||||||
|
Cell: ({ row }: any) => (
|
||||||
|
<AdvancedPlaygroundEnvironmentCell
|
||||||
|
value={row.original.environments[name]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}) || []),
|
||||||
|
{
|
||||||
|
Header: 'Diff',
|
||||||
|
minWidth: 150,
|
||||||
|
id: 'diff',
|
||||||
|
align: 'left',
|
||||||
|
Cell: ({ row }: any) => (
|
||||||
|
<StyledButton variant={'body2'}>Preview diff</StyledButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [input]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: searchedData,
|
||||||
|
getSearchText,
|
||||||
|
getSearchContext,
|
||||||
|
} = useSearch(COLUMNS, searchValue, features || []);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return loading
|
||||||
|
? Array(5).fill({
|
||||||
|
name: 'Feature name',
|
||||||
|
projectId: 'Feature Project',
|
||||||
|
environments: { name: 'Feature Envrironments', variants: [] },
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
: searchedData;
|
||||||
|
}, [searchedData, loading]);
|
||||||
|
|
||||||
|
const [initialState] = useState(() => ({
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: searchParams.get('sort') || value.id,
|
||||||
|
desc: searchParams.has('order')
|
||||||
|
? searchParams.get('order') === 'desc'
|
||||||
|
: value.desc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
state: { sortBy },
|
||||||
|
prepareRow,
|
||||||
|
setHiddenColumns,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
initialState,
|
||||||
|
columns: COLUMNS as any,
|
||||||
|
data: data as any,
|
||||||
|
sortTypes,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetHiddenColumns: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
disableMultiSort: true,
|
||||||
|
defaultColumn: {
|
||||||
|
Cell: HighlightCell,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useGlobalFilter,
|
||||||
|
useFlexLayout,
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
|
useConditionallyHiddenColumns(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: isSmallScreen,
|
||||||
|
columns: ['projectId'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setHiddenColumns,
|
||||||
|
COLUMNS
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tableState = Object.fromEntries(searchParams);
|
||||||
|
tableState.sort = sortBy[0].id;
|
||||||
|
if (sortBy[0].desc) {
|
||||||
|
tableState.order = 'desc';
|
||||||
|
} else if (tableState.order) {
|
||||||
|
delete tableState.order;
|
||||||
|
}
|
||||||
|
if (searchValue) {
|
||||||
|
tableState.search = searchValue;
|
||||||
|
} else {
|
||||||
|
delete tableState.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(tableState, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
setValue({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- don't re-render after search params change
|
||||||
|
}, [loading, sortBy, searchValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1" sx={{ ml: 1 }}>
|
||||||
|
{features !== undefined && !loading
|
||||||
|
? `Results (${
|
||||||
|
rows.length < data.length
|
||||||
|
? `${rows.length} of ${data.length}`
|
||||||
|
: data.length
|
||||||
|
})`
|
||||||
|
: 'Results'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
disabled={loading}
|
||||||
|
containerStyles={{ marginLeft: '1rem', maxWidth: '400px' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!loading && !data}
|
||||||
|
show={() => (
|
||||||
|
<TablePlaceholder>
|
||||||
|
{data === undefined
|
||||||
|
? 'None of the feature toggles were evaluated yet.'
|
||||||
|
: 'No results found.'}
|
||||||
|
</TablePlaceholder>
|
||||||
|
)}
|
||||||
|
elseShow={() => (
|
||||||
|
<Box ref={ref} sx={{ overflow: 'auto' }}>
|
||||||
|
<SearchHighlightProvider
|
||||||
|
value={getSearchText(searchValue)}
|
||||||
|
>
|
||||||
|
<VirtualizedTable
|
||||||
|
rows={rows}
|
||||||
|
headerGroups={headerGroups}
|
||||||
|
prepareRow={prepareRow}
|
||||||
|
/>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
data.length === 0 && searchValue?.length > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No feature toggles found matching “
|
||||||
|
{searchValue}”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
data && data.length === 0 && !searchValue
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No features toggles to display
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,15 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
export const LazyPlayground = lazy(() => import('./Playground'));
|
export const LazyLegacyPlayground = lazy(() => import('./AdvancedPlayground'));
|
||||||
|
export const LazyAdvancedPlayground = lazy(
|
||||||
|
() => import('./AdvancedPlayground')
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LazyPlayground = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
if (uiConfig.flags.advancedPlayground) return <LazyAdvancedPlayground />;
|
||||||
|
|
||||||
|
return <LazyLegacyPlayground />;
|
||||||
|
};
|
||||||
|
@ -21,11 +21,11 @@ import { PlaygroundGuidancePopper } from './PlaygroundGuidancePopper/PlaygroundG
|
|||||||
import Loader from '../../common/Loader/Loader';
|
import Loader from '../../common/Loader/Loader';
|
||||||
|
|
||||||
export const Playground: VFC<{}> = () => {
|
export const Playground: VFC<{}> = () => {
|
||||||
const { environments } = useEnvironments();
|
const { environments: availableEnvironments } = useEnvironments();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matches = useMediaQuery(theme.breakpoints.down('lg'));
|
const matches = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
const [environment, setEnvironment] = useState<string>('');
|
const [environments, setEnvironments] = useState<string[]>([]);
|
||||||
const [projects, setProjects] = 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<
|
||||||
@ -36,7 +36,7 @@ export const Playground: VFC<{}> = () => {
|
|||||||
const { evaluatePlayground, loading } = usePlaygroundApi();
|
const { evaluatePlayground, loading } = usePlaygroundApi();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEnvironment(resolveDefaultEnvironment(environments));
|
setEnvironments([resolveDefaultEnvironment(availableEnvironments)]);
|
||||||
}, [environments]);
|
}, [environments]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -44,7 +44,7 @@ export const Playground: VFC<{}> = () => {
|
|||||||
try {
|
try {
|
||||||
const environmentFromUrl = searchParams.get('environment');
|
const environmentFromUrl = searchParams.get('environment');
|
||||||
if (environmentFromUrl) {
|
if (environmentFromUrl) {
|
||||||
setEnvironment(environmentFromUrl);
|
setEnvironments([environmentFromUrl]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let projectsArray: string[];
|
let projectsArray: string[];
|
||||||
@ -115,7 +115,7 @@ export const Playground: VFC<{}> = () => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
await evaluatePlaygroundContext(
|
await evaluatePlaygroundContext(
|
||||||
environment,
|
environments[0],
|
||||||
projects,
|
projects,
|
||||||
context,
|
context,
|
||||||
setURLParameters
|
setURLParameters
|
||||||
@ -124,7 +124,7 @@ export const Playground: VFC<{}> = () => {
|
|||||||
|
|
||||||
const setURLParameters = () => {
|
const setURLParameters = () => {
|
||||||
searchParams.set('context', encodeURI(context || '')); // always set because of native validation
|
searchParams.set('context', encodeURI(context || '')); // always set because of native validation
|
||||||
searchParams.set('environment', environment);
|
searchParams.set('environment', environments[0]);
|
||||||
if (
|
if (
|
||||||
Array.isArray(projects) &&
|
Array.isArray(projects) &&
|
||||||
projects.length > 0 &&
|
projects.length > 0 &&
|
||||||
@ -182,11 +182,11 @@ export const Playground: VFC<{}> = () => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
context={context}
|
context={context}
|
||||||
setContext={setContext}
|
setContext={setContext}
|
||||||
environments={environments}
|
availableEnvironments={availableEnvironments}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
environment={environment}
|
environments={environments}
|
||||||
setProjects={setProjects}
|
setProjects={setProjects}
|
||||||
setEnvironment={setEnvironment}
|
setEnvironments={setEnvironments}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -18,6 +18,7 @@ import { FeatureStatusCell } from '../PlaygroundResultsTable/FeatureStatusCell/F
|
|||||||
import { FeatureResultInfoPopoverCell } from '../PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell';
|
import { FeatureResultInfoPopoverCell } from '../PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell';
|
||||||
import { VariantCell } from '../PlaygroundResultsTable/VariantCell/VariantCell';
|
import { VariantCell } from '../PlaygroundResultsTable/VariantCell/VariantCell';
|
||||||
import { HighlightCell } from '../../../common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from '../../../common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { capitalizeFirst } from 'utils/capitalizeFirst';
|
||||||
|
|
||||||
interface IPlaygroundEnvironmentTableProps {
|
interface IPlaygroundEnvironmentTableProps {
|
||||||
features: AdvancedPlaygroundEnvironmentFeatureSchema[];
|
features: AdvancedPlaygroundEnvironmentFeatureSchema[];
|
||||||
@ -32,7 +33,7 @@ export const PlaygroundEnvironmentTable = ({
|
|||||||
const dynamicHeaders = Object.keys(features[0].context)
|
const dynamicHeaders = Object.keys(features[0].context)
|
||||||
.filter(contextField => contextField !== 'appName')
|
.filter(contextField => contextField !== 'appName')
|
||||||
.map(contextField => ({
|
.map(contextField => ({
|
||||||
Header: contextField,
|
Header: capitalizeFirst(contextField),
|
||||||
accessor: `context.${contextField}`,
|
accessor: `context.${contextField}`,
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
Cell: HighlightCell,
|
Cell: HighlightCell,
|
||||||
|
@ -10,11 +10,11 @@ import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
|||||||
import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator';
|
import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator';
|
||||||
|
|
||||||
interface IPlaygroundConnectionFieldsetProps {
|
interface IPlaygroundConnectionFieldsetProps {
|
||||||
environment: string;
|
environments: string[];
|
||||||
projects: string[];
|
projects: string[];
|
||||||
setProjects: (projects: string[]) => void;
|
setProjects: (projects: string[]) => void;
|
||||||
setEnvironment: (environment: string) => void;
|
setEnvironments: (environments: string[]) => void;
|
||||||
environmentOptions: string[];
|
availableEnvironments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IOption {
|
interface IOption {
|
||||||
@ -27,11 +27,11 @@ const allOption: IOption = { label: 'ALL', id: '*' };
|
|||||||
export const PlaygroundConnectionFieldset: VFC<
|
export const PlaygroundConnectionFieldset: VFC<
|
||||||
IPlaygroundConnectionFieldsetProps
|
IPlaygroundConnectionFieldsetProps
|
||||||
> = ({
|
> = ({
|
||||||
environment,
|
environments,
|
||||||
projects,
|
projects,
|
||||||
setProjects,
|
setProjects,
|
||||||
setEnvironment,
|
setEnvironments,
|
||||||
environmentOptions,
|
availableEnvironments,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -44,6 +44,13 @@ export const PlaygroundConnectionFieldset: VFC<
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const environmentOptions = [
|
||||||
|
...availableEnvironments.map(name => ({
|
||||||
|
label: name,
|
||||||
|
id: name,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = (
|
const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = (
|
||||||
event,
|
event,
|
||||||
value,
|
value,
|
||||||
@ -71,6 +78,23 @@ export const PlaygroundConnectionFieldset: VFC<
|
|||||||
return setProjects([newProjects.id]);
|
return setProjects([newProjects.id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEnvironmentsChange: ComponentProps<
|
||||||
|
typeof Autocomplete
|
||||||
|
>['onChange'] = (event, value, reason) => {
|
||||||
|
const newEnvironments = value as IOption | IOption[];
|
||||||
|
if (reason === 'clear' || newEnvironments === null) {
|
||||||
|
return setEnvironments([]);
|
||||||
|
}
|
||||||
|
if (Array.isArray(newEnvironments)) {
|
||||||
|
if (newEnvironments.length === 0) {
|
||||||
|
return setEnvironments([]);
|
||||||
|
}
|
||||||
|
return setEnvironments(newEnvironments.map(({ id }) => id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return setEnvironments([newEnvironments.id]);
|
||||||
|
};
|
||||||
|
|
||||||
const isAllProjects =
|
const isAllProjects =
|
||||||
projects.length === 0 || (projects.length === 1 && projects[0] === '*');
|
projects.length === 0 || (projects.length === 1 && projects[0] === '*');
|
||||||
|
|
||||||
@ -89,15 +113,18 @@ export const PlaygroundConnectionFieldset: VFC<
|
|||||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
|
multiple
|
||||||
id="environment"
|
id="environment"
|
||||||
options={environmentOptions}
|
options={environmentOptions}
|
||||||
sx={{ width: 200, maxWidth: '100%' }}
|
sx={{ width: 200, maxWidth: '100%' }}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField {...params} label="Environment" required />
|
<TextField {...params} label="Environments" />
|
||||||
)}
|
)}
|
||||||
value={environment}
|
|
||||||
onChange={(event, value) => setEnvironment(value || '')}
|
|
||||||
size="small"
|
size="small"
|
||||||
|
value={environmentOptions.filter(({ id }) =>
|
||||||
|
environments.includes(id)
|
||||||
|
)}
|
||||||
|
onChange={onEnvironmentsChange}
|
||||||
/>
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
|
@ -2,28 +2,27 @@ import { Box, Button, Divider, useTheme } from '@mui/material';
|
|||||||
import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator';
|
import { GuidanceIndicator } from 'component/common/GuidanceIndicator/GuidanceIndicator';
|
||||||
import { IEnvironment } from 'interfaces/environments';
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
import { FormEvent, VFC } from 'react';
|
import { FormEvent, VFC } from 'react';
|
||||||
import { getEnvironmentOptions } from '../playground.utils';
|
|
||||||
import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset';
|
import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset';
|
||||||
import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/PlaygroundConnectionFieldset';
|
import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/PlaygroundConnectionFieldset';
|
||||||
|
|
||||||
interface IPlaygroundFormProps {
|
interface IPlaygroundFormProps {
|
||||||
environments: IEnvironment[];
|
availableEnvironments: IEnvironment[];
|
||||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
environment: string;
|
environments: string | string[];
|
||||||
projects: string[];
|
projects: string[];
|
||||||
setProjects: React.Dispatch<React.SetStateAction<string[]>>;
|
setProjects: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
setEnvironment: React.Dispatch<React.SetStateAction<string>>;
|
setEnvironments: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
context: string | undefined;
|
context: string | undefined;
|
||||||
setContext: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setContext: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
|
export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
|
||||||
|
availableEnvironments,
|
||||||
environments,
|
environments,
|
||||||
environment,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
projects,
|
projects,
|
||||||
setProjects,
|
setProjects,
|
||||||
setEnvironment,
|
setEnvironments,
|
||||||
context,
|
context,
|
||||||
setContext,
|
setContext,
|
||||||
}) => {
|
}) => {
|
||||||
@ -39,11 +38,15 @@ export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlaygroundConnectionFieldset
|
<PlaygroundConnectionFieldset
|
||||||
environment={environment}
|
environments={
|
||||||
|
Array.isArray(environments) ? environments : [environments]
|
||||||
|
}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
setEnvironment={setEnvironment}
|
setEnvironments={setEnvironments}
|
||||||
setProjects={setProjects}
|
setProjects={setProjects}
|
||||||
environmentOptions={getEnvironmentOptions(environments)}
|
availableEnvironments={availableEnvironments.map(
|
||||||
|
({ name }) => name
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Divider
|
<Divider
|
||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { PlaygroundResponseSchema } from 'openapi';
|
import {
|
||||||
|
PlaygroundResponseSchema,
|
||||||
|
AdvancedPlaygroundResponseSchema,
|
||||||
|
} from 'openapi';
|
||||||
import { IEnvironment } from 'interfaces/environments';
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
|
import { ensureArray } from '@server/util/ensureArray';
|
||||||
|
|
||||||
export const resolveProjects = (
|
export const resolveProjects = (
|
||||||
projects: string[] | string
|
projects: string[] | string
|
||||||
@ -12,11 +16,13 @@ export const resolveProjects = (
|
|||||||
return '*';
|
return '*';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(projects)) {
|
return ensureArray(projects);
|
||||||
return projects;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return [projects];
|
export const resolveEnvironments = (
|
||||||
|
envrironments: string[] | string
|
||||||
|
): string[] => {
|
||||||
|
return ensureArray(envrironments);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resolveDefaultEnvironment = (
|
export const resolveDefaultEnvironment = (
|
||||||
@ -38,7 +44,10 @@ export const getEnvironmentOptions = (environments: IEnvironment[]) => {
|
|||||||
|
|
||||||
export const resolveResultsWidth = (
|
export const resolveResultsWidth = (
|
||||||
matches: boolean,
|
matches: boolean,
|
||||||
results: PlaygroundResponseSchema | undefined
|
results:
|
||||||
|
| PlaygroundResponseSchema
|
||||||
|
| AdvancedPlaygroundResponseSchema
|
||||||
|
| undefined
|
||||||
) => {
|
) => {
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return '100%';
|
return '100%';
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
import {
|
import {
|
||||||
|
AdvancedPlaygroundRequestSchema,
|
||||||
|
AdvancedPlaygroundResponseSchema,
|
||||||
PlaygroundRequestSchema,
|
PlaygroundRequestSchema,
|
||||||
PlaygroundResponseSchema,
|
PlaygroundResponseSchema,
|
||||||
} from '../../../../openapi';
|
} from 'openapi';
|
||||||
|
|
||||||
export const usePlaygroundApi = () => {
|
export const usePlaygroundApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
@ -12,8 +14,7 @@ export const usePlaygroundApi = () => {
|
|||||||
const URI = 'api/admin/playground';
|
const URI = 'api/admin/playground';
|
||||||
|
|
||||||
const evaluatePlayground = async (payload: PlaygroundRequestSchema) => {
|
const evaluatePlayground = async (payload: PlaygroundRequestSchema) => {
|
||||||
const path = URI;
|
const req = createRequest(URI, {
|
||||||
const req = createRequest(path, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
@ -27,8 +28,27 @@ export const usePlaygroundApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const evaluateAdvancedPlayground = async (
|
||||||
|
payload: AdvancedPlaygroundRequestSchema
|
||||||
|
) => {
|
||||||
|
const path = `${URI}/advanced`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res.json() as Promise<AdvancedPlaygroundResponseSchema>;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
evaluatePlayground,
|
evaluatePlayground,
|
||||||
|
evaluateAdvancedPlayground,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
|
3
frontend/src/utils/capitalizeFirst.ts
Normal file
3
frontend/src/utils/capitalizeFirst.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const capitalizeFirst = (string: string) => {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
};
|
3
src/lib/util/ensureArray.ts
Normal file
3
src/lib/util/ensureArray.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function ensureArray<T>(input: T | T[]): T[] {
|
||||||
|
return Array.isArray(input) ? input : [input];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user