Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add input for api token in playground #5130

Merged
merged 13 commits into from
Oct 25, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import {
} from './playground.utils';
import { PlaygroundGuidance } from './PlaygroundGuidance/PlaygroundGuidance';
import { PlaygroundGuidancePopper } from './PlaygroundGuidancePopper/PlaygroundGuidancePopper';
import Loader from '../../common/Loader/Loader';
import Loader from 'component/common/Loader/Loader';
import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable';
import { AdvancedPlaygroundResponseSchema } from 'openapi';
import { createLocalStorage } from 'utils/createLocalStorage';
import { BadRequestError } from '../../../utils/apiUtils';
import { BadRequestError } from 'utils/apiUtils';

const StyledAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(3),
Expand All @@ -34,6 +34,7 @@ export const AdvancedPlayground: VFC<{
projects: string[];
environments: string[];
context?: string;
token?: string;
} = { projects: [], environments: [] };
const { value, setValue } = createLocalStorage(
'AdvancedPlayground:v1',
Expand All @@ -49,6 +50,7 @@ export const AdvancedPlayground: VFC<{
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
Expand Down Expand Up @@ -76,6 +78,7 @@ export const AdvancedPlayground: VFC<{
const environments = resolveEnvironmentsFromUrl();
const projects = resolveProjectsFromUrl();
const context = resolveContextFromUrl();
const token = resolveTokenFromUrl();
const makePlaygroundRequest = async () => {
if (environments && context) {
await evaluatePlaygroundContext(
Expand Down Expand Up @@ -124,6 +127,15 @@ export const AdvancedPlayground: VFC<{
return contextFromUrl;
};

const resolveTokenFromUrl = () => {
let tokenFromUrl = searchParams.get('token');
if (tokenFromUrl) {
tokenFromUrl = decodeURI(tokenFromUrl);
setToken(tokenFromUrl);
}
return tokenFromUrl;
};

const evaluatePlaygroundContext = async (
environments: string[] | string,
projects: string[] | string,
Expand Down Expand Up @@ -249,6 +261,8 @@ export const AdvancedPlayground: VFC<{
availableEnvironments={availableEnvironments}
projects={projects}
environments={environments}
token={token}
setToken={setToken}
setProjects={setProjects}
setEnvironments={setEnvironments}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ComponentProps, VFC } from 'react';
import React, { ComponentProps, useState, VFC } from 'react';
import {
Autocomplete,
Box,
Expand All @@ -8,12 +8,21 @@ import {
} from '@mui/material';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { renderOption } from '../renderOption';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag';
import {
IApiToken,
useApiTokens,
} from 'hooks/api/getters/useApiTokens/useApiTokens';
import Input from 'component/common/Input/Input';

interface IPlaygroundConnectionFieldsetProps {
environments: string[];
projects: string[];
token?: string;
setProjects: (projects: string[]) => void;
setEnvironments: (environments: string[]) => void;
setToken?: (token: string) => void;
availableEnvironments: string[];
}

Expand All @@ -29,11 +38,16 @@ export const PlaygroundConnectionFieldset: VFC<
> = ({
environments,
projects,
token,
setProjects,
setEnvironments,
setToken,
availableEnvironments,
}) => {
const theme = useTheme();
const playgroundImprovements = useUiFlag('playgroundImprovements');
const { tokens } = useApiTokens();
const [tokenError, setTokenError] = useState<string | undefined>();

const { projects: availableProjects = [] } = useProjects();
const projectsOptions = [
Expand Down Expand Up @@ -102,6 +116,44 @@ export const PlaygroundConnectionFieldset: VFC<
environments.includes(id),
);

const onSetToken: ComponentProps<typeof TextField>['onChange'] = async (
event,
Copy link
Contributor

@kwasniew kwasniew Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to break the algorithm into smaller focused functions so that a reader can figure out the high level algorithm w/o reading all the details

E.g.

const onSetToken: ComponentProps<typeof TextField>['onChange'] = async (event) => {
    const tempToken = event.target.value;
    setToken?.(tempToken);

    if (tempToken === '') {
        resetTokenState();
        return;
    }

    try {
        validateTokenFormat(tempToken);
        const [tokenProject, tokenEnvironment] = extractProjectEnvironmentFromToken(tempToken);

        handleTokenProcessing(tokenProject, tempToken);
    } catch (e: any) {
        setTokenError(e.message);
    }
};

const resetTokenState = () => {
    setTokenError(undefined);
}

const handleTokenProcessing = (tokenProject: string, tempToken: string) => {
    setTokenError(undefined);
    setEnvironments([tokenEnvironment]);

    if (tokenProject === '[]') {
        processTokenWithEmptyProject(tempToken);
        return;
    }
    
    if (tokenProject === '*') {
        setProjects([allOption.id]);
        return;
    }

    validateTokenProject(tokenProject);
}

const processTokenWithEmptyProject = (tempToken: string) => {
    const validToken = tokens.find(({ secret }) => secret === tempToken);
    
    if (validToken) {
        if (!validToken.projects) {
            setProjects([allOption.id]);
        } else if (typeof validToken.projects === 'string') {
            setProjects([validToken.projects]);
        } else if (Array.isArray(validToken.projects)) {
            setProjects(validToken.projects);
        }
    } else {
        setTokenError('Invalid token. Please make sure you are using a valid token from this Unleash instance');
    }
}

const validateTokenProject = (tokenProject: string) => {
    if (!projectsOptions.map((option) => option.id).includes(tokenProject)) {
        setTokenError(`Invalid token. Project ${tokenProject} does not exist`);
    } else {
        setProjects([tokenProject]);
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing. thanks

) => {
const tempToken = event.target.value;
setToken?.(tempToken);
if (tempToken === '') {
setTokenError(undefined);
return;
}

const validToken = tokens.find(
(token: IApiToken) => token.secret === tempToken,
);
if (validToken) {
if (typeof validToken.projects === 'undefined') {
setProjects([allOption.id]);
}

if (typeof validToken.projects === 'string') {
setProjects([validToken.projects]);
}

if (Array.isArray(validToken.projects)) {
setProjects(validToken.projects);
}

if (validToken.environment === '*') {
setEnvironments(availableEnvironments);
} else {
setEnvironments([validToken.environment]);
}
} else {
setTokenError(
'Invalid token. Please make sure you are using a valid token from this Unleash instance',
);
}
};

return (
<Box sx={{ pb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
Expand Down Expand Up @@ -130,6 +182,7 @@ export const PlaygroundConnectionFieldset: VFC<
size='small'
value={envValue}
onChange={onEnvironmentsChange}
disabled={Boolean(token)}
data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'}
/>
<Autocomplete
Expand All @@ -154,9 +207,25 @@ export const PlaygroundConnectionFieldset: VFC<
)
}
onChange={onProjectsChange}
disabled={Boolean(token)}
data-testid={'PLAYGROUND_PROJECT_SELECT'}
/>
</Box>
<ConditionallyRender
condition={Boolean(playgroundImprovements)}
show={
<Input
sx={{ mt: 2, width: '50%', pr: 1 }}
label='Api token'
value={token || ''}
onChange={onSetToken}
type={'text'}
error={Boolean(tokenError)}
errorText={tokenError}
placeholder={'Enter your api token'}
/>
}
/>
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface IPlaygroundFormProps {
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
environments: string | string[];
projects: string[];
token?: string;
setToken?: React.Dispatch<React.SetStateAction<string | undefined>>;
setProjects: React.Dispatch<React.SetStateAction<string[]>>;
setEnvironments: React.Dispatch<React.SetStateAction<string[]>>;
context: string | undefined;
Expand All @@ -20,6 +22,8 @@ export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
environments,
onSubmit,
projects,
token,
setToken,
setProjects,
setEnvironments,
context,
Expand All @@ -39,6 +43,8 @@ export const PlaygroundForm: VFC<IPlaygroundFormProps> = ({
Array.isArray(environments) ? environments : [environments]
}
projects={projects}
token={token}
setToken={setToken}
setEnvironments={setEnvironments}
setProjects={setProjects}
availableEnvironments={availableEnvironments.map(
Expand Down
Loading