mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
feat: project environments configuration (#365)
This commit is contained in:
parent
44d1e61db4
commit
97893aa762
@ -1,13 +1,11 @@
|
|||||||
import { Portal, Snackbar } from '@material-ui/core';
|
import { Portal, Snackbar } from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import { IToast } from '../../../hooks/useToast';
|
||||||
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
||||||
|
|
||||||
interface IToastProps {
|
interface IToastProps extends IToast {
|
||||||
show: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
type: string;
|
|
||||||
text: string;
|
|
||||||
autoHideDuration?: number;
|
autoHideDuration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,6 +184,15 @@ Array [
|
|||||||
"title": ":id",
|
"title": ":id",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"layout": "main",
|
||||||
|
"menu": Object {},
|
||||||
|
"parent": "/projects",
|
||||||
|
"path": "/projects/:id/environments",
|
||||||
|
"title": "Environments",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { baseRoutes, getRoute } from '../routes';
|
import { baseRoutes, getRoute } from '../routes';
|
||||||
|
|
||||||
test('returns all baseRoutes', () => {
|
test('returns all baseRoutes', () => {
|
||||||
expect(baseRoutes).toHaveLength(39);
|
expect(baseRoutes).toHaveLength(40);
|
||||||
expect(baseRoutes).toMatchSnapshot();
|
expect(baseRoutes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import EditContextField from '../../page/context/edit';
|
|||||||
import CreateProject from '../../page/project/create';
|
import CreateProject from '../../page/project/create';
|
||||||
import EditProject from '../../page/project/edit';
|
import EditProject from '../../page/project/edit';
|
||||||
import EditProjectAccess from '../../page/project/access';
|
import EditProjectAccess from '../../page/project/access';
|
||||||
|
import EditProjectEnvironment from '../../page/project/environment';
|
||||||
import ListTagTypes from '../../page/tag-types';
|
import ListTagTypes from '../../page/tag-types';
|
||||||
import CreateTagType from '../../page/tag-types/create';
|
import CreateTagType from '../../page/tag-types/create';
|
||||||
import EditTagType from '../../page/tag-types/edit';
|
import EditTagType from '../../page/tag-types/edit';
|
||||||
@ -225,6 +226,15 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/projects/:id/environments',
|
||||||
|
parent: '/projects',
|
||||||
|
title: 'Environments',
|
||||||
|
component: EditProjectEnvironment,
|
||||||
|
type: 'protected',
|
||||||
|
layout: 'main',
|
||||||
|
menu: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/:id/archived',
|
path: '/projects/:id/archived',
|
||||||
title: ':name',
|
title: ':name',
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
deleteParagraph: {
|
||||||
|
marginTop: '2rem',
|
||||||
|
},
|
||||||
|
environmentDeleteInput: {
|
||||||
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,61 @@
|
|||||||
|
import { Alert } from '@material-ui/lab';
|
||||||
|
import React from 'react';
|
||||||
|
import Dialogue from '../../../common/Dialogue';
|
||||||
|
import Input from '../../../common/Input/Input';
|
||||||
|
import { ProjectEnvironment } from '../ProjectEnvironment';
|
||||||
|
import { useStyles } from './EnvironmentDisableConfirm.styles';
|
||||||
|
|
||||||
|
interface IEnvironmentDisableConfirmProps {
|
||||||
|
env?: ProjectEnvironment;
|
||||||
|
open: boolean;
|
||||||
|
handleDisableEnvironment: () => Promise<void>;
|
||||||
|
handleCancelDisableEnvironment: () => void;
|
||||||
|
confirmName: string;
|
||||||
|
setConfirmName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentDisableConfirm = ({
|
||||||
|
env,
|
||||||
|
open,
|
||||||
|
handleDisableEnvironment,
|
||||||
|
handleCancelDisableEnvironment,
|
||||||
|
confirmName,
|
||||||
|
setConfirmName,
|
||||||
|
}: IEnvironmentDisableConfirmProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setConfirmName(e.currentTarget.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
title="Are you sure you want to disable this environment?"
|
||||||
|
open={open}
|
||||||
|
primaryButtonText="Disable environment"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={() => handleDisableEnvironment()}
|
||||||
|
disabledPrimaryButton={env?.name !== confirmName}
|
||||||
|
onClose={handleCancelDisableEnvironment}
|
||||||
|
>
|
||||||
|
<Alert severity="error">
|
||||||
|
Danger. Disabling an environment can impact client applications
|
||||||
|
connected to the environment and result in feature toggles being
|
||||||
|
disabled.
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<p className={styles.deleteParagraph}>
|
||||||
|
In order to disable this environment, please enter the id of the
|
||||||
|
environment in the textfield below: <strong>{env?.name}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
onChange={handleChange}
|
||||||
|
value={confirmName}
|
||||||
|
label="Environment name"
|
||||||
|
className={styles.environmentDeleteInput}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnvironmentDisableConfirm;
|
@ -0,0 +1,24 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apiError: {
|
||||||
|
maxWidth: '400px',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
cardLink: {
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: '0',
|
||||||
|
background: 'transparent',
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
pointer: 'cursor',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,158 @@
|
|||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import { useStyles } from './ProjectEnvironment.styles';
|
||||||
|
|
||||||
|
import useLoading from '../../../hooks/useLoading';
|
||||||
|
import PageContent from '../../common/PageContent';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
import { UPDATE_PROJECT } from '../../AccessProvider/permissions';
|
||||||
|
|
||||||
|
import ApiError from '../../common/ApiError/ApiError';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
|
import useProject from '../../../hooks/api/getters/useProject/useProject';
|
||||||
|
import { FormControlLabel, FormGroup, Switch } from '@material-ui/core';
|
||||||
|
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDisableConfirm';
|
||||||
|
|
||||||
|
export interface ProjectEnvironment {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectEnvironmentList = () => {
|
||||||
|
const { id } = useParams<{id: string}>();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
|
// api state
|
||||||
|
const { toast, setToastData } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { environments, loading, error, refetch: refetchEnvs } = useEnvironments();
|
||||||
|
const { project, refetch: refetchProject } = useProject(id);
|
||||||
|
const { removeEnvironmentFromProject, addEnvironmentToProject } = useProjectApi();
|
||||||
|
|
||||||
|
// local state
|
||||||
|
const [selectedEnv, setSelectedEnv] = useState<ProjectEnvironment>();
|
||||||
|
const [confirmName, setConfirmName] = useState('');
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const refetch = () => {
|
||||||
|
refetchEnvs();
|
||||||
|
refetchProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderError = () => {
|
||||||
|
return (
|
||||||
|
<ApiError
|
||||||
|
onClick={refetch}
|
||||||
|
className={styles.apiError}
|
||||||
|
text="Error fetching environments"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorMsg = (enable: boolean): string => {
|
||||||
|
return `Got an API error when trying to ${enable ? 'enable' : 'disable'} the environment.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleEnv = async (env: ProjectEnvironment) => {
|
||||||
|
if(env.enabled) {
|
||||||
|
setSelectedEnv(env);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await addEnvironmentToProject(id, env.name);
|
||||||
|
setToastData({ text: 'Environment successfully enabled.', type: 'success', show: true});
|
||||||
|
} catch (error) {
|
||||||
|
setToastData({text: errorMsg(true), type: 'error', show: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDisableEnvironment = async () => {
|
||||||
|
if(selectedEnv && confirmName===selectedEnv.name) {
|
||||||
|
try {
|
||||||
|
await removeEnvironmentFromProject(id, selectedEnv.name);
|
||||||
|
setSelectedEnv(undefined);
|
||||||
|
setConfirmName('');
|
||||||
|
setToastData({ text: 'Environment successfully disabled.', type: 'success', show: true});
|
||||||
|
} catch (e) {
|
||||||
|
setToastData({ text: errorMsg(false), type: 'error', show: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelDisableEnvironment = () => {
|
||||||
|
setSelectedEnv(undefined);
|
||||||
|
setConfirmName('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envs = environments.map(e => ({
|
||||||
|
name: e.name,
|
||||||
|
enabled: (project?.environments).includes(e.name),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hasPermission = hasAccess(UPDATE_PROJECT, id);
|
||||||
|
|
||||||
|
const genLabel = (env: ProjectEnvironment) => (
|
||||||
|
<>
|
||||||
|
<code>{env.name}</code> environment is <strong>{env.enabled ? 'enabled' : 'disabled'}</strong>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderEnvironments = () => {
|
||||||
|
if(!uiConfig.flags.E) {
|
||||||
|
return <p>Feature not enabled.</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
{envs.map(env => (
|
||||||
|
<FormControlLabel key={env.name} label={genLabel(env)} control={
|
||||||
|
<Switch size="medium" disabled={!hasPermission} checked={env.enabled} onChange={toggleEnv.bind(this, env)} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<PageContent
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
title={`Configure environments for "${project?.name}"`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender condition={error} show={renderError()} />
|
||||||
|
<div className={styles.container}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={environments.length < 1 && !loading}
|
||||||
|
show={<div>No environments available.</div>}
|
||||||
|
elseShow={renderEnvironments()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<EnvironmentDisableConfirm
|
||||||
|
env={selectedEnv}
|
||||||
|
open={!!selectedEnv}
|
||||||
|
handleDisableEnvironment={handleDisableEnvironment}
|
||||||
|
handleCancelDisableEnvironment={handleCancelDisableEnvironment}
|
||||||
|
confirmName={confirmName}
|
||||||
|
setConfirmName={setConfirmName}
|
||||||
|
/>
|
||||||
|
{toast}
|
||||||
|
</PageContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectEnvironmentList;
|
@ -118,6 +118,17 @@ class ProjectFormComponent extends Component {
|
|||||||
hasAccess(CREATE_PROJECT) && editMode
|
hasAccess(CREATE_PROJECT) && editMode
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={() =>
|
||||||
|
this.props.history.push(
|
||||||
|
`/projects/${project.id}/environments`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Environments
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -128,6 +139,7 @@ class ProjectFormComponent extends Component {
|
|||||||
>
|
>
|
||||||
Manage access
|
Manage access
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,36 @@ const useProjectApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { deleteProject, errors };
|
const addEnvironmentToProject = async (projectId: string, environment: string) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/environments`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ environment }),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeEnvironmentFromProject = async (projectId: string, environment: string) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/environments/${environment}`;
|
||||||
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deleteProject, addEnvironmentToProject, removeEnvironmentFromProject, errors };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useProjectApi;
|
export default useProjectApi;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const fallbackProject = {
|
export const fallbackProject = {
|
||||||
features: [],
|
features: [],
|
||||||
|
environments: [],
|
||||||
name: '',
|
name: '',
|
||||||
health: 0,
|
health: 0,
|
||||||
members: 0,
|
members: 0,
|
||||||
|
@ -3,7 +3,7 @@ import Toast from '../component/common/Toast/Toast';
|
|||||||
|
|
||||||
export interface IToast {
|
export interface IToast {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
type: string;
|
type: 'success' | 'info' | 'warning' | 'error';
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ export interface IProject {
|
|||||||
version: string;
|
version: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
environments: string[];
|
||||||
health: number;
|
health: number;
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
}
|
}
|
||||||
|
6
frontend/src/page/project/environment.tsx
Normal file
6
frontend/src/page/project/environment.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ProjectEnvironment from '../../component/project/ProjectEnvironment/ProjectEnvironment';
|
||||||
|
|
||||||
|
const ProjectEnvironmentConfigPage = () => <ProjectEnvironment />;
|
||||||
|
|
||||||
|
export default ProjectEnvironmentConfigPage;
|
Loading…
Reference in New Issue
Block a user