diff --git a/frontend/src/component/playground/Playground/AdvancedPlayground.tsx b/frontend/src/component/playground/Playground/AdvancedPlayground.tsx index 31702dcc9a..b46231015e 100644 --- a/frontend/src/component/playground/Playground/AdvancedPlayground.tsx +++ b/frontend/src/component/playground/Playground/AdvancedPlayground.tsx @@ -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(); const { evaluateAdvancedPlayground, loading, errors } = usePlaygroundApi(); const [hasFormBeenSubmitted, setHasFormBeenSubmitted] = useState(false); @@ -123,28 +123,25 @@ export const AdvancedPlayground: VFC<{ }, [JSON.stringify(environments), JSON.stringify(availableEnvironments)]); useEffect(() => { - if (searchParamsLength > 0) { - loadInitialValuesFromUrl(); - } + loadInitialValuesFromUrl(); }, []); - const loadInitialValuesFromUrl = () => { + const loadInitialValuesFromUrl = async () => { try { const environments = resolveEnvironmentsFromUrl(); const projects = resolveProjectsFromUrl(); const context = resolveContextFromUrl(); - const token = resolveTokenFromUrl(); - const makePlaygroundRequest = async () => { - if (environments && context) { - await evaluatePlaygroundContext( - environments || [], - projects || '*', - context, - ); - } - }; + resolveTokenFromUrl(); + resolveChangeRequestFromUrl(); + // TODO: Add support for changeRequest - makePlaygroundRequest(); + if (environments && context) { + await evaluatePlaygroundContext( + environments || [], + projects || '*', + context, + ); + } } 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(); - setValue({ - environments, - projects, - context, - }); + 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} /> diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/EnvironmentsField/EnvironmentsField.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/EnvironmentsField/EnvironmentsField.tsx new file mode 100644 index 0000000000..a2f6d13287 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/EnvironmentsField/EnvironmentsField.tsx @@ -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>; + availableEnvironments: string[]; + disabled?: boolean; +} + +interface IOption { + label: string; + id: string; +} + +export const EnvironmentsField: FC = ({ + 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 ( + ( + + )} + renderOption={renderOption} + getOptionLabel={({ label }) => label} + disableCloseOnSelect={false} + size='small' + value={envValue} + onChange={onEnvironmentsChange} + disabled={disabled} + data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'} + /> + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.test.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.test.tsx index 196ad3f26c..b673edecca 100644 --- a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.test.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.test.tsx @@ -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([]); + const [projects, setProjects] = useState(['test-project']); + const [token, setToken] = useState(); + const [changeRequest, setChangeRequest] = useState('CR #1'); + + const availableEnvironments = ['development', 'production']; + + return ( + setChangeRequest('')} + /> + ); + }; + render(); + + 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(); +}); diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx index 58205a355e..6520d587c4 100644 --- a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundConnectionFieldset/PlaygroundConnectionFieldset.tsx @@ -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>; setToken?: Dispatch>; 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['onChange'] = async ( event, ) => { @@ -208,18 +190,6 @@ export const PlaygroundConnectionFieldset: VFC< resetTokenState(); }; - const renderClearButton = () => ( - - - - - - ); - return ( @@ -241,25 +211,14 @@ export const PlaygroundConnectionFieldset: VFC< : 'Select environments to use in the playground' } > - ( - - )} - renderOption={renderOption} - getOptionLabel={({ label }) => label} - disableCloseOnSelect={false} - size='small' - value={envValue} - onChange={onEnvironmentsChange} - disabled={Boolean(token)} - data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'} - /> + + + @@ -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} /> @@ -283,7 +242,7 @@ export const PlaygroundConnectionFieldset: VFC< + + + + + ) : null, }} + disabled={Boolean(changeRequest)} /> - {}} 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: ( + + + + + + ), }} /> - diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx index c3eea50fff..d8b4228f70 100644 --- a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundForm.tsx @@ -15,6 +15,8 @@ interface IPlaygroundFormProps { setEnvironments: React.Dispatch>; context: string | undefined; setContext: React.Dispatch>; + changeRequest?: string; + onClearChangeRequest?: () => void; } export const PlaygroundForm: VFC = ({ @@ -28,6 +30,8 @@ export const PlaygroundForm: VFC = ({ setEnvironments, context, setContext, + changeRequest, + onClearChangeRequest, }) => { return ( = ({ availableEnvironments={availableEnvironments.map( ({ name }) => name, )} + changeRequest={changeRequest} + onClearChangeRequest={onClearChangeRequest} />