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

refactor: restrict API tokens to enabled environments (#809)

* refactor: add missing Tooltip wrapper elements

* refactor: rewrite useEnvironments

* refactor: disable environments in select box

* refactor: make sure initial environment is enabled
This commit is contained in:
olav 2022-03-23 12:55:00 +01:00 committed by GitHub
parent cc0b9f7291
commit 2ca88b019a
9 changed files with 93 additions and 120 deletions

View File

@ -1,7 +1,7 @@
import { Button } from '@material-ui/core'; import { Button } from '@material-ui/core';
import { KeyboardArrowDownOutlined } from '@material-ui/icons'; import { KeyboardArrowDownOutlined } from '@material-ui/icons';
import React from 'react'; import React from 'react';
import useEnvironments from '../../../../hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from '../../../../hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from '../../../../hooks/api/getters/useProjects/useProjects'; import useProjects from '../../../../hooks/api/getters/useProjects/useProjects';
import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect'; import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect';
import Input from '../../../common/Input/Input'; import Input from '../../../common/Input/Input';
@ -10,11 +10,11 @@ interface IApiTokenFormProps {
username: string; username: string;
type: string; type: string;
project: string; project: string;
environment: string; environment?: string;
setTokenType: (value: string) => void; setTokenType: (value: string) => void;
setUsername: React.Dispatch<React.SetStateAction<string>>; setUsername: React.Dispatch<React.SetStateAction<string>>;
setProject: React.Dispatch<React.SetStateAction<string>>; setProject: React.Dispatch<React.SetStateAction<string>>;
setEnvironment: React.Dispatch<React.SetStateAction<string>>; setEnvironment: React.Dispatch<React.SetStateAction<string | undefined>>;
handleSubmit: (e: any) => void; handleSubmit: (e: any) => void;
handleCancel: () => void; handleCancel: () => void;
errors: { [key: string]: string }; errors: { [key: string]: string };
@ -54,13 +54,15 @@ const ApiTokenForm: React.FC<IApiTokenFormProps> = ({
title: i.name, title: i.name,
}) })
); );
const selectableEnvs = const selectableEnvs =
type === TYPE_ADMIN type === TYPE_ADMIN
? [{ key: '*', label: 'ALL' }] ? [{ key: '*', label: 'ALL' }]
: environments.map(i => ({ : environments.map(environment => ({
key: i.name, key: environment.name,
label: i.name, label: environment.name,
title: i.name, title: environment.name,
disabled: !environment.enabled,
})); }));
return ( return (

View File

@ -5,7 +5,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi'; import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useApiTokenForm from '../hooks/useApiTokenForm'; import { useApiTokenForm } from '../hooks/useApiTokenForm';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { ConfirmToken } from '../ConfirmToken/ConfirmToken'; import { ConfirmToken } from '../ConfirmToken/ConfirmToken';
import { useState } from 'react'; import { useState } from 'react';

View File

@ -1,37 +1,19 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
const useApiToken = ( export const useApiTokenForm = () => {
initialUserName = '', const { environments } = useEnvironments();
initialtype = 'CLIENT', const initialEnvironment = environments?.find(e => e.enabled)?.name;
initialProject = '*',
initialEnvironment = 'default' const [username, setUsername] = useState('');
) => { const [type, setType] = useState('CLIENT');
const [username, setUsername] = useState(initialUserName); const [project, setProject] = useState('*');
const [type, setType] = useState(initialtype); const [environment, setEnvironment] = useState<string>();
const [project, setProject] = useState(initialtype);
const [environment, setEnvironment] = useState(initialEnvironment);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
useEffect(() => { useEffect(() => {
setUsername(initialUserName); setEnvironment(type === 'ADMIN' ? '*' : initialEnvironment);
}, [initialUserName]); }, [type, initialEnvironment]);
useEffect(() => {
setType(initialtype);
if (type === 'ADMIN') {
setProject('*');
setEnvironment('*');
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialtype]);
useEffect(() => {
setProject(initialProject);
}, [initialProject]);
useEffect(() => {
setEnvironment(initialEnvironment);
}, [initialEnvironment]);
const setTokenType = (value: string) => { const setTokenType = (value: string) => {
if (value === 'ADMIN') { if (value === 'ADMIN') {
@ -82,5 +64,3 @@ const useApiToken = (
errors, errors,
}; };
}; };
export default useApiToken;

View File

@ -7,6 +7,7 @@ export interface ISelectOption {
key: string; key: string;
title?: string; title?: string;
label?: string; label?: string;
disabled?: boolean;
} }
export interface ISelectMenuProps { export interface ISelectMenuProps {
@ -52,6 +53,7 @@ const GeneralSelect: React.FC<ISelectMenuProps> = ({
value={option.key} value={option.key}
title={option.title || ''} title={option.title || ''}
data-test={`${SELECT_ITEM_ID}-${option.label}`} data-test={`${SELECT_ITEM_ID}-${option.label}`}
disabled={option.disabled}
> >
{option.label} {option.label}
</MenuItem> </MenuItem>

View File

@ -8,7 +8,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useEnvironments from 'hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import ConditionallyRender from 'component/common/ConditionallyRender'; import ConditionallyRender from 'component/common/ConditionallyRender';
import PageContent from 'component/common/PageContent/PageContent'; import PageContent from 'component/common/PageContent/PageContent';

View File

@ -3,9 +3,7 @@ import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton';
import { Add } from '@material-ui/icons'; import { Add } from '@material-ui/icons';
import PageContent from '../../common/PageContent'; import PageContent from '../../common/PageContent';
import { List } from '@material-ui/core'; import { List } from '@material-ui/core';
import useEnvironments, { import { useEnvironments } from '../../../hooks/api/getters/useEnvironments/useEnvironments';
ENVIRONMENT_CACHE_KEY,
} from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import { import {
IEnvironment, IEnvironment,
ISortOrderPayload, ISortOrderPayload,
@ -16,7 +14,6 @@ import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDele
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem'; import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
import { mutate } from 'swr';
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm'; import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
@ -32,7 +29,7 @@ const EnvironmentList = () => {
enabled: true, enabled: true,
protected: false, protected: false,
}; };
const { environments, refetch } = useEnvironments(); const { environments, refetchEnvironments } = useEnvironments();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { refetch: refetchProjectRolePermissions } = const { refetch: refetchProjectRolePermissions } =
useProjectRolePermissions(); useProjectRolePermissions();
@ -58,8 +55,7 @@ const EnvironmentList = () => {
const item = newEnvList.splice(dragIndex, 1)[0]; const item = newEnvList.splice(dragIndex, 1)[0];
newEnvList.splice(hoverIndex, 0, item); newEnvList.splice(hoverIndex, 0, item);
refetchEnvironments({ environments: newEnvList }, false);
mutate(ENVIRONMENT_CACHE_KEY, { environments: newEnvList }, false);
return newEnvList; return newEnvList;
}; };
@ -75,7 +71,6 @@ const EnvironmentList = () => {
try { try {
await sortOrderAPICall(sortOrder); await sortOrderAPICall(sortOrder);
refetch();
} catch (error: unknown) { } catch (error: unknown) {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} }
@ -104,7 +99,7 @@ const EnvironmentList = () => {
setDeldialogue(false); setDeldialogue(false);
setSelectedEnv(defaultEnv); setSelectedEnv(defaultEnv);
setConfirmName(''); setConfirmName('');
refetch(); refetchEnvironments();
} }
}; };
@ -128,7 +123,7 @@ const EnvironmentList = () => {
} catch (error: unknown) { } catch (error: unknown) {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} finally { } finally {
refetch(); refetchEnvironments();
} }
}; };
@ -144,7 +139,7 @@ const EnvironmentList = () => {
} catch (error: unknown) { } catch (error: unknown) {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} finally { } finally {
refetch(); refetchEnvironments();
} }
}; };

View File

@ -152,9 +152,11 @@ const EnvironmentListItem = ({
condition={updatePermission} condition={updatePermission}
show={ show={
<Tooltip title="Drag to reorder"> <Tooltip title="Drag to reorder">
<IconButton> <div>
<DragIndicator /> <IconButton>
</IconButton> <DragIndicator titleAccess="Drag" />
</IconButton>
</div>
</Tooltip> </Tooltip>
} }
/> />
@ -162,15 +164,16 @@ const EnvironmentListItem = ({
condition={updatePermission} condition={updatePermission}
show={ show={
<Tooltip title={`${tooltipText} environment`}> <Tooltip title={`${tooltipText} environment`}>
<IconButton <div>
aria-label="disable" <IconButton
onClick={() => { onClick={() => {
setSelectedEnv(env); setSelectedEnv(env);
setToggleDialog(prev => !prev); setToggleDialog(prev => !prev);
}} }}
> >
<OfflineBolt /> <OfflineBolt titleAccess="Toggle" />
</IconButton> </IconButton>
</div>
</Tooltip> </Tooltip>
} }
/> />
@ -178,15 +181,16 @@ const EnvironmentListItem = ({
condition={updatePermission} condition={updatePermission}
show={ show={
<Tooltip title="Update environment"> <Tooltip title="Update environment">
<IconButton <div>
aria-label="update" <IconButton
disabled={env.protected} disabled={env.protected}
onClick={() => { onClick={() => {
history.push(`/environments/${env.name}`); history.push(`/environments/${env.name}`);
}} }}
> >
<Edit /> <Edit titleAccess="Edit" />
</IconButton> </IconButton>
</div>
</Tooltip> </Tooltip>
} }
/> />
@ -194,16 +198,17 @@ const EnvironmentListItem = ({
condition={hasAccess(DELETE_ENVIRONMENT)} condition={hasAccess(DELETE_ENVIRONMENT)}
show={ show={
<Tooltip title="Delete environment"> <Tooltip title="Delete environment">
<IconButton <div>
aria-label="delete" <IconButton
disabled={env.protected} disabled={env.protected}
onClick={() => { onClick={() => {
setDeldialogue(true); setDeldialogue(true);
setSelectedEnv(env); setSelectedEnv(env);
}} }}
> >
<Delete /> <Delete titleAccess="Delete" />
</IconButton> </IconButton>
</div>
</Tooltip> </Tooltip>
} }
/> />

View File

@ -10,7 +10,7 @@ import { UPDATE_PROJECT } from '../../providers/AccessProvider/permissions';
import ApiError from '../../common/ApiError/ApiError'; import ApiError from '../../common/ApiError/ApiError';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import useProject from '../../../hooks/api/getters/useProject/useProject'; import useProject from '../../../hooks/api/getters/useProject/useProject';
import { FormControlLabel, FormGroup } from '@material-ui/core'; import { FormControlLabel, FormGroup } from '@material-ui/core';
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi'; import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
@ -32,12 +32,8 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
const [envs, setEnvs] = useState<IProjectEnvironment[]>([]); const [envs, setEnvs] = useState<IProjectEnvironment[]>([]);
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { const { environments, loading, error, refetchEnvironments } =
environments, useEnvironments();
loading,
error,
refetch: refetchEnvs,
} = useEnvironments();
const { project, refetch: refetchProject } = useProject(projectId); const { project, refetch: refetchProject } = useProject(projectId);
const { removeEnvironmentFromProject, addEnvironmentToProject } = const { removeEnvironmentFromProject, addEnvironmentToProject } =
useProjectApi(); useProjectApi();
@ -59,7 +55,7 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
}, [environments, project?.environments]); }, [environments, project?.environments]);
const refetch = () => { const refetch = () => {
refetchEnvs(); refetchEnvironments();
refetchProject(); refetchProject();
}; };

View File

@ -1,42 +1,35 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import useSWR, { mutate } from 'swr';
import { useState, useEffect } from 'react'; import { useCallback, useMemo } from 'react';
import { IEnvironmentResponse } from '../../../../interfaces/environments'; import { IEnvironmentResponse } from 'interfaces/environments';
import { formatApiPath } from '../../../../utils/format-path'; import { formatApiPath } from 'utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
export const ENVIRONMENT_CACHE_KEY = `api/admin/environments`; const PATH = formatApiPath(`api/admin/environments`);
const useEnvironments = (options: SWRConfiguration = {}) => { export const useEnvironments = () => {
const fetcher = () => { const { data, error } = useSWR<IEnvironmentResponse>(PATH, fetcher);
const path = formatApiPath(`api/admin/environments`);
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('Environments'))
.then(res => res.json());
};
const { data, error } = useSWR<IEnvironmentResponse>( const refetchEnvironments = useCallback(
ENVIRONMENT_CACHE_KEY, (data?: IEnvironmentResponse, revalidate?: boolean) => {
fetcher, mutate(PATH, data, revalidate).catch(console.warn);
options },
[]
); );
const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const environments = useMemo(() => {
mutate(ENVIRONMENT_CACHE_KEY); return data?.environments || [];
}; }, [data]);
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return { return {
environments: data?.environments || [], environments,
refetchEnvironments,
loading: !error && !data,
error, error,
loading,
refetch,
}; };
}; };
export default useEnvironments; const fetcher = (): Promise<IEnvironmentResponse> => {
return fetch(PATH)
.then(handleErrorResponses('Environments'))
.then(res => res.json());
};