mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Feat/environment crud (#335)
* feat: add env * fix: create environment form * feat: create environment * feat: add deletion protection * fix: lift up state * feat: add ability to update environment * fix: remove env reset * fix: remove link * feat: add drag and drop sorting * fix: remove unused imports * feat: add methods to toggle env on/off * feat: only make api call on drop * fix: disabled text * fix: add disabled indicator * fix: add edit env payload * fix: add E flag * fix: cleanup * fix: update snapshots * fix: remove useFeature * fix: change property to errorText * fix: update tests * fix: change menu * fix: update snapshots * feat: toggle view v2 * fix: handle error on sort order api call * fix: remove unused import * fix: useFeature * fix: update tests * fix: console logs * fix: use try catch * fix: update snapshots
This commit is contained in:
parent
b2a6201ce3
commit
92f3f8af08
@ -24,3 +24,5 @@ export const DELETE_ADDON = 'DELETE_ADDON';
|
||||
export const UPDATE_API_TOKEN = 'UPDATE_API_TOKEN';
|
||||
export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
||||
export const DELETE_API_TOKEN = 'DELETE_API_TOKEN';
|
||||
export const DELETE_ENVIRONMENT = 'DELETE_ENVIRONMENT';
|
||||
export const UPDATE_ENVIRONMENT = 'UPDATE_ENVIRONMENT';
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
badge: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
width: '75px',
|
||||
height: '75px',
|
||||
borderRadius: '50px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
check: {
|
||||
color: '#fff',
|
||||
width: '35px',
|
||||
height: '35px',
|
||||
},
|
||||
}));
|
@ -0,0 +1,13 @@
|
||||
import { Check } from '@material-ui/icons';
|
||||
import { useStyles } from './CheckMarkBadge.styles';
|
||||
|
||||
const CheckMarkBadge = () => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.badge}>
|
||||
<Check className={styles.check} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckMarkBadge;
|
@ -13,7 +13,7 @@ const HeaderTitle = ({
|
||||
subtitle,
|
||||
variant,
|
||||
loading,
|
||||
className,
|
||||
className = '',
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const headerClasses = classnames({ skeleton: loading });
|
||||
|
12
frontend/src/component/common/Input/Input.styles.ts
Normal file
12
frontend/src/component/common/Input/Input.styles.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
helperText: {
|
||||
position: 'absolute',
|
||||
top: '35px',
|
||||
},
|
||||
inputContainer: {
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
}));
|
53
frontend/src/component/common/Input/Input.tsx
Normal file
53
frontend/src/component/common/Input/Input.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { TextField } from '@material-ui/core';
|
||||
import { useStyles } from './Input.styles.ts';
|
||||
|
||||
interface IInputProps {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
error?: boolean;
|
||||
errorText?: string;
|
||||
style?: Object;
|
||||
className?: string;
|
||||
value: string;
|
||||
onChange: (e: any) => any;
|
||||
onFocus?: (e: any) => any;
|
||||
onBlur?: (e: any) => any;
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
label,
|
||||
placeholder,
|
||||
error,
|
||||
errorText,
|
||||
style,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
...rest
|
||||
}: IInputProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.inputContainer} data-loading>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
error={error}
|
||||
helperText={errorText}
|
||||
style={style}
|
||||
className={className ? className : ''}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
FormHelperTextProps={{
|
||||
classes: {
|
||||
root: styles.helperText,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
@ -1,4 +1,5 @@
|
||||
export const P = 'P';
|
||||
export const C = 'C';
|
||||
export const E = 'E';
|
||||
export const RBAC = 'RBAC';
|
||||
export const PROJECTFILTERING = false;
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
helperText: { marginBottom: '1rem' },
|
||||
formHeader: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1rem',
|
||||
marginTop: '2rem',
|
||||
},
|
||||
radioGroup: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
environmentDetailsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin: '1rem 0',
|
||||
},
|
||||
submitButton: {
|
||||
marginTop: '1rem',
|
||||
width: '150px',
|
||||
marginRight: '1rem',
|
||||
},
|
||||
btnContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputField: {
|
||||
width: '100%',
|
||||
marginTop: '1rem',
|
||||
},
|
||||
}));
|
@ -0,0 +1,184 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormControl, Button } from '@material-ui/core';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import PageContent from '../../common/PageContent';
|
||||
|
||||
import { useStyles } from './CreateEnvironment.styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import CreateEnvironmentSuccess from './CreateEnvironmentSuccess/CreateEnvironmentSuccess';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
|
||||
import Input from '../../common/Input/Input';
|
||||
|
||||
const NAME_EXISTS_ERROR = 'Error: Environment';
|
||||
|
||||
const CreateEnvironment = () => {
|
||||
const [type, setType] = useState('development');
|
||||
const [envName, setEnvName] = useState('');
|
||||
const [envDisplayName, setEnvDisplayName] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [createSuccess, setCreateSucceess] = useState(false);
|
||||
const history = useHistory();
|
||||
const styles = useStyles();
|
||||
const { validateEnvName, createEnvironment, loading } = useEnvironmentApi();
|
||||
const ref = useLoading(loading);
|
||||
const { toast, setToastData } = useToast();
|
||||
|
||||
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
setType(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleEnvNameChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
setEnvName(e.currentTarget.value);
|
||||
setEnvDisplayName(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleEnvDisplayName = (e: React.FormEvent<HTMLInputElement>) =>
|
||||
setEnvDisplayName(e.currentTarget.value);
|
||||
|
||||
const goBack = () => history.goBack();
|
||||
|
||||
const validateEnvironmentName = async () => {
|
||||
if (envName.length === 0) {
|
||||
setNameError('Environment Id can not be empty.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await validateEnvName(envName);
|
||||
} catch (e) {
|
||||
if (e.toString().includes(NAME_EXISTS_ERROR)) {
|
||||
setNameError('Name already exists');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const clearNameError = () => setNameError('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const validName = await validateEnvironmentName();
|
||||
|
||||
if (validName) {
|
||||
const environment = {
|
||||
name: envName,
|
||||
displayName: envDisplayName,
|
||||
type,
|
||||
};
|
||||
|
||||
try {
|
||||
await createEnvironment(environment);
|
||||
setCreateSucceess(true);
|
||||
} catch (e) {
|
||||
setToastData({ show: true, type: 'error', text: e.toString() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent headerContent={<HeaderTitle title="Create environment" />}>
|
||||
<ConditionallyRender
|
||||
condition={createSuccess}
|
||||
show={
|
||||
<CreateEnvironmentSuccess
|
||||
name={envName}
|
||||
displayName={envDisplayName}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<div ref={ref}>
|
||||
<p className={styles.helperText} data-loading>
|
||||
Environments allow you to manage your product
|
||||
lifecycle from local development through production.
|
||||
Your projects and feature toggles are accessible in
|
||||
all your environments, but they can take different
|
||||
configurations per environment. This means that you
|
||||
can enable a feature toggle in a development or test
|
||||
environment without enabling the feature toggle in
|
||||
the production environment.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormControl component="fieldset">
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment Id and name
|
||||
</h3>
|
||||
|
||||
<div
|
||||
data-loading
|
||||
className={
|
||||
styles.environmentDetailsContainer
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Unique env name for SDK configurations.
|
||||
</p>
|
||||
<Input
|
||||
label="Environment Id"
|
||||
onFocus={clearNameError}
|
||||
placeholder="A unique name for your environment"
|
||||
onBlur={validateEnvironmentName}
|
||||
error={Boolean(nameError)}
|
||||
errorText={nameError}
|
||||
value={envName}
|
||||
onChange={handleEnvNameChange}
|
||||
className={styles.inputField}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-loading
|
||||
className={
|
||||
styles.environmentDetailsContainer
|
||||
}
|
||||
>
|
||||
<p>Environment display name.</p>
|
||||
<Input
|
||||
label="Display name"
|
||||
placeholder="Optional name to be displayed in the admin panel"
|
||||
className={styles.inputField}
|
||||
value={envDisplayName}
|
||||
onChange={handleEnvDisplayName}
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentTypeSelector
|
||||
onChange={handleTypeChange}
|
||||
value={type}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={styles.btnContainer}>
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-loading
|
||||
>
|
||||
Submit
|
||||
</Button>{' '}
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={goBack}
|
||||
data-loading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{toast}
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEnvironment;
|
@ -0,0 +1,41 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
subheader: {
|
||||
fontSize: theme.fontSizes.subHeader,
|
||||
fontWeight: 'normal',
|
||||
marginTop: '2rem',
|
||||
},
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
nextSteps: {
|
||||
display: 'flex',
|
||||
},
|
||||
step: { maxWidth: '350px', margin: '0 1.5rem', position: 'relative' },
|
||||
stepBadge: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
borderRadius: '25px',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
margin: '2rem auto',
|
||||
},
|
||||
stepParagraph: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
button: {
|
||||
marginTop: '2.5rem',
|
||||
minWidth: '150px',
|
||||
},
|
||||
link: {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
@ -0,0 +1,93 @@
|
||||
import { Button } from '@material-ui/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import CheckMarkBadge from '../../../common/CheckmarkBadge/CheckMarkBadge';
|
||||
import { useStyles } from './CreateEnvironmentSuccess.styles';
|
||||
import CreateEnvironmentSuccessCard from './CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
|
||||
|
||||
export interface ICreateEnvironmentSuccessProps {
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const CreateEnvironmentSuccess = ({
|
||||
name,
|
||||
displayName,
|
||||
type,
|
||||
}: ICreateEnvironmentSuccessProps) => {
|
||||
const history = useHistory();
|
||||
const styles = useStyles();
|
||||
|
||||
const navigateToEnvironmentList = () => {
|
||||
history.push('/environments');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CheckMarkBadge />
|
||||
<h2 className={styles.subheader}>Environment created</h2>
|
||||
<CreateEnvironmentSuccessCard
|
||||
name={name}
|
||||
displayName={displayName}
|
||||
type={type}
|
||||
/>
|
||||
<h2 className={styles.subheader}>Next steps</h2>
|
||||
<div className={styles.nextSteps}>
|
||||
<div className={styles.step}>
|
||||
<div>
|
||||
<div className={styles.stepBadge}>1</div>
|
||||
<h3 className={styles.subheader}>
|
||||
Update SDK version and provide the environment id to
|
||||
the SDK
|
||||
</h3>
|
||||
<p className={styles.stepParagraph}>
|
||||
By providing the environment id in the SDK the SDK
|
||||
will only retrieve activation strategies for
|
||||
specified environment
|
||||
</p>
|
||||
<a
|
||||
href="https://docs.getunleash.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.step}>
|
||||
<div>
|
||||
<div className={styles.stepBadge}>2</div>
|
||||
<h3 className={styles.subheader}>
|
||||
Add environment specific activation strategies
|
||||
</h3>
|
||||
|
||||
<p className={styles.stepParagraph}>
|
||||
You can now select this environment when you are
|
||||
adding new activation strategies on feature toggles.
|
||||
</p>
|
||||
<a
|
||||
href="https://docs.getunleash.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={styles.button}
|
||||
onClick={navigateToEnvironmentList}
|
||||
>
|
||||
Got it!
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEnvironmentSuccess;
|
@ -0,0 +1,31 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: `1px solid ${theme.palette.grey[200]}`,
|
||||
padding: '1.5rem',
|
||||
borderRadius: '5px',
|
||||
margin: '1.5rem 0',
|
||||
minWidth: '450px',
|
||||
},
|
||||
icon: {
|
||||
fill: theme.palette.grey[600],
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '0.25rem',
|
||||
},
|
||||
infoContainer: {
|
||||
marginTop: '1rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
infoInnerContainer: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
infoTitle: { fontWeight: 'bold', marginBottom: '0.25rem' },
|
||||
}));
|
@ -0,0 +1,35 @@
|
||||
import { CloudCircle } from '@material-ui/icons';
|
||||
import { ICreateEnvironmentSuccessProps } from '../CreateEnvironmentSuccess';
|
||||
import { useStyles } from './CreateEnvironmentSuccessCard.styles';
|
||||
|
||||
const CreateEnvironmentSuccessCard = ({
|
||||
name,
|
||||
displayName,
|
||||
type,
|
||||
}: ICreateEnvironmentSuccessProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<CloudCircle className={styles.icon} /> Environment
|
||||
</div>
|
||||
|
||||
<div className={styles.infoContainer}>
|
||||
<div className={styles.infoInnerContainer}>
|
||||
<div className={styles.infoTitle}>Id</div>
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
<div className={styles.infoInnerContainer}>
|
||||
<div className={styles.infoTitle}>Displayname</div>
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
<div className={styles.infoInnerContainer}>
|
||||
<div className={styles.infoTitle}>Type</div>
|
||||
<div>{type}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEnvironmentSuccessCard;
|
@ -0,0 +1,54 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
minWidth: '300px',
|
||||
position: 'absolute',
|
||||
right: '80px',
|
||||
bottom: '-475px',
|
||||
zIndex: 9999,
|
||||
opacity: 0,
|
||||
transform: 'translateY(100px)',
|
||||
},
|
||||
inputField: {
|
||||
width: '100%',
|
||||
},
|
||||
header: {
|
||||
fontSize: theme.fontSizes.subHeader,
|
||||
fontWeight: 'normal',
|
||||
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||
padding: '1rem',
|
||||
},
|
||||
body: { padding: '1rem' },
|
||||
subheader: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
icon: {
|
||||
marginRight: '0.5rem',
|
||||
fill: theme.palette.grey[600],
|
||||
},
|
||||
formHeader: {
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
},
|
||||
buttonGroup: {
|
||||
marginTop: '2rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
editEnvButton: {
|
||||
width: '150px',
|
||||
},
|
||||
fadeInBottomEnter: {
|
||||
transform: 'translateY(0)',
|
||||
opacity: '1',
|
||||
transition: 'transform 0.4s ease, opacity .4s ease',
|
||||
},
|
||||
fadeInBottomLeave: {
|
||||
transform: 'translateY(100px)',
|
||||
opacity: '0',
|
||||
transition: 'transform 0.4s ease, opacity 0.4s ease',
|
||||
},
|
||||
}));
|
@ -0,0 +1,130 @@
|
||||
import { CloudCircle } from '@material-ui/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
|
||||
import { useStyles } from './EditEnvironment.styles';
|
||||
import { IEnvironment } from '../../../interfaces/environments';
|
||||
import Input from '../../common/Input/Input';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import Dialogue from '../../common/Dialogue';
|
||||
|
||||
interface IEditEnvironmentProps {
|
||||
env: IEnvironment;
|
||||
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
editEnvironment: boolean;
|
||||
setToastData: React.Dispatch<React.SetStateAction<any>>;
|
||||
}
|
||||
|
||||
const EditEnvironment = ({
|
||||
env,
|
||||
setEditEnvironment,
|
||||
editEnvironment,
|
||||
setToastData,
|
||||
}: IEditEnvironmentProps) => {
|
||||
const styles = useStyles();
|
||||
const [type, setType] = useState(env.type);
|
||||
const [envDisplayName, setEnvDisplayName] = useState(env.displayName);
|
||||
const { updateEnvironment, loading } = useEnvironmentApi();
|
||||
const { refetch } = useEnvironments();
|
||||
const ref = useLoading(loading);
|
||||
|
||||
useEffect(() => {
|
||||
setType(env.type);
|
||||
setEnvDisplayName(env.displayName);
|
||||
}, [env.type, env.displayName]);
|
||||
|
||||
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
setType(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleEnvDisplayName = (e: React.FormEvent<HTMLInputElement>) =>
|
||||
setEnvDisplayName(e.currentTarget.value);
|
||||
|
||||
const isDisabled = () => {
|
||||
if (type === env.type && envDisplayName === env.displayName) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditEnvironment(false);
|
||||
resetFields();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const updatedEnv = {
|
||||
sortOrder: env.sortOrder,
|
||||
displayName: envDisplayName,
|
||||
type,
|
||||
};
|
||||
|
||||
try {
|
||||
await updateEnvironment(env.name, updatedEnv);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
text: 'Successfully updated environment.',
|
||||
});
|
||||
resetFields();
|
||||
refetch();
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
} finally {
|
||||
setEditEnvironment(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFields = () => {
|
||||
setType(env.type);
|
||||
setEnvDisplayName(env.displayName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={editEnvironment}
|
||||
title="Edit environment"
|
||||
onClose={handleCancel}
|
||||
onClick={handleSubmit}
|
||||
primaryButtonText="Save"
|
||||
secondaryButtonText="Cancel"
|
||||
disabledPrimaryButton={isDisabled()}
|
||||
>
|
||||
<div className={styles.body} ref={ref}>
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment Id
|
||||
</h3>
|
||||
<h3 className={styles.subheader} data-loading>
|
||||
<CloudCircle className={styles.icon} /> {env.name}
|
||||
</h3>
|
||||
<form>
|
||||
<EnvironmentTypeSelector
|
||||
onChange={handleTypeChange}
|
||||
value={type}
|
||||
/>
|
||||
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment display name
|
||||
</h3>
|
||||
|
||||
<Input
|
||||
label="Display name"
|
||||
placeholder="Optional name to be displayed in the admin panel"
|
||||
className={styles.inputField}
|
||||
value={envDisplayName}
|
||||
onChange={handleEnvDisplayName}
|
||||
data-loading
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditEnvironment;
|
@ -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,73 @@
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { IEnvironment } from '../../../../interfaces/environments';
|
||||
import Dialogue from '../../../common/Dialogue';
|
||||
import Input from '../../../common/Input/Input';
|
||||
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
|
||||
import { useStyles } from './EnvironmentDeleteConfirm.styles';
|
||||
|
||||
interface IEnviromentDeleteConfirmProps {
|
||||
env: IEnvironment;
|
||||
open: boolean;
|
||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteEnvironment: (name: string) => Promise<void>;
|
||||
confirmName: string;
|
||||
setConfirmName: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const EnvironmentDeleteConfirm = ({
|
||||
env,
|
||||
open,
|
||||
setDeldialogue,
|
||||
handleDeleteEnvironment,
|
||||
confirmName,
|
||||
setConfirmName,
|
||||
}: IEnviromentDeleteConfirmProps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setConfirmName(e.currentTarget.value);
|
||||
|
||||
const handleCancel = () => {
|
||||
setDeldialogue(false);
|
||||
setConfirmName('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
title="Are you sure you want to delete this environment?"
|
||||
open={open}
|
||||
primaryButtonText="Delete environment"
|
||||
secondaryButtonText="Cancel"
|
||||
onClick={handleDeleteEnvironment}
|
||||
disabledPrimaryButton={env?.name !== confirmName}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
<Alert severity="error">
|
||||
Danger. Deleting this environment will result in removing all
|
||||
strategies that are active in this environment across all
|
||||
feature toggles.
|
||||
</Alert>
|
||||
<CreateEnvironmentSuccessCard
|
||||
name={env?.name}
|
||||
displayName={env?.displayName}
|
||||
type={env?.type}
|
||||
/>
|
||||
|
||||
<p className={styles.deleteParagraph}>
|
||||
In order to delete 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 EnvironmentDeleteConfirm;
|
@ -0,0 +1,233 @@
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton';
|
||||
import { Add } from '@material-ui/icons';
|
||||
import PageContent from '../../common/PageContent';
|
||||
import { List } from '@material-ui/core';
|
||||
import useEnvironments, {
|
||||
ENVIRONMENT_CACHE_KEY,
|
||||
} from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import {
|
||||
IEnvironment,
|
||||
ISortOrderPayload,
|
||||
} from '../../../interfaces/environments';
|
||||
import { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
|
||||
import { mutate } from 'swr';
|
||||
import EditEnvironment from '../EditEnvironment/EditEnvironment';
|
||||
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
||||
|
||||
const EnvironmentList = () => {
|
||||
const defaultEnv = {
|
||||
name: '',
|
||||
type: '',
|
||||
displayName: '',
|
||||
sortOrder: 0,
|
||||
createdAt: '',
|
||||
enabled: true,
|
||||
protected: false,
|
||||
};
|
||||
const { environments, refetch } = useEnvironments();
|
||||
const [editEnvironment, setEditEnvironment] = useState(false);
|
||||
|
||||
const [selectedEnv, setSelectedEnv] = useState(defaultEnv);
|
||||
const [delDialog, setDeldialogue] = useState(false);
|
||||
const [toggleDialog, setToggleDialog] = useState(false);
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
|
||||
const history = useHistory();
|
||||
const { toast, setToastData } = useToast();
|
||||
const {
|
||||
deleteEnvironment,
|
||||
changeSortOrder,
|
||||
toggleEnvironmentOn,
|
||||
toggleEnvironmentOff,
|
||||
} = useEnvironmentApi();
|
||||
|
||||
const moveListItem = (dragIndex: number, hoverIndex: number) => {
|
||||
const newEnvList = [...environments];
|
||||
if (newEnvList.length === 0) return newEnvList;
|
||||
|
||||
const item = newEnvList.splice(dragIndex, 1)[0];
|
||||
|
||||
newEnvList.splice(hoverIndex, 0, item);
|
||||
|
||||
mutate(ENVIRONMENT_CACHE_KEY, { environments: newEnvList }, false);
|
||||
return newEnvList;
|
||||
};
|
||||
|
||||
const moveListItemApi = async (dragIndex: number, hoverIndex: number) => {
|
||||
const newEnvList = moveListItem(dragIndex, hoverIndex);
|
||||
const sortOrder = newEnvList.reduce(
|
||||
(acc: ISortOrderPayload, env: IEnvironment, index: number) => {
|
||||
acc[env.name] = index + 1;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
try {
|
||||
await sortOrderAPICall(sortOrder);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
mutate(ENVIRONMENT_CACHE_KEY);
|
||||
};
|
||||
|
||||
const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => {
|
||||
try {
|
||||
changeSortOrder(sortOrder);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEnvironment = async () => {
|
||||
try {
|
||||
await deleteEnvironment(selectedEnv.name);
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully deleted environment.',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
} finally {
|
||||
setDeldialogue(false);
|
||||
setSelectedEnv(defaultEnv);
|
||||
setConfirmName('');
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmToggleEnvironment = () => {
|
||||
if (selectedEnv.enabled) {
|
||||
return handleToggleEnvironmentOff();
|
||||
}
|
||||
handleToggleEnvironmentOn();
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOn = async () => {
|
||||
try {
|
||||
await toggleEnvironmentOn(selectedEnv.name);
|
||||
setToggleDialog(false);
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully enabled environment.',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOff = async () => {
|
||||
try {
|
||||
await toggleEnvironmentOff(selectedEnv.name);
|
||||
setToggleDialog(false);
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully disabled environment.',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const environmentList = () =>
|
||||
environments.map((env: IEnvironment, index: number) => (
|
||||
<EnvironmentListItem
|
||||
key={env.name}
|
||||
env={env}
|
||||
setEditEnvironment={setEditEnvironment}
|
||||
setDeldialogue={setDeldialogue}
|
||||
setSelectedEnv={setSelectedEnv}
|
||||
setToggleDialog={setToggleDialog}
|
||||
index={index}
|
||||
moveListItem={moveListItem}
|
||||
moveListItemApi={moveListItemApi}
|
||||
/>
|
||||
));
|
||||
|
||||
const navigateToCreateEnvironment = () => {
|
||||
history.push('/environments/create');
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title="Environments"
|
||||
actions={
|
||||
<>
|
||||
<ResponsiveButton
|
||||
onClick={navigateToCreateEnvironment}
|
||||
maxWidth="700px"
|
||||
tooltip="Add environment"
|
||||
Icon={Add}
|
||||
>
|
||||
Add Environment
|
||||
</ResponsiveButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<List>{environmentList()}</List>
|
||||
<EnvironmentDeleteConfirm
|
||||
env={selectedEnv}
|
||||
setSelectedEnv={setSelectedEnv}
|
||||
setDeldialogue={setDeldialogue}
|
||||
open={delDialog}
|
||||
handleDeleteEnvironment={handleDeleteEnvironment}
|
||||
confirmName={confirmName}
|
||||
setConfirmName={setConfirmName}
|
||||
/>
|
||||
|
||||
<EditEnvironment
|
||||
env={selectedEnv}
|
||||
setEditEnvironment={setEditEnvironment}
|
||||
editEnvironment={editEnvironment}
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
<EnvironmentToggleConfirm
|
||||
env={selectedEnv}
|
||||
open={toggleDialog}
|
||||
setToggleDialog={setToggleDialog}
|
||||
handleConfirmToggleEnvironment={handleConfirmToggleEnvironment}
|
||||
/>
|
||||
{toast}
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentList;
|
@ -0,0 +1,213 @@
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
CloudCircle,
|
||||
Delete,
|
||||
DragIndicator,
|
||||
Edit,
|
||||
OfflineBolt,
|
||||
} from '@material-ui/icons';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
|
||||
import { IEnvironment } from '../../../../interfaces/environments';
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import AccessContext from '../../../../contexts/AccessContext';
|
||||
import {
|
||||
DELETE_ENVIRONMENT,
|
||||
UPDATE_ENVIRONMENT,
|
||||
} from '../../../AccessProvider/permissions';
|
||||
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
|
||||
import { XYCoord } from 'dnd-core';
|
||||
|
||||
interface IEnvironmentListItemProps {
|
||||
env: IEnvironment;
|
||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
index: number;
|
||||
moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[];
|
||||
moveListItemApi: (dragIndex: number, hoverIndex: number) => Promise<void>;
|
||||
}
|
||||
|
||||
interface DragItem {
|
||||
index: number;
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const EnvironmentListItem = ({
|
||||
env,
|
||||
setSelectedEnv,
|
||||
setDeldialogue,
|
||||
index,
|
||||
moveListItem,
|
||||
moveListItemApi,
|
||||
setToggleDialog,
|
||||
setEditEnvironment,
|
||||
}: IEnvironmentListItemProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ACCEPT_TYPE = 'LIST_ITEM';
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: ACCEPT_TYPE,
|
||||
item: () => {
|
||||
return { env, index };
|
||||
},
|
||||
collect: (monitor: any) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ handlerId }, drop] = useDrop({
|
||||
accept: ACCEPT_TYPE,
|
||||
collect(monitor) {
|
||||
return {
|
||||
handlerId: monitor.getHandlerId(),
|
||||
};
|
||||
},
|
||||
drop(item: DragItem, monitor: DropTargetMonitor) {
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
moveListItemApi(dragIndex, hoverIndex);
|
||||
},
|
||||
hover(item: DragItem, monitor: DropTargetMonitor) {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
|
||||
const hoverMiddleY =
|
||||
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
|
||||
const hoverClientY =
|
||||
(clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveListItem(dragIndex, hoverIndex);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
const opacity = isDragging ? 0 : 1;
|
||||
drag(drop(ref));
|
||||
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const tooltipText = env.enabled ? 'Disable' : 'Enable';
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
style={{ position: 'relative', opacity }}
|
||||
ref={ref}
|
||||
data-handler-id={handlerId}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<CloudCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<>
|
||||
<strong>{env.name}</strong>
|
||||
<ConditionallyRender
|
||||
condition={!env.enabled}
|
||||
show={
|
||||
<span
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
borderRadius: '5px',
|
||||
marginLeft: '0.5rem',
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
disabled
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
secondary={env.displayName}
|
||||
/>
|
||||
|
||||
<Tooltip title="Drag to reorder">
|
||||
<IconButton>
|
||||
<DragIndicator />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_ENVIRONMENT)}
|
||||
show={
|
||||
<Tooltip title={`${tooltipText} environment`}>
|
||||
<IconButton
|
||||
aria-label="disable"
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
setSelectedEnv(env);
|
||||
setToggleDialog(prev => !prev);
|
||||
}}
|
||||
>
|
||||
<OfflineBolt />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_ENVIRONMENT)}
|
||||
show={
|
||||
<Tooltip title="Update environment">
|
||||
<IconButton
|
||||
aria-label="update"
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
setSelectedEnv(env);
|
||||
setEditEnvironment(prev => !prev);
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(DELETE_ENVIRONMENT)}
|
||||
show={
|
||||
<Tooltip title="Delete environment">
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
setDeldialogue(true);
|
||||
setSelectedEnv(env);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentListItem;
|
@ -0,0 +1,64 @@
|
||||
import { capitalize } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { IEnvironment } from '../../../../interfaces/environments';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import Dialogue from '../../../common/Dialogue';
|
||||
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
|
||||
|
||||
interface IEnvironmentToggleConfirmProps {
|
||||
env: IEnvironment;
|
||||
open: boolean;
|
||||
setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleConfirmToggleEnvironment: () => void;
|
||||
}
|
||||
|
||||
const EnvironmentToggleConfirm = ({
|
||||
env,
|
||||
open,
|
||||
setToggleDialog,
|
||||
handleConfirmToggleEnvironment,
|
||||
}: IEnvironmentToggleConfirmProps) => {
|
||||
let text = env.enabled ? 'disable' : 'enable';
|
||||
|
||||
const handleCancel = () => {
|
||||
setToggleDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
title={`Are you sure you want to ${text} this environment?`}
|
||||
open={open}
|
||||
primaryButtonText={capitalize(text)}
|
||||
secondaryButtonText="Cancel"
|
||||
onClick={handleConfirmToggleEnvironment}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={env.enabled}
|
||||
show={
|
||||
<Alert severity="info">
|
||||
Disabling an environment will not effect any strategies
|
||||
that already exist in that environment, but it will make
|
||||
it unavailable as a selection option for new activation
|
||||
strategies.
|
||||
</Alert>
|
||||
}
|
||||
elseShow={
|
||||
<Alert severity="info">
|
||||
Enabling an environment will allow you to add new
|
||||
activation strategies to this environment.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateEnvironmentSuccessCard
|
||||
name={env?.name}
|
||||
displayName={env?.displayName}
|
||||
type={env?.type}
|
||||
/>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentToggleConfirm;
|
@ -0,0 +1,14 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
radioGroup: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
formHeader: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
marginTop: '1.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
radioBtnGroup: { display: 'flex', flexDirection: 'column' },
|
||||
}));
|
@ -0,0 +1,60 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
} from '@material-ui/core';
|
||||
import { useStyles } from './EnvironmentTypeSelector.styles';
|
||||
|
||||
interface IEnvironmentTypeSelectorProps {
|
||||
onChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const EnvironmentTypeSelector = ({
|
||||
onChange,
|
||||
value,
|
||||
}: IEnvironmentTypeSelectorProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<FormControl component="fieldset">
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment Type
|
||||
</h3>
|
||||
|
||||
<RadioGroup
|
||||
data-loading
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<div className={styles.radioBtnGroup}>
|
||||
<FormControlLabel
|
||||
value="development"
|
||||
label="Development"
|
||||
control={<Radio />}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="test"
|
||||
label="Test"
|
||||
control={<Radio />}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<FormControlLabel
|
||||
value="preproduction"
|
||||
label="Pre production"
|
||||
control={<Radio />}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="production"
|
||||
label="Production"
|
||||
control={<Radio />}
|
||||
/>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentTypeSelector;
|
20
frontend/src/component/feature/FeatureView2/FeatureView2.tsx
Normal file
20
frontend/src/component/feature/FeatureView2/FeatureView2.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
||||
import FeatureViewEnvironment from './FeatureViewEnvironment/FeatureViewEnvironment';
|
||||
import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
|
||||
|
||||
const FeatureView2 = () => {
|
||||
const { projectId, featureId } = useParams();
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<FeatureViewMetaData />
|
||||
{feature.environments.map(env => {
|
||||
return <FeatureViewEnvironment env={env} key={env.name} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureView2;
|
@ -0,0 +1,12 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
environmentContainer: {
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#fff',
|
||||
display: 'flex',
|
||||
padding: '1.5rem',
|
||||
},
|
||||
}));
|
@ -0,0 +1,16 @@
|
||||
import { Switch } from '@material-ui/core';
|
||||
import { useStyles } from './FeatureViewEnvironment.styles';
|
||||
|
||||
const FeatureViewEnvironment = ({ env }: any) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div className={styles.environmentContainer}>
|
||||
<Switch value={env.enabled} checked={env.enabled} /> Toggle in{' '}
|
||||
{env.name} is {env.enabled ? 'enabled' : 'disabled'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureViewEnvironment;
|
@ -0,0 +1,58 @@
|
||||
import { capitalize, IconButton } from '@material-ui/core';
|
||||
import classnames from 'classnames';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useCommonStyles } from '../../../../common.styles';
|
||||
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import { getFeatureTypeIcons } from '../../../../utils/get-feature-type-icons';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import { useStyles } from './FeatureViewMetadata.styles';
|
||||
|
||||
import { Edit } from '@material-ui/icons';
|
||||
|
||||
const FeatureViewMetaData = () => {
|
||||
const styles = useStyles();
|
||||
const commonStyles = useCommonStyles();
|
||||
const { projectId, featureId } = useParams();
|
||||
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
|
||||
const { project, description, type } = feature;
|
||||
|
||||
const IconComponent = getFeatureTypeIcons(type);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
styles.container,
|
||||
commonStyles.contentSpacingY
|
||||
)}
|
||||
>
|
||||
<span className={styles.metaDataHeader}>
|
||||
<IconComponent className={styles.headerIcon} />{' '}
|
||||
{capitalize(type || '')} toggle
|
||||
</span>
|
||||
<span>Project: {project}</span>
|
||||
<ConditionallyRender
|
||||
condition
|
||||
show={
|
||||
<span>
|
||||
Description: {description}{' '}
|
||||
<IconButton>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</span>
|
||||
}
|
||||
elseShow={
|
||||
<span>
|
||||
No description.{' '}
|
||||
<IconButton>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureViewMetaData;
|
@ -0,0 +1,22 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '1.5rem',
|
||||
maxWidth: '350px',
|
||||
minWidth: '350px',
|
||||
marginRight: '1rem',
|
||||
},
|
||||
metaDataHeader: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerIcon: {
|
||||
marginRight: '1rem',
|
||||
fill: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
@ -38,7 +38,7 @@ const MainLayout = ({ children, location, uiConfig }) => {
|
||||
<Grid item className={styles.content} xs={12} sm={12}>
|
||||
<div
|
||||
className={muiStyles.contentContainer}
|
||||
style={{ zIndex: '100' }}
|
||||
style={{ zIndex: '200' }}
|
||||
>
|
||||
<BreadcrumbNav />
|
||||
<Proclamation toast={uiConfig.toast} />
|
||||
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/styles';
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
footer: {
|
||||
background: theme.palette.footer.background,
|
||||
padding: '2.5rem 4rem',
|
||||
padding: '2rem 4rem',
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
zIndex: 100,
|
||||
|
@ -96,6 +96,7 @@ const Header = () => {
|
||||
show={<Link to="/projects">Projects</Link>}
|
||||
/>
|
||||
<Link to="/features">Feature toggles</Link>
|
||||
<Link to="/reporting">Reporting</Link>
|
||||
|
||||
<button
|
||||
className={styles.advancedNavButton}
|
||||
@ -103,7 +104,7 @@ const Header = () => {
|
||||
setAnchorElAdvanced(e.currentTarget)
|
||||
}
|
||||
>
|
||||
Advanced
|
||||
Configure
|
||||
<KeyboardArrowDown />
|
||||
</button>
|
||||
<NavigationMenu
|
||||
|
@ -50,6 +50,27 @@ Array [
|
||||
"title": "Strategies",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/environments",
|
||||
"path": "/environments/create",
|
||||
"title": "Environments",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "E",
|
||||
"layout": "main",
|
||||
"menu": Object {
|
||||
"advanced": true,
|
||||
"mobile": true,
|
||||
},
|
||||
"path": "/environments",
|
||||
"title": "Environments",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
@ -181,6 +202,16 @@ Array [
|
||||
"title": "Copy",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flags": "E",
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/projects",
|
||||
"path": "/projects/:projectId/features2/:featureId",
|
||||
"title": "FeatureView2",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
@ -292,7 +323,6 @@ Array [
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
"menu": Object {
|
||||
"advanced": true,
|
||||
"mobile": true,
|
||||
},
|
||||
"path": "/reporting",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { baseRoutes, getRoute } from '../routes';
|
||||
|
||||
test('returns all baseRoutes', () => {
|
||||
expect(baseRoutes).toHaveLength(35);
|
||||
expect(baseRoutes).toHaveLength(38);
|
||||
expect(baseRoutes).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -32,7 +32,7 @@ import AdminInvoice from '../../page/admin/invoice';
|
||||
import AdminAuth from '../../page/admin/auth';
|
||||
import Reporting from '../../page/reporting';
|
||||
import Login from '../user/Login';
|
||||
import { P, C } from '../common/flags';
|
||||
import { P, C, E } from '../common/flags';
|
||||
import NewUser from '../user/NewUser';
|
||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||
@ -40,6 +40,9 @@ import ProjectListNew from '../project/ProjectList/ProjectList';
|
||||
import Project from '../project/Project/Project';
|
||||
import RedirectFeatureViewPage from '../../page/features/redirect';
|
||||
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
|
||||
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
|
||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
|
||||
|
||||
export const routes = [
|
||||
// Features
|
||||
@ -88,6 +91,24 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: { mobile: true, advanced: true },
|
||||
},
|
||||
{
|
||||
path: '/environments/create',
|
||||
title: 'Environments',
|
||||
component: CreateEnvironment,
|
||||
parent: '/environments',
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/environments',
|
||||
title: 'Environments',
|
||||
component: EnvironmentList,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
flag: E,
|
||||
menu: { mobile: true, advanced: true },
|
||||
},
|
||||
|
||||
// History
|
||||
{
|
||||
@ -221,6 +242,16 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/features2/:featureId',
|
||||
parent: '/projects',
|
||||
title: 'FeatureView2',
|
||||
component: FeatureView2,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
flags: E,
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/features/:name/:activeTab',
|
||||
parent: '/projects',
|
||||
@ -339,7 +370,7 @@ export const routes = [
|
||||
component: Reporting,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: { mobile: true, advanced: true },
|
||||
menu: { mobile: true },
|
||||
},
|
||||
// Admin
|
||||
{
|
||||
@ -427,8 +458,7 @@ export const routes = [
|
||||
|
||||
export const getRoute = path => routes.find(route => route.path === path);
|
||||
|
||||
export const baseRoutes = routes
|
||||
.filter(route => !route.hidden)
|
||||
export const baseRoutes = routes.filter(route => !route.hidden);
|
||||
|
||||
const computeRoutes = () => {
|
||||
const mainNavRoutes = baseRoutes.filter(route => route.menu.advanced);
|
||||
|
@ -55,9 +55,12 @@ const useAPI = ({
|
||||
|
||||
const makeRequest = async (
|
||||
apiCaller: any,
|
||||
requestId?: string
|
||||
requestId?: string,
|
||||
loading: boolean = true
|
||||
): Promise<Response> => {
|
||||
setLoading(true);
|
||||
if (loading) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const res = await apiCaller();
|
||||
setLoading(false);
|
||||
|
@ -0,0 +1,148 @@
|
||||
import {
|
||||
IEnvironmentPayload,
|
||||
ISortOrderPayload,
|
||||
IEnvironmentEditPayload,
|
||||
} from '../../../../interfaces/environments';
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
const useEnvironmentApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const validateEnvName = async (envName: string) => {
|
||||
const path = `api/admin/environments/validate`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'POST', body: JSON.stringify({ name: envName }) },
|
||||
'validateEnvName'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id, false);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const createEnvironment = async (payload: IEnvironmentPayload) => {
|
||||
const path = `api/admin/environments`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'POST', body: JSON.stringify(payload) },
|
||||
'createEnvironment'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEnvironment = async (name: string) => {
|
||||
const path = `api/admin/environments/${name}`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'DELETE' },
|
||||
'deleteEnvironment'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const updateEnvironment = async (
|
||||
name: string,
|
||||
payload: IEnvironmentEditPayload
|
||||
) => {
|
||||
const path = `api/admin/environments/update/${name}`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'PUT', body: JSON.stringify(payload) },
|
||||
'updateEnvironment'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const changeSortOrder = async (payload: ISortOrderPayload) => {
|
||||
const path = `api/admin/environments/sort-order`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'PUT', body: JSON.stringify(payload) },
|
||||
'changeSortOrder'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEnvironmentOn = async (name: string) => {
|
||||
const path = `api/admin/environments/${name}/on`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'POST' },
|
||||
'toggleEnvironmentOn'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEnvironmentOff = async (name: string) => {
|
||||
const path = `api/admin/environments/${name}/off`;
|
||||
const req = createRequest(
|
||||
path,
|
||||
{ method: 'POST' },
|
||||
'toggleEnvironmentOff'
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
validateEnvName,
|
||||
createEnvironment,
|
||||
errors,
|
||||
loading,
|
||||
deleteEnvironment,
|
||||
updateEnvironment,
|
||||
changeSortOrder,
|
||||
toggleEnvironmentOff,
|
||||
toggleEnvironmentOn,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEnvironmentApi;
|
@ -0,0 +1,38 @@
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { IEnvironmentResponse } from '../../../../interfaces/environments';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
|
||||
export const ENVIRONMENT_CACHE_KEY = `api/admin/environments`;
|
||||
|
||||
const useEnvironments = () => {
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(`api/admin/environments`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
}).then(res => res.json());
|
||||
};
|
||||
|
||||
const { data, error } = useSWR<IEnvironmentResponse>(
|
||||
ENVIRONMENT_CACHE_KEY,
|
||||
fetcher
|
||||
);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(ENVIRONMENT_CACHE_KEY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
environments: data?.environments || [],
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEnvironments;
|
14
frontend/src/hooks/api/getters/useFeature/defaultFeature.ts
Normal file
14
frontend/src/hooks/api/getters/useFeature/defaultFeature.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
|
||||
|
||||
export const defaultFeature: IFeatureToggle = {
|
||||
environments: [],
|
||||
name: '',
|
||||
type: '',
|
||||
stale: false,
|
||||
archived: false,
|
||||
createdAt: '',
|
||||
lastSeenAt: '',
|
||||
project: '',
|
||||
variants: [],
|
||||
description: '',
|
||||
};
|
46
frontend/src/hooks/api/getters/useFeature/useFeature.ts
Normal file
46
frontend/src/hooks/api/getters/useFeature/useFeature.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
|
||||
import { defaultFeature } from './defaultFeature';
|
||||
|
||||
const useFeature = (projectId: string, id: string) => {
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(
|
||||
`api/admin/projects/${projectId}/features/${id}`
|
||||
);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
}).then(res => res.json());
|
||||
};
|
||||
|
||||
const KEY = `api/admin/projects/${projectId}/features/${id}`;
|
||||
|
||||
const { data, error } = useSWR<IFeatureToggle>(KEY, fetcher);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(KEY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
let feature = defaultFeature;
|
||||
if (data) {
|
||||
if (data.environments) {
|
||||
feature = data;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
feature,
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFeature;
|
@ -5,7 +5,7 @@ export const defaultValue = {
|
||||
version: '3.x',
|
||||
environment: '',
|
||||
slogan: 'The enterprise ready feature toggle service.',
|
||||
flags: { P: false, C: false },
|
||||
flags: { P: false, C: false, E: false },
|
||||
links: [
|
||||
{
|
||||
value: 'Documentation',
|
||||
|
29
frontend/src/interfaces/environments.ts
Normal file
29
frontend/src/interfaces/environments.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export interface IEnvironment {
|
||||
name: string;
|
||||
type: string;
|
||||
createdAt: string;
|
||||
displayName: string;
|
||||
sortOrder: number;
|
||||
enabled: boolean;
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export interface IEnvironmentPayload {
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IEnvironmentEditPayload {
|
||||
sortOrder: number;
|
||||
displayName: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IEnvironmentResponse {
|
||||
environments: IEnvironment[];
|
||||
}
|
||||
|
||||
export interface ISortOrderPayload {
|
||||
[index: string]: number;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { IStrategy } from './strategy';
|
||||
|
||||
export interface IFeatureToggleListItem {
|
||||
type: string;
|
||||
name: string;
|
||||
@ -9,3 +11,41 @@ export interface IEnvironments {
|
||||
displayName: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface IFeatureToggle {
|
||||
stale: boolean;
|
||||
archived: boolean;
|
||||
createdAt: string;
|
||||
lastSeenAt: string;
|
||||
description: string;
|
||||
environments: IFeatureEnvironment[];
|
||||
name: string;
|
||||
project: string;
|
||||
type: string;
|
||||
variants: IFeatureVariant[];
|
||||
}
|
||||
|
||||
export interface IFeatureEnvironment {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
strategies: IStrategy[];
|
||||
}
|
||||
|
||||
export interface IFeatureVariant {
|
||||
name: string;
|
||||
stickiness: string;
|
||||
weight: number;
|
||||
weightType: string;
|
||||
overrides: IOverride[];
|
||||
payload?: IPayload;
|
||||
}
|
||||
|
||||
export interface IOverride {
|
||||
contextName: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface IPayload {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
19
frontend/src/interfaces/strategy.ts
Normal file
19
frontend/src/interfaces/strategy.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface IStrategy {
|
||||
constraints: IConstraint[];
|
||||
id: string;
|
||||
name: string;
|
||||
parameters: IParameter;
|
||||
}
|
||||
|
||||
export interface IConstraint {
|
||||
values: string[];
|
||||
operator: string;
|
||||
contextName: string;
|
||||
}
|
||||
|
||||
export interface IParameter {
|
||||
groupId?: string;
|
||||
rollout?: number;
|
||||
stickiness?: string;
|
||||
[index: string]: any;
|
||||
}
|
@ -101,6 +101,7 @@ const theme = createMuiTheme({
|
||||
fontSizes: {
|
||||
mainHeader: '1.2rem',
|
||||
subHeader: '1.1rem',
|
||||
bodySize: '1rem',
|
||||
},
|
||||
boxShadows: {
|
||||
chip: {
|
||||
|
Loading…
Reference in New Issue
Block a user