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 { useSearchParams } from 'react-router-dom';
import { Box, Paper, useTheme, styled, Alert } from '@mui/material'; import { Box, Paper, useTheme, styled, Alert } from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent'; 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?: typeof PlaygroundForm;
}> = ({ FormComponent = PlaygroundForm }) => { }> = ({ FormComponent = PlaygroundForm }) => {
const defaultSettings: { const defaultSettings: {
@ -112,7 +112,7 @@ export const AdvancedPlayground: VFC<{
>(); >();
const { setToastData } = useToast(); const { setToastData } = useToast();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const searchParamsLength = Array.from(searchParams.entries()).length; const [changeRequest, setChangeRequest] = useState<string>();
const { evaluateAdvancedPlayground, loading, errors } = usePlaygroundApi(); const { evaluateAdvancedPlayground, loading, errors } = usePlaygroundApi();
const [hasFormBeenSubmitted, setHasFormBeenSubmitted] = useState(false); const [hasFormBeenSubmitted, setHasFormBeenSubmitted] = useState(false);
@ -123,18 +123,18 @@ export const AdvancedPlayground: VFC<{
}, [JSON.stringify(environments), JSON.stringify(availableEnvironments)]); }, [JSON.stringify(environments), JSON.stringify(availableEnvironments)]);
useEffect(() => { useEffect(() => {
if (searchParamsLength > 0) {
loadInitialValuesFromUrl(); loadInitialValuesFromUrl();
}
}, []); }, []);
const loadInitialValuesFromUrl = () => { const loadInitialValuesFromUrl = async () => {
try { try {
const environments = resolveEnvironmentsFromUrl(); const environments = resolveEnvironmentsFromUrl();
const projects = resolveProjectsFromUrl(); const projects = resolveProjectsFromUrl();
const context = resolveContextFromUrl(); const context = resolveContextFromUrl();
const token = resolveTokenFromUrl(); resolveTokenFromUrl();
const makePlaygroundRequest = async () => { resolveChangeRequestFromUrl();
// TODO: Add support for changeRequest
if (environments && context) { if (environments && context) {
await evaluatePlaygroundContext( await evaluatePlaygroundContext(
environments || [], environments || [],
@ -142,9 +142,6 @@ export const AdvancedPlayground: VFC<{
context, context,
); );
} }
};
makePlaygroundRequest();
} catch (error) { } catch (error) {
setToastData({ setToastData({
type: 'error', type: 'error',
@ -191,6 +188,13 @@ export const AdvancedPlayground: VFC<{
return tokenFromUrl; return tokenFromUrl;
}; };
const resolveChangeRequestFromUrl = () => {
const changeRequestFromUrl = searchParams.get('changeRequest');
if (changeRequestFromUrl) {
setChangeRequest(changeRequestFromUrl);
}
};
const evaluatePlaygroundContext = async ( const evaluatePlaygroundContext = async (
environments: string[] | string, environments: string[] | string,
projects: string[] | string, projects: string[] | string,
@ -243,14 +247,20 @@ export const AdvancedPlayground: VFC<{
await evaluatePlaygroundContext(environments, projects, context, () => { await evaluatePlaygroundContext(environments, projects, context, () => {
setURLParameters(); setURLParameters();
if (!changeRequest) {
setValue({ setValue({
environments, environments,
projects, projects,
context, context,
}); });
}
}); });
}; };
const onClearChangeRequest = () => {
setChangeRequest(undefined);
};
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
if ( if (
@ -271,6 +281,11 @@ export const AdvancedPlayground: VFC<{
} else { } else {
searchParams.delete('projects'); searchParams.delete('projects');
} }
if (changeRequest) {
searchParams.set('changeRequest', changeRequest);
} else {
searchParams.delete('changeRequest');
}
setSearchParams(searchParams); setSearchParams(searchParams);
}; };
@ -326,6 +341,8 @@ export const AdvancedPlayground: VFC<{
setToken={setToken} setToken={setToken}
setProjects={setProjects} setProjects={setProjects}
setEnvironments={setEnvironments} setEnvironments={setEnvironments}
changeRequest={changeRequest || undefined}
onClearChangeRequest={onClearChangeRequest}
/> />
</Paper> </Paper>
</Box> </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 { fireEvent, screen, within } from '@testing-library/react';
import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset'; import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset';
import { useState } from 'react'; import { useState } from 'react';
import userEvent from '@testing-library/user-event';
const server = testServerSetup(); const server = testServerSetup();
@ -11,6 +12,9 @@ beforeEach(() => {
versionInfo: { versionInfo: {
current: { oss: 'version', enterprise: 'version' }, current: { oss: 'version', enterprise: 'version' },
}, },
flags: {
changeRequestPlayground: true,
},
}); });
testServerRoute( testServerRoute(
server, server,
@ -203,3 +207,43 @@ test('should have a working clear button when token is filled', async () => {
expect(tokenInput).toHaveValue(''); 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 Dispatch,
type SetStateAction, type SetStateAction,
useState, useState,
type VFC, type FC,
} from 'react'; } from 'react';
import { import {
Autocomplete,
Box, Box,
Button, Button,
IconButton, IconButton,
InputAdornment, InputAdornment,
styled, styled,
TextField, type TextField,
Tooltip, Tooltip,
Typography, Typography,
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import useProjects from 'hooks/api/getters/useProjects/useProjects'; import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { renderOption } from '../renderOption';
import { import {
type IApiToken, type IApiToken,
useApiTokens, useApiTokens,
@ -32,6 +30,8 @@ import Clear from '@mui/icons-material/Clear';
import { ProjectSelect } from '../../../../common/ProjectSelect/ProjectSelect'; import { ProjectSelect } from '../../../../common/ProjectSelect/ProjectSelect';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { EnvironmentsField } from './EnvironmentsField/EnvironmentsField';
import { Link } from 'react-router-dom';
interface IPlaygroundConnectionFieldsetProps { interface IPlaygroundConnectionFieldsetProps {
environments: string[]; environments: string[];
@ -41,6 +41,8 @@ interface IPlaygroundConnectionFieldsetProps {
setEnvironments: Dispatch<SetStateAction<string[]>>; setEnvironments: Dispatch<SetStateAction<string[]>>;
setToken?: Dispatch<SetStateAction<string | undefined>>; setToken?: Dispatch<SetStateAction<string | undefined>>;
availableEnvironments: string[]; availableEnvironments: string[];
changeRequest?: string;
onClearChangeRequest?: () => void;
} }
interface IOption { interface IOption {
@ -61,7 +63,7 @@ const StyledInput = styled(Input)(() => ({
const StyledGrid = styled(Box)(({ theme }) => ({ const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid', display: 'grid',
columnGap: theme.spacing(2), columnGap: theme.spacing(2),
rowGap: theme.spacing(4), rowGap: theme.spacing(2),
gridTemplateColumns: '1fr', gridTemplateColumns: '1fr',
[theme.breakpoints.up('md')]: { [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 IPlaygroundConnectionFieldsetProps
> = ({ > = ({
environments, environments,
@ -79,6 +90,8 @@ export const PlaygroundConnectionFieldset: VFC<
setEnvironments, setEnvironments,
setToken, setToken,
availableEnvironments, availableEnvironments,
changeRequest,
onClearChangeRequest,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { tokens } = useApiTokens(); const { tokens } = useApiTokens();
@ -86,9 +99,7 @@ export const PlaygroundConnectionFieldset: VFC<
const { projects: availableProjects } = useProjects(); const { projects: availableProjects } = useProjects();
const isChangeRequestPlaygroundEnabled = useUiFlag( const changeRequestPlaygroundEnabled = useUiFlag('changeRequestPlayground');
'changeRequestPlayground',
);
const projectsOptions = [ const projectsOptions = [
allOption, allOption,
@ -97,35 +108,6 @@ export const PlaygroundConnectionFieldset: VFC<
id, 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 ( const onSetToken: ComponentProps<typeof TextField>['onChange'] = async (
event, event,
) => { ) => {
@ -208,18 +190,6 @@ export const PlaygroundConnectionFieldset: VFC<
resetTokenState(); 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 ( return (
<Box sx={{ pb: 2 }}> <Box sx={{ pb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
@ -241,25 +211,14 @@ export const PlaygroundConnectionFieldset: VFC<
: 'Select environments to use in the playground' : 'Select environments to use in the playground'
} }
> >
<Autocomplete <Box>
disablePortal <EnvironmentsField
limitTags={3} environments={environments}
id='environment' setEnvironments={setEnvironments}
multiple={true} availableEnvironments={availableEnvironments}
options={environmentOptions} disabled={Boolean(token || changeRequest)}
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>
</Tooltip> </Tooltip>
</Box> </Box>
<Box> <Box>
@ -275,7 +234,7 @@ export const PlaygroundConnectionFieldset: VFC<
selectedProjects={projects} selectedProjects={projects}
onChange={setProjects} onChange={setProjects}
dataTestId={'PLAYGROUND_PROJECT_SELECT'} dataTestId={'PLAYGROUND_PROJECT_SELECT'}
disabled={Boolean(token)} disabled={Boolean(token || changeRequest)}
limitTags={3} limitTags={3}
/> />
</Tooltip> </Tooltip>
@ -283,7 +242,7 @@ export const PlaygroundConnectionFieldset: VFC<
<Box> <Box>
<StyledInput <StyledInput
label='API token' label='API token'
value={token || ''} value={token || changeRequest ? ' ' : ''}
onChange={onSetToken} onChange={onSetToken}
type={'text'} type={'text'}
error={Boolean(tokenError)} error={Boolean(tokenError)}
@ -291,36 +250,67 @@ export const PlaygroundConnectionFieldset: VFC<
placeholder={'Enter your API token'} placeholder={'Enter your API token'}
data-testid={'PLAYGROUND_TOKEN_INPUT'} data-testid={'PLAYGROUND_TOKEN_INPUT'}
InputProps={{ 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> </Box>
<ConditionallyRender <ConditionallyRender
condition={Boolean(isChangeRequestPlaygroundEnabled)} condition={Boolean(
changeRequestPlaygroundEnabled && changeRequest,
)}
show={ show={
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<StyledInput <StyledChangeRequestInput
label='Change request' label='Change request'
value={ value={changeRequest || ''}
'// TODO: Change request #5 (feature1, feature2)'
}
onChange={() => {}} onChange={() => {}}
type={'text'} type={'text'}
// error={Boolean(tokenError)} // error={Boolean(changeRequestError)}
// errorText={tokenError} // errorText={changeRequestError)}}
placeholder={'Enter your API token'} placeholder={'Enter your API token'}
data-testid={'PLAYGROUND_TOKEN_INPUT'} data-testid={'PLAYGROUND_TOKEN_INPUT'}
// disabled disabled
InputProps={{ InputProps={{
endAdornment: renderClearButton(), endAdornment: (
sx: { <InputAdornment
cursor: 'default', position='end'
}, data-testid='CR_INPUT_CLEAR_BTN'
>
<IconButton
aria-label='clear Change request results'
onClick={
onClearChangeRequest
}
edge='end'
>
<SmallClear />
</IconButton>
</InputAdornment>
),
}} }}
/> />
</Box> </Box>
<Button variant='outlined' size='small'> <Button
variant='outlined'
size='small'
to={`/projects/${projects[0]}/change-requests/${changeRequest}`}
component={Link}
>
View change request View change request
</Button> </Button>
</Box> </Box>

View File

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