1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

Playground fields with Change request (#7724)

using `changeRequest` value from URL
This commit is contained in:
Tymoteusz Czech 2024-08-02 13:01:29 +02:00 committed by GitHub
parent 65131727c1
commit 4b6813aa5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 236 additions and 108 deletions

View File

@ -1,4 +1,4 @@
import { type FormEventHandler, useEffect, useState, type VFC } from 'react';
import { type FormEventHandler, useEffect, useState, type FC } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Box, Paper, useTheme, styled, Alert } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent';
@ -81,7 +81,7 @@ const GenerateWarningMessages: React.FC<{
}
};
export const AdvancedPlayground: VFC<{
export const AdvancedPlayground: FC<{
FormComponent?: typeof PlaygroundForm;
}> = ({ FormComponent = PlaygroundForm }) => {
const defaultSettings: {
@ -112,7 +112,7 @@ export const AdvancedPlayground: VFC<{
>();
const { setToastData } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
const searchParamsLength = Array.from(searchParams.entries()).length;
const [changeRequest, setChangeRequest] = useState<string>();
const { evaluateAdvancedPlayground, loading, errors } = usePlaygroundApi();
const [hasFormBeenSubmitted, setHasFormBeenSubmitted] = useState(false);
@ -123,18 +123,18 @@ export const AdvancedPlayground: VFC<{
}, [JSON.stringify(environments), JSON.stringify(availableEnvironments)]);
useEffect(() => {
if (searchParamsLength > 0) {
loadInitialValuesFromUrl();
}
}, []);
const loadInitialValuesFromUrl = () => {
const loadInitialValuesFromUrl = async () => {
try {
const environments = resolveEnvironmentsFromUrl();
const projects = resolveProjectsFromUrl();
const context = resolveContextFromUrl();
const token = resolveTokenFromUrl();
const makePlaygroundRequest = async () => {
resolveTokenFromUrl();
resolveChangeRequestFromUrl();
// TODO: Add support for changeRequest
if (environments && context) {
await evaluatePlaygroundContext(
environments || [],
@ -142,9 +142,6 @@ export const AdvancedPlayground: VFC<{
context,
);
}
};
makePlaygroundRequest();
} catch (error) {
setToastData({
type: 'error',
@ -191,6 +188,13 @@ export const AdvancedPlayground: VFC<{
return tokenFromUrl;
};
const resolveChangeRequestFromUrl = () => {
const changeRequestFromUrl = searchParams.get('changeRequest');
if (changeRequestFromUrl) {
setChangeRequest(changeRequestFromUrl);
}
};
const evaluatePlaygroundContext = async (
environments: string[] | string,
projects: string[] | string,
@ -243,14 +247,20 @@ export const AdvancedPlayground: VFC<{
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 (
@ -271,6 +281,11 @@ export const AdvancedPlayground: VFC<{
} else {
searchParams.delete('projects');
}
if (changeRequest) {
searchParams.set('changeRequest', changeRequest);
} else {
searchParams.delete('changeRequest');
}
setSearchParams(searchParams);
};
@ -326,6 +341,8 @@ export const AdvancedPlayground: VFC<{
setToken={setToken}
setProjects={setProjects}
setEnvironments={setEnvironments}
changeRequest={changeRequest || undefined}
onClearChangeRequest={onClearChangeRequest}
/>
</Paper>
</Box>

View File

@ -0,0 +1,71 @@
import type { ComponentProps, Dispatch, FC, SetStateAction } from 'react';
import { Autocomplete, TextField } from '@mui/material';
import { renderOption } from '../../renderOption';
interface IEnvironmentsFieldProps {
environments: string[];
setEnvironments: Dispatch<SetStateAction<string[]>>;
availableEnvironments: string[];
disabled?: boolean;
}
interface IOption {
label: string;
id: string;
}
export const EnvironmentsField: FC<IEnvironmentsFieldProps> = ({
environments,
setEnvironments,
availableEnvironments,
disabled,
}) => {
const environmentOptions = [
...availableEnvironments.map((name) => ({
label: name,
id: name,
})),
];
const envValue = environmentOptions.filter(({ id }) =>
environments.includes(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]);
};
return (
<Autocomplete
disablePortal
limitTags={3}
id='environment'
multiple={true}
options={environmentOptions}
sx={{ flex: 1 }}
renderInput={(params) => (
<TextField {...params} label='Environments' />
)}
renderOption={renderOption}
getOptionLabel={({ label }) => label}
disableCloseOnSelect={false}
size='small'
value={envValue}
onChange={onEnvironmentsChange}
disabled={disabled}
data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'}
/>
);
};

View File

@ -3,6 +3,7 @@ import { render } from 'utils/testRenderer';
import { fireEvent, screen, within } from '@testing-library/react';
import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset';
import { useState } from 'react';
import userEvent from '@testing-library/user-event';
const server = testServerSetup();
@ -11,6 +12,9 @@ beforeEach(() => {
versionInfo: {
current: { oss: 'version', enterprise: 'version' },
},
flags: {
changeRequestPlayground: true,
},
});
testServerRoute(
server,
@ -203,3 +207,43 @@ test('should have a working clear button when token is filled', async () => {
expect(tokenInput).toHaveValue('');
});
test('should show change request and disable other fields until removed', async () => {
const Component = () => {
const [environments, setEnvironments] = useState<string[]>([]);
const [projects, setProjects] = useState<string[]>(['test-project']);
const [token, setToken] = useState<string>();
const [changeRequest, setChangeRequest] = useState('CR #1');
const availableEnvironments = ['development', 'production'];
return (
<PlaygroundConnectionFieldset
environments={environments}
projects={projects}
token={token}
setToken={setToken}
setEnvironments={setEnvironments}
setProjects={setProjects}
availableEnvironments={availableEnvironments}
changeRequest={changeRequest}
onClearChangeRequest={() => setChangeRequest('')}
/>
);
};
render(<Component />);
const changeRequestInput = await screen.findByDisplayValue('CR #1');
// expect(changeRequestInput).toHaveValue('CR #1');
const viewButton = await screen.findByText(/View change request/);
expect(viewButton).toHaveProperty(
'href',
'http://localhost:3000/projects/test-project/change-requests/CR%20#1',
);
// TODO: check if other fields are disabled
const clearButton = await screen.findByLabelText(/clear change request/i);
await userEvent.click(clearButton);
expect(changeRequestInput).not.toBeInTheDocument();
});

View File

@ -3,22 +3,20 @@ import {
type Dispatch,
type SetStateAction,
useState,
type VFC,
type FC,
} from 'react';
import {
Autocomplete,
Box,
Button,
IconButton,
InputAdornment,
styled,
TextField,
type TextField,
Tooltip,
Typography,
useTheme,
} from '@mui/material';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { renderOption } from '../renderOption';
import {
type IApiToken,
useApiTokens,
@ -32,6 +30,8 @@ import Clear from '@mui/icons-material/Clear';
import { ProjectSelect } from '../../../../common/ProjectSelect/ProjectSelect';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag';
import { EnvironmentsField } from './EnvironmentsField/EnvironmentsField';
import { Link } from 'react-router-dom';
interface IPlaygroundConnectionFieldsetProps {
environments: string[];
@ -41,6 +41,8 @@ interface IPlaygroundConnectionFieldsetProps {
setEnvironments: Dispatch<SetStateAction<string[]>>;
setToken?: Dispatch<SetStateAction<string | undefined>>;
availableEnvironments: string[];
changeRequest?: string;
onClearChangeRequest?: () => void;
}
interface IOption {
@ -61,7 +63,7 @@ const StyledInput = styled(Input)(() => ({
const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid',
columnGap: theme.spacing(2),
rowGap: theme.spacing(4),
rowGap: theme.spacing(2),
gridTemplateColumns: '1fr',
[theme.breakpoints.up('md')]: {
@ -69,7 +71,16 @@ const StyledGrid = styled(Box)(({ theme }) => ({
},
}));
export const PlaygroundConnectionFieldset: VFC<
const StyledChangeRequestInput = styled(StyledInput)(({ theme }) => ({
'& label': {
WebkitTextFillColor: theme.palette.text.secondary,
},
'& input.Mui-disabled': {
WebkitTextFillColor: theme.palette.text.secondary,
},
}));
export const PlaygroundConnectionFieldset: FC<
IPlaygroundConnectionFieldsetProps
> = ({
environments,
@ -79,6 +90,8 @@ export const PlaygroundConnectionFieldset: VFC<
setEnvironments,
setToken,
availableEnvironments,
changeRequest,
onClearChangeRequest,
}) => {
const theme = useTheme();
const { tokens } = useApiTokens();
@ -86,9 +99,7 @@ export const PlaygroundConnectionFieldset: VFC<
const { projects: availableProjects } = useProjects();
const isChangeRequestPlaygroundEnabled = useUiFlag(
'changeRequestPlayground',
);
const changeRequestPlaygroundEnabled = useUiFlag('changeRequestPlayground');
const projectsOptions = [
allOption,
@ -97,35 +108,6 @@ export const PlaygroundConnectionFieldset: VFC<
id,
})),
];
const environmentOptions = [
...availableEnvironments.map((name) => ({
label: name,
id: name,
})),
];
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 envValue = environmentOptions.filter(({ id }) =>
environments.includes(id),
);
const onSetToken: ComponentProps<typeof TextField>['onChange'] = async (
event,
) => {
@ -208,18 +190,6 @@ export const PlaygroundConnectionFieldset: VFC<
resetTokenState();
};
const renderClearButton = () => (
<InputAdornment position='end' data-testid='TOKEN_INPUT_CLEAR_BTN'>
<IconButton
aria-label='toggle password visibility'
onClick={clearToken}
edge='end'
>
<SmallClear />
</IconButton>
</InputAdornment>
);
return (
<Box sx={{ pb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
@ -241,25 +211,14 @@ export const PlaygroundConnectionFieldset: VFC<
: 'Select environments to use in the playground'
}
>
<Autocomplete
disablePortal
limitTags={3}
id='environment'
multiple={true}
options={environmentOptions}
sx={{ flex: 1 }}
renderInput={(params) => (
<TextField {...params} label='Environments' />
)}
renderOption={renderOption}
getOptionLabel={({ label }) => label}
disableCloseOnSelect={false}
size='small'
value={envValue}
onChange={onEnvironmentsChange}
disabled={Boolean(token)}
data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'}
<Box>
<EnvironmentsField
environments={environments}
setEnvironments={setEnvironments}
availableEnvironments={availableEnvironments}
disabled={Boolean(token || changeRequest)}
/>
</Box>
</Tooltip>
</Box>
<Box>
@ -275,7 +234,7 @@ export const PlaygroundConnectionFieldset: VFC<
selectedProjects={projects}
onChange={setProjects}
dataTestId={'PLAYGROUND_PROJECT_SELECT'}
disabled={Boolean(token)}
disabled={Boolean(token || changeRequest)}
limitTags={3}
/>
</Tooltip>
@ -283,7 +242,7 @@ export const PlaygroundConnectionFieldset: VFC<
<Box>
<StyledInput
label='API token'
value={token || ''}
value={token || changeRequest ? ' ' : ''}
onChange={onSetToken}
type={'text'}
error={Boolean(tokenError)}
@ -291,36 +250,67 @@ export const PlaygroundConnectionFieldset: VFC<
placeholder={'Enter your API token'}
data-testid={'PLAYGROUND_TOKEN_INPUT'}
InputProps={{
endAdornment: token ? renderClearButton() : null,
endAdornment: token ? (
<InputAdornment
position='end'
data-testid='TOKEN_INPUT_CLEAR_BTN'
>
<IconButton
aria-label='clear API token'
onClick={clearToken}
edge='end'
>
<SmallClear />
</IconButton>
</InputAdornment>
) : null,
}}
disabled={Boolean(changeRequest)}
/>
</Box>
<ConditionallyRender
condition={Boolean(isChangeRequestPlaygroundEnabled)}
condition={Boolean(
changeRequestPlaygroundEnabled && changeRequest,
)}
show={
<Box sx={{ display: 'flex', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<StyledInput
<StyledChangeRequestInput
label='Change request'
value={
'// TODO: Change request #5 (feature1, feature2)'
}
value={changeRequest || ''}
onChange={() => {}}
type={'text'}
// error={Boolean(tokenError)}
// errorText={tokenError}
// error={Boolean(changeRequestError)}
// errorText={changeRequestError)}}
placeholder={'Enter your API token'}
data-testid={'PLAYGROUND_TOKEN_INPUT'}
// disabled
disabled
InputProps={{
endAdornment: renderClearButton(),
sx: {
cursor: 'default',
},
endAdornment: (
<InputAdornment
position='end'
data-testid='CR_INPUT_CLEAR_BTN'
>
<IconButton
aria-label='clear Change request results'
onClick={
onClearChangeRequest
}
edge='end'
>
<SmallClear />
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
<Button variant='outlined' size='small'>
<Button
variant='outlined'
size='small'
to={`/projects/${projects[0]}/change-requests/${changeRequest}`}
component={Link}
>
View change request
</Button>
</Box>

View File

@ -15,6 +15,8 @@ interface IPlaygroundFormProps {
setEnvironments: React.Dispatch<React.SetStateAction<string[]>>;
context: string | undefined;
setContext: React.Dispatch<React.SetStateAction<string | undefined>>;
changeRequest?: string;
onClearChangeRequest?: () => void;
}
export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
@ -28,6 +30,8 @@ export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
setEnvironments,
context,
setContext,
changeRequest,
onClearChangeRequest,
}) => {
return (
<Box
@ -50,6 +54,8 @@ export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
availableEnvironments={availableEnvironments.map(
({ name }) => name,
)}
changeRequest={changeRequest}
onClearChangeRequest={onClearChangeRequest}
/>
<PlaygroundCodeFieldset context={context} setContext={setContext} />