mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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 UPDATE_API_TOKEN = 'UPDATE_API_TOKEN';
|
||||||
export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
||||||
export const DELETE_API_TOKEN = 'DELETE_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,
|
subtitle,
|
||||||
variant,
|
variant,
|
||||||
loading,
|
loading,
|
||||||
className,
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const headerClasses = classnames({ skeleton: loading });
|
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 P = 'P';
|
||||||
export const C = 'C';
|
export const C = 'C';
|
||||||
|
export const E = 'E';
|
||||||
export const RBAC = 'RBAC';
|
export const RBAC = 'RBAC';
|
||||||
export const PROJECTFILTERING = false;
|
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}>
|
<Grid item className={styles.content} xs={12} sm={12}>
|
||||||
<div
|
<div
|
||||||
className={muiStyles.contentContainer}
|
className={muiStyles.contentContainer}
|
||||||
style={{ zIndex: '100' }}
|
style={{ zIndex: '200' }}
|
||||||
>
|
>
|
||||||
<BreadcrumbNav />
|
<BreadcrumbNav />
|
||||||
<Proclamation toast={uiConfig.toast} />
|
<Proclamation toast={uiConfig.toast} />
|
||||||
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/styles';
|
|||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
footer: {
|
footer: {
|
||||||
background: theme.palette.footer.background,
|
background: theme.palette.footer.background,
|
||||||
padding: '2.5rem 4rem',
|
padding: '2rem 4rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
|
@ -96,6 +96,7 @@ const Header = () => {
|
|||||||
show={<Link to="/projects">Projects</Link>}
|
show={<Link to="/projects">Projects</Link>}
|
||||||
/>
|
/>
|
||||||
<Link to="/features">Feature toggles</Link>
|
<Link to="/features">Feature toggles</Link>
|
||||||
|
<Link to="/reporting">Reporting</Link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={styles.advancedNavButton}
|
className={styles.advancedNavButton}
|
||||||
@ -103,7 +104,7 @@ const Header = () => {
|
|||||||
setAnchorElAdvanced(e.currentTarget)
|
setAnchorElAdvanced(e.currentTarget)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Advanced
|
Configure
|
||||||
<KeyboardArrowDown />
|
<KeyboardArrowDown />
|
||||||
</button>
|
</button>
|
||||||
<NavigationMenu
|
<NavigationMenu
|
||||||
|
@ -50,6 +50,27 @@ Array [
|
|||||||
"title": "Strategies",
|
"title": "Strategies",
|
||||||
"type": "protected",
|
"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 {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
@ -181,6 +202,16 @@ Array [
|
|||||||
"title": "Copy",
|
"title": "Copy",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"flags": "E",
|
||||||
|
"layout": "main",
|
||||||
|
"menu": Object {},
|
||||||
|
"parent": "/projects",
|
||||||
|
"path": "/projects/:projectId/features2/:featureId",
|
||||||
|
"title": "FeatureView2",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
@ -292,7 +323,6 @@ Array [
|
|||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"menu": Object {
|
"menu": Object {
|
||||||
"advanced": true,
|
|
||||||
"mobile": true,
|
"mobile": true,
|
||||||
},
|
},
|
||||||
"path": "/reporting",
|
"path": "/reporting",
|
||||||
|
@ -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(35);
|
expect(baseRoutes).toHaveLength(38);
|
||||||
expect(baseRoutes).toMatchSnapshot();
|
expect(baseRoutes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ import AdminInvoice from '../../page/admin/invoice';
|
|||||||
import AdminAuth from '../../page/admin/auth';
|
import AdminAuth from '../../page/admin/auth';
|
||||||
import Reporting from '../../page/reporting';
|
import Reporting from '../../page/reporting';
|
||||||
import Login from '../user/Login';
|
import Login from '../user/Login';
|
||||||
import { P, C } from '../common/flags';
|
import { P, C, E } from '../common/flags';
|
||||||
import NewUser from '../user/NewUser';
|
import NewUser from '../user/NewUser';
|
||||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||||
@ -40,6 +40,9 @@ import ProjectListNew from '../project/ProjectList/ProjectList';
|
|||||||
import Project from '../project/Project/Project';
|
import Project from '../project/Project/Project';
|
||||||
import RedirectFeatureViewPage from '../../page/features/redirect';
|
import RedirectFeatureViewPage from '../../page/features/redirect';
|
||||||
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
|
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 = [
|
export const routes = [
|
||||||
// Features
|
// Features
|
||||||
@ -88,6 +91,24 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: { mobile: true, advanced: true },
|
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
|
// History
|
||||||
{
|
{
|
||||||
@ -221,6 +242,16 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
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',
|
path: '/projects/:id/features/:name/:activeTab',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
@ -339,7 +370,7 @@ export const routes = [
|
|||||||
component: Reporting,
|
component: Reporting,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true },
|
||||||
},
|
},
|
||||||
// Admin
|
// Admin
|
||||||
{
|
{
|
||||||
@ -427,8 +458,7 @@ export const routes = [
|
|||||||
|
|
||||||
export const getRoute = path => routes.find(route => route.path === path);
|
export const getRoute = path => routes.find(route => route.path === path);
|
||||||
|
|
||||||
export const baseRoutes = routes
|
export const baseRoutes = routes.filter(route => !route.hidden);
|
||||||
.filter(route => !route.hidden)
|
|
||||||
|
|
||||||
const computeRoutes = () => {
|
const computeRoutes = () => {
|
||||||
const mainNavRoutes = baseRoutes.filter(route => route.menu.advanced);
|
const mainNavRoutes = baseRoutes.filter(route => route.menu.advanced);
|
||||||
|
@ -55,9 +55,12 @@ const useAPI = ({
|
|||||||
|
|
||||||
const makeRequest = async (
|
const makeRequest = async (
|
||||||
apiCaller: any,
|
apiCaller: any,
|
||||||
requestId?: string
|
requestId?: string,
|
||||||
|
loading: boolean = true
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
setLoading(true);
|
if (loading) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await apiCaller();
|
const res = await apiCaller();
|
||||||
setLoading(false);
|
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',
|
version: '3.x',
|
||||||
environment: '',
|
environment: '',
|
||||||
slogan: 'The enterprise ready feature toggle service.',
|
slogan: 'The enterprise ready feature toggle service.',
|
||||||
flags: { P: false, C: false },
|
flags: { P: false, C: false, E: false },
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
value: 'Documentation',
|
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 {
|
export interface IFeatureToggleListItem {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -9,3 +11,41 @@ export interface IEnvironments {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
enabled: boolean;
|
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: {
|
fontSizes: {
|
||||||
mainHeader: '1.2rem',
|
mainHeader: '1.2rem',
|
||||||
subHeader: '1.1rem',
|
subHeader: '1.1rem',
|
||||||
|
bodySize: '1rem',
|
||||||
},
|
},
|
||||||
boxShadows: {
|
boxShadows: {
|
||||||
chip: {
|
chip: {
|
||||||
|
Loading…
Reference in New Issue
Block a user