mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
As of PR #8935, we no longer support both text and title, and confetti has been removed. This PR: - removes `confetti` from the toast interface - merges `text` and `title` into `text` and updates its uses across the codebase. - readjusts the text where necessary.
426 lines
16 KiB
TypeScript
426 lines
16 KiB
TypeScript
import { type FC, type FormEventHandler, useEffect, useState } from 'react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { Alert, Box, Paper, styled, 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 'component/common/Loader/Loader';
|
|
import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable';
|
|
import type { AdvancedPlaygroundResponseSchema } from 'openapi';
|
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
|
import { BadRequestError } from 'utils/apiUtils';
|
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
|
|
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
|
marginBottom: theme.spacing(3),
|
|
}));
|
|
|
|
const GenerateWarningMessages: React.FC<{
|
|
response?: AdvancedPlaygroundResponseSchema;
|
|
}> = ({ response }) => {
|
|
const invalidContextProperties =
|
|
response?.warnings?.invalidContextProperties;
|
|
|
|
if (invalidContextProperties && invalidContextProperties.length > 0) {
|
|
invalidContextProperties.sort();
|
|
const summary =
|
|
'Some context properties were not taken into account during evaluation';
|
|
|
|
const StyledDetails = styled('details')(({ theme }) => ({
|
|
'* + *': { marginBlockStart: theme.spacing(1) },
|
|
}));
|
|
|
|
return (
|
|
<StyledAlert severity='warning'>
|
|
<StyledDetails>
|
|
<summary>{summary}</summary>
|
|
<p>
|
|
The context you provided for this query contained
|
|
top-level properties with invalid values. These
|
|
properties were not taken into consideration when
|
|
evaluating your query. The properties are:
|
|
</p>
|
|
<ul>
|
|
{invalidContextProperties.map((prop) => (
|
|
<li
|
|
key={prop}
|
|
data-testid='context-warning-list-element'
|
|
>
|
|
<code>{prop}</code>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<p>
|
|
Remember that context fields (with the exception of the{' '}
|
|
<code>properties</code> object) must be strings.
|
|
</p>
|
|
<p>
|
|
Because we didn't take these properties into account
|
|
during the feature flag evaluation, they will not appear
|
|
in the results table.
|
|
</p>
|
|
</StyledDetails>
|
|
</StyledAlert>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const AdvancedPlayground: FC<{
|
|
FormComponent?: typeof PlaygroundForm;
|
|
}> = ({ FormComponent = PlaygroundForm }) => {
|
|
const defaultSettings: {
|
|
projects: string[];
|
|
environments: string[];
|
|
context?: string;
|
|
token?: string;
|
|
} = { projects: [], environments: [] };
|
|
const { value, setValue } = createLocalStorage(
|
|
'AdvancedPlayground:v1',
|
|
defaultSettings,
|
|
);
|
|
const { trackEvent } = usePlausibleTracker();
|
|
|
|
const { environments: availableEnvironments } = useEnvironments();
|
|
const theme = useTheme();
|
|
const matches = true;
|
|
|
|
const [configurationError, setConfigurationError] = useState<string>();
|
|
const [environments, setEnvironments] = useState<string[]>(
|
|
value.environments,
|
|
);
|
|
const [projects, setProjects] = useState<string[]>(value.projects);
|
|
const [token, setToken] = useState<string | undefined>(value.token);
|
|
const [context, setContext] = useState<string | undefined>(value.context);
|
|
const [results, setResults] = useState<
|
|
AdvancedPlaygroundResponseSchema | undefined
|
|
>();
|
|
const { setToastData } = useToast();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [changeRequest, setChangeRequest] = useState<string>();
|
|
const {
|
|
evaluateAdvancedPlayground,
|
|
evaluateChangeRequestPlayground,
|
|
loading,
|
|
errors,
|
|
} = usePlaygroundApi();
|
|
const [hasFormBeenSubmitted, setHasFormBeenSubmitted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (environments?.length === 0 && availableEnvironments.length > 0) {
|
|
setEnvironments([resolveDefaultEnvironment(availableEnvironments)]);
|
|
}
|
|
}, [JSON.stringify(environments), JSON.stringify(availableEnvironments)]);
|
|
|
|
useEffect(() => {
|
|
loadInitialValuesFromUrl();
|
|
}, []);
|
|
|
|
const loadInitialValuesFromUrl = async () => {
|
|
try {
|
|
const environments = resolveEnvironmentsFromUrl();
|
|
const projects = resolveProjectsFromUrl();
|
|
const context = resolveContextFromUrl();
|
|
resolveTokenFromUrl();
|
|
resolveChangeRequestFromUrl();
|
|
// TODO: Add support for changeRequest
|
|
|
|
if (environments && context) {
|
|
await evaluatePlaygroundContext(
|
|
environments || [],
|
|
projects || '*',
|
|
context,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
setToastData({
|
|
type: 'error',
|
|
text: `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;
|
|
const 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 resolveTokenFromUrl = () => {
|
|
let tokenFromUrl = searchParams.get('token');
|
|
if (tokenFromUrl) {
|
|
tokenFromUrl = decodeURI(tokenFromUrl);
|
|
setToken(tokenFromUrl);
|
|
}
|
|
return tokenFromUrl;
|
|
};
|
|
|
|
const resolveChangeRequestFromUrl = () => {
|
|
const changeRequestFromUrl = searchParams.get('changeRequest');
|
|
if (changeRequestFromUrl) {
|
|
setChangeRequest(changeRequestFromUrl);
|
|
}
|
|
};
|
|
|
|
const evaluatePlaygroundContext = async (
|
|
environments: string[] | string,
|
|
projects: string[] | string,
|
|
context: string | undefined,
|
|
action?: () => void,
|
|
) => {
|
|
try {
|
|
setConfigurationError(undefined);
|
|
const parsedContext = {
|
|
appName: 'playground',
|
|
...JSON.parse(context || '{}'),
|
|
};
|
|
|
|
const response = changeRequest
|
|
? await evaluateChangeRequestPlayground(changeRequest, {
|
|
context: parsedContext,
|
|
})
|
|
: await evaluateAdvancedPlayground({
|
|
environments: resolveEnvironments(environments),
|
|
projects: resolveProjects(projects),
|
|
context: parsedContext,
|
|
});
|
|
|
|
if (action && typeof action === 'function') {
|
|
action();
|
|
}
|
|
setResults(response);
|
|
} catch (error: unknown) {
|
|
if (error instanceof BadRequestError) {
|
|
setConfigurationError(error.message);
|
|
} else if (error instanceof SyntaxError) {
|
|
setToastData({
|
|
type: 'error',
|
|
text: `Error parsing context: ${formatUnknownError(error)}`,
|
|
});
|
|
} else {
|
|
setToastData({
|
|
type: 'error',
|
|
text: formatUnknownError(error),
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const trackTryConfiguration = () => {
|
|
let mode: 'default' | 'api_token' | 'change_request' = 'default';
|
|
if (token && token !== '') {
|
|
mode = 'api_token';
|
|
} else if (changeRequest) {
|
|
mode = 'change_request';
|
|
}
|
|
trackEvent('playground', {
|
|
props: {
|
|
eventType: 'try-configuration',
|
|
mode,
|
|
},
|
|
});
|
|
};
|
|
|
|
const onSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
|
|
event.preventDefault();
|
|
|
|
setHasFormBeenSubmitted(true);
|
|
|
|
trackTryConfiguration();
|
|
|
|
await evaluatePlaygroundContext(environments, projects, context, () => {
|
|
setURLParameters();
|
|
if (!changeRequest) {
|
|
setValue({
|
|
environments,
|
|
projects,
|
|
context,
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const onClearChangeRequest = () => {
|
|
setChangeRequest(undefined);
|
|
};
|
|
|
|
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');
|
|
}
|
|
if (changeRequest) {
|
|
searchParams.set('changeRequest', changeRequest);
|
|
} else {
|
|
searchParams.delete('changeRequest');
|
|
}
|
|
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,
|
|
isolation: 'isolate',
|
|
zIndex: 2,
|
|
}}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
px: 4,
|
|
py: 3,
|
|
background: theme.palette.background.elevation2,
|
|
transition: 'width 0.4s ease',
|
|
minWidth: matches ? 'auto' : '500px',
|
|
width: formWidth,
|
|
position: 'sticky',
|
|
top: 0,
|
|
}}
|
|
>
|
|
<FormComponent
|
|
onSubmit={onSubmit}
|
|
context={context}
|
|
setContext={setContext}
|
|
availableEnvironments={availableEnvironments}
|
|
projects={projects}
|
|
environments={environments}
|
|
token={token}
|
|
setToken={setToken}
|
|
setProjects={setProjects}
|
|
setEnvironments={setEnvironments}
|
|
changeRequest={changeRequest || undefined}
|
|
onClearChangeRequest={onClearChangeRequest}
|
|
/>
|
|
</Paper>
|
|
</Box>
|
|
<Box
|
|
sx={(theme) => ({
|
|
width: resultsWidth,
|
|
transition: 'width 0.4s ease',
|
|
padding: theme.spacing(4, 4),
|
|
isolation: 'isolate',
|
|
zIndex: 1,
|
|
})}
|
|
>
|
|
<ConditionallyRender
|
|
condition={Boolean(configurationError)}
|
|
show={
|
|
<StyledAlert severity='warning'>
|
|
{configurationError}
|
|
</StyledAlert>
|
|
}
|
|
/>
|
|
<ConditionallyRender
|
|
condition={loading}
|
|
show={<Loader />}
|
|
elseShow={
|
|
<>
|
|
<ConditionallyRender
|
|
condition={
|
|
Boolean(results) &&
|
|
Object.values(errors).length === 0
|
|
}
|
|
show={
|
|
<>
|
|
<GenerateWarningMessages
|
|
response={results}
|
|
/>
|
|
<AdvancedPlaygroundResultsTable
|
|
loading={loading}
|
|
features={results?.features}
|
|
input={results?.input}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
<ConditionallyRender
|
|
condition={
|
|
!results && !hasFormBeenSubmitted
|
|
}
|
|
show={<PlaygroundGuidance />}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</PageContent>
|
|
);
|
|
};
|
|
|
|
export default AdvancedPlayground;
|