mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
Feat/toggle view (#389)
* feat: toggle view * fix: navigation * eat: toggle view * fix: resolve lint * fix: remove console logs * fix: reimplement feature validation
This commit is contained in:
parent
fe2a8311bf
commit
47579e2616
@ -13,6 +13,10 @@ body {
|
|||||||
font-family: 'Sen', sans-serif;
|
font-family: 'Sen', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: 'Sen', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.MuiButton-root {
|
.MuiButton-root {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
borderRadius: '12.5px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '2rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,19 @@
|
|||||||
|
import { useParams } from 'react-router';
|
||||||
|
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
|
import { useStyles } from './FeatureLog.styles';
|
||||||
|
import { IFeatureViewParams } from '../../../../interfaces/params';
|
||||||
|
import HistoryComponent from '../../../history/FeatureEventHistory';
|
||||||
|
|
||||||
|
const FeatureLog = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
|
const { feature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<HistoryComponent toggleName={feature.name} />;
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureLog;
|
@ -0,0 +1,9 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
borderRadius: '12.5px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '2rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,19 @@
|
|||||||
|
import { useParams } from 'react-router';
|
||||||
|
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
|
import MetricComponent from '../../view/metric-container';
|
||||||
|
import { useStyles } from './FeatureMetrics.styles';
|
||||||
|
import { IFeatureViewParams } from '../../../../interfaces/params';
|
||||||
|
|
||||||
|
const FeatureMetrics = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
|
const { feature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<MetricComponent featureToggle={feature} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureMetrics;
|
@ -0,0 +1,10 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: { display: 'flex', width: '100%' },
|
||||||
|
mainContent: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,21 @@
|
|||||||
|
import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
|
||||||
|
import FeatureOverviewStrategies from './FeatureOverviewStrategies/FeatureOverviewStrategies';
|
||||||
|
import { useStyles } from './FeatureOverview.styles';
|
||||||
|
import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
|
||||||
|
|
||||||
|
const FeatureOverview = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.sidebar}>
|
||||||
|
<FeatureViewMetaData />
|
||||||
|
<FeatureOverviewTags />
|
||||||
|
</div>
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<FeatureOverviewStrategies />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverview;
|
@ -0,0 +1,77 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
marginBottom: '2rem',
|
||||||
|
border: `1px solid ${theme.palette.grey[300]}`,
|
||||||
|
borderRadius: '5px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||||
|
padding: '1rem',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fill: '#fff',
|
||||||
|
height: '17.5px',
|
||||||
|
width: '17.5px',
|
||||||
|
},
|
||||||
|
strategiesContainer: {
|
||||||
|
padding: '1rem 0',
|
||||||
|
['& > *']: {
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
environmentIdentifier: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '42.5%',
|
||||||
|
top: '-25px',
|
||||||
|
display: 'flex',
|
||||||
|
background: theme.palette.primary.light,
|
||||||
|
borderRadius: '25px',
|
||||||
|
padding: '0.4rem 1rem',
|
||||||
|
minWidth: '150px',
|
||||||
|
color: '#fff',
|
||||||
|
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
environmentBadgeParagraph: {
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
padding: '0.25rem',
|
||||||
|
borderRadius: '50%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
border: '1px solid #fff',
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: '1rem',
|
||||||
|
},
|
||||||
|
disabledEnvContainer: {
|
||||||
|
backgroundColor: theme.palette.grey[300],
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
disabledIconContainer: {
|
||||||
|
border: `1px solid ${theme.palette.grey[500]}`,
|
||||||
|
},
|
||||||
|
iconDisabled: {
|
||||||
|
fill: theme.palette.grey[500],
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleText: {
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
toggleLink: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
headerDisabledEnv: {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,173 @@
|
|||||||
|
import { Cloud } from '@material-ui/icons';
|
||||||
|
import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle';
|
||||||
|
import { Switch } from '@material-ui/core';
|
||||||
|
import { useStyles } from './FeatureOverviewEnvironment.styles';
|
||||||
|
import FeatureOverviewStrategyCard from './FeatureOverviewStrategyCard/FeatureOverviewStrategyCard';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||||
|
import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
|
import { useHistory, useParams, Link } from 'react-router-dom';
|
||||||
|
import { IFeatureViewParams } from '../../../../../../interfaces/params';
|
||||||
|
import useToast from '../../../../../../hooks/useToast';
|
||||||
|
|
||||||
|
interface IFeatureOverviewEnvironmentProps {
|
||||||
|
env: IFeatureEnvironment;
|
||||||
|
refetch: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureOverviewEnvironment = ({
|
||||||
|
env,
|
||||||
|
refetch,
|
||||||
|
}: IFeatureOverviewEnvironmentProps) => {
|
||||||
|
const { featureId, projectId } = useParams<IFeatureViewParams>();
|
||||||
|
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||||
|
useFeatureApi();
|
||||||
|
const styles = useStyles();
|
||||||
|
const { toast, setToastData } = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
console.log(env);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
history.push(
|
||||||
|
`/projects/${projectId}/features2/${featureId}/strategies?environment=${env.name}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStrategies = () => {
|
||||||
|
const { strategies } = env;
|
||||||
|
|
||||||
|
return strategies.map(strategy => {
|
||||||
|
return (
|
||||||
|
<FeatureOverviewStrategyCard
|
||||||
|
strategy={strategy}
|
||||||
|
key={strategy.id}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEnvironmentOn = async () => {
|
||||||
|
try {
|
||||||
|
await toggleFeatureEnvironmentOn(projectId, featureId, env.name);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
show: true,
|
||||||
|
text: 'Successfully turned environment on.',
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
} catch (e) {
|
||||||
|
setToastData({
|
||||||
|
show: true,
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEnvironmentOff = async () => {
|
||||||
|
try {
|
||||||
|
await toggleFeatureEnvironmentOff(projectId, featureId, env.name);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
show: true,
|
||||||
|
text: 'Successfully turned environment off.',
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
} catch (e) {
|
||||||
|
setToastData({
|
||||||
|
show: true,
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEnvironment = (e: React.ChangeEvent) => {
|
||||||
|
if (env.enabled) {
|
||||||
|
handleToggleEnvironmentOff();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleToggleEnvironmentOn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconContainerClasses = classNames(styles.iconContainer, {
|
||||||
|
[styles.disabledIconContainer]: !env.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconClasses = classNames(styles.icon, {
|
||||||
|
[styles.iconDisabled]: !env.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerClasses = classNames(styles.header, {
|
||||||
|
[styles.headerDisabledEnv]: !env.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentIdentifierClasses = classNames(
|
||||||
|
styles.environmentIdentifier,
|
||||||
|
{ [styles.disabledEnvContainer]: !env.enabled }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={environmentIdentifierClasses}>
|
||||||
|
<div className={iconContainerClasses}>
|
||||||
|
<Cloud className={iconClasses} />
|
||||||
|
</div>
|
||||||
|
<p className={styles.environmentBadgeParagraph}>{env.type}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={headerClasses}>
|
||||||
|
<div className={styles.headerInfo}>
|
||||||
|
<p className={styles.environmentTitle}>{env.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.environmentStatus}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={env.strategies.length > 0}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
value={env.enabled}
|
||||||
|
checked={env.enabled}
|
||||||
|
onChange={toggleEnvironment}
|
||||||
|
/>{' '}
|
||||||
|
<span className={styles.toggleText}>
|
||||||
|
This environment is{' '}
|
||||||
|
{env.enabled ? 'enabled' : 'disabled'}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<p className={styles.toggleText}>
|
||||||
|
No strategies configured for environment.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to={`/projects/${projectId}/features2/${featureId}/strategies?addStrategy=true&environment=${env.name}`}
|
||||||
|
className={styles.toggleLink}
|
||||||
|
>
|
||||||
|
Configure strategies for {env.name}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={env.enabled}
|
||||||
|
show={
|
||||||
|
<div className={styles.body}>
|
||||||
|
<div className={styles.strategiesContainer}>
|
||||||
|
{renderStrategies()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{toast}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverviewEnvironment;
|
@ -0,0 +1,36 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
card: {
|
||||||
|
border: `1px solid ${theme.palette.grey[300]}`,
|
||||||
|
borderRadius: '5px',
|
||||||
|
transition: 'transform 0.3s ease',
|
||||||
|
transitionDelay: '0.1s',
|
||||||
|
position: 'relative',
|
||||||
|
background: 'transparent',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.75rem',
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
},
|
||||||
|
cardHeader: {
|
||||||
|
maxWidth: '200px',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
[theme.breakpoints.down(700)]: {
|
||||||
|
maxWidth: '100px',
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
fill: theme.palette.primary.main,
|
||||||
|
minWidth: '35px',
|
||||||
|
},
|
||||||
|
rollout: {
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,42 @@
|
|||||||
|
import { useMediaQuery } from '@material-ui/core';
|
||||||
|
import { IFeatureStrategy } from '../../../../../../../interfaces/strategy';
|
||||||
|
import {
|
||||||
|
getFeatureStrategyIcon,
|
||||||
|
getHumanReadbleStrategyName,
|
||||||
|
} from '../../../../../../../utils/strategy-names';
|
||||||
|
import ConditionallyRender from '../../../../../../common/ConditionallyRender';
|
||||||
|
import { useStyles } from './FeatureOverviewStrategyCard.styles';
|
||||||
|
|
||||||
|
interface IFeatureOverviewStrategyCardProps {
|
||||||
|
strategy: IFeatureStrategy;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureOverviewStrategyCard = ({
|
||||||
|
strategy,
|
||||||
|
onClick,
|
||||||
|
}: IFeatureOverviewStrategyCardProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const smallScreen = useMediaQuery('(max-width:500px)');
|
||||||
|
const strategyName = getHumanReadbleStrategyName(strategy.name);
|
||||||
|
const Icon = getFeatureStrategyIcon(strategy.name);
|
||||||
|
|
||||||
|
const { parameters } = strategy;
|
||||||
|
return (
|
||||||
|
<button className={styles.card} onClick={onClick}>
|
||||||
|
<Icon className={styles.icon} />
|
||||||
|
<p className={styles.cardHeader}>{strategyName}</p>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(parameters?.rollout) && !smallScreen}
|
||||||
|
show={
|
||||||
|
<p className={styles.rollout}>
|
||||||
|
Rolling out to {parameters?.rollout}%
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverviewStrategyCard;
|
@ -0,0 +1,27 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
borderRadius: '12.5px',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
headerContainer: {
|
||||||
|
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: theme.fontSizes.subHeader,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
headerInnerContainer: {
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
actions: { position: 'relative' },
|
||||||
|
bodyContainer: {
|
||||||
|
padding: '3rem 2rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,54 @@
|
|||||||
|
import { Add } from '@material-ui/icons';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
|
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||||
|
import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import FeatureOverviewEnvironment from './FeatureOverviewEnvironment/FeatureOverviewEnvironment';
|
||||||
|
import { useStyles } from './FeatureOverviewStrategies.styles';
|
||||||
|
|
||||||
|
const FeatureOverviewStrategies = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
|
const { feature, refetch } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
if (!feature) return null;
|
||||||
|
|
||||||
|
const { environments } = feature;
|
||||||
|
|
||||||
|
const renderEnvironments = () => {
|
||||||
|
return environments?.map(env => {
|
||||||
|
return (
|
||||||
|
<FeatureOverviewEnvironment
|
||||||
|
env={env}
|
||||||
|
key={env.name}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<div className={styles.headerInnerContainer}>
|
||||||
|
<h3 className={styles.headerTitle}>Toggle Strategies</h3>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<ResponsiveButton
|
||||||
|
maxWidth="700px"
|
||||||
|
Icon={Add}
|
||||||
|
className={styles.addStrategyButton}
|
||||||
|
component={Link}
|
||||||
|
to={`/projects/${projectId}/features2/${featureId}/strategies?addStrategy=true`}
|
||||||
|
>
|
||||||
|
Add new strategy
|
||||||
|
</ResponsiveButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.bodyContainer}>{renderEnvironments()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverviewStrategies;
|
@ -0,0 +1,40 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
borderRadius: '10px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
|
||||||
|
maxWidth: '350px',
|
||||||
|
minWidth: '350px',
|
||||||
|
marginRight: '1rem',
|
||||||
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
|
tagheaderContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '1.5rem',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||||
|
},
|
||||||
|
tagHeader: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
height: '40px',
|
||||||
|
width: '40px',
|
||||||
|
fill: theme.palette.primary.main,
|
||||||
|
marginRight: '0.8rem',
|
||||||
|
},
|
||||||
|
tagHeaderText: {
|
||||||
|
fontSize: theme.fontSizes.subHeader,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
tagContent: {
|
||||||
|
padding: '1.5rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,101 @@
|
|||||||
|
import { Chip } from '@material-ui/core';
|
||||||
|
import { Label } from '@material-ui/icons';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import useTags from '../../../../../hooks/api/getters/useTags/useTags';
|
||||||
|
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||||
|
import { useStyles } from './FeatureOverviewTags.styles';
|
||||||
|
|
||||||
|
import slackIcon from '../../../../../assets/icons/slack.svg';
|
||||||
|
import jiraIcon from '../../../../../assets/icons/jira.svg';
|
||||||
|
import webhookIcon from '../../../../../assets/icons/webhooks.svg';
|
||||||
|
import { formatAssetPath } from '../../../../../utils/format-path';
|
||||||
|
import useTagTypes from '../../../../../hooks/api/getters/useTagTypes/useTagTypes';
|
||||||
|
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
|
import AddTagDialogContainer from '../../../add-tag-dialog-container';
|
||||||
|
|
||||||
|
const FeatureOverviewTags = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { featureId } = useParams<IFeatureViewParams>();
|
||||||
|
const { tags, refetch } = useTags(featureId);
|
||||||
|
const { tagTypes } = useTagTypes();
|
||||||
|
const { deleteTag } = useFeatureApi();
|
||||||
|
|
||||||
|
const handleDelete = async (type: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await deleteTag(featureId, type, value);
|
||||||
|
refetch();
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: Handle error
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagIcon = (typeName: string) => {
|
||||||
|
let tagType = tagTypes.find(type => type.name === typeName);
|
||||||
|
|
||||||
|
const style = { width: '20px', height: '20px', marginRight: '5px' };
|
||||||
|
|
||||||
|
if (tagType && tagType.icon) {
|
||||||
|
switch (tagType.name) {
|
||||||
|
case 'slack':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="slack"
|
||||||
|
src={formatAssetPath(slackIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'jira':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="jira"
|
||||||
|
src={formatAssetPath(jiraIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'webhook':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="webhook"
|
||||||
|
src={formatAssetPath(webhookIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <Label />;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return <span>{typeName[0].toUpperCase()}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTag = t => (
|
||||||
|
<Chip
|
||||||
|
icon={tagIcon(t.type)}
|
||||||
|
style={{ marginRight: '3px', fontSize: '0.8em' }}
|
||||||
|
label={t.value}
|
||||||
|
key={`${t.type}:${t.value}`}
|
||||||
|
onDelete={() => handleDelete(t.type, t.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.tagheaderContainer}>
|
||||||
|
<div className={styles.tagHeader}>
|
||||||
|
<Label className={styles.tag} />
|
||||||
|
<h4 className={styles.tagHeaderText}>Tags</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddTagDialogContainer featureToggleName={featureId} />
|
||||||
|
{/* <IconButton>
|
||||||
|
<Add />
|
||||||
|
</IconButton> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.tagContent}>{tags.map(renderTag)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverviewTags;
|
@ -0,0 +1,59 @@
|
|||||||
|
import { capitalize, IconButton } from '@material-ui/core';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
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';
|
||||||
|
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||||
|
|
||||||
|
const FeatureViewMetaData = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
|
|
||||||
|
const { feature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
const { project, description, type } = feature;
|
||||||
|
|
||||||
|
const IconComponent = getFeatureTypeIcons(type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames(styles.container)}>
|
||||||
|
<div className={styles.metaDataHeader}>
|
||||||
|
<IconComponent className={styles.headerIcon} />{' '}
|
||||||
|
<h3 className={styles.header}>
|
||||||
|
{capitalize(type || '')} toggle
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className={styles.body}>
|
||||||
|
<span className={styles.bodyItem}>Project: {project}</span>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition
|
||||||
|
show={
|
||||||
|
<span className={styles.bodyItem}>
|
||||||
|
<div>Description:</div>
|
||||||
|
<div className={styles.descriptionContainer}>
|
||||||
|
<p>{description}</p>
|
||||||
|
<IconButton>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<span>
|
||||||
|
No description.{' '}
|
||||||
|
<IconButton>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureViewMetaData;
|
@ -15,8 +15,29 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
header: {
|
||||||
|
fontSize: theme.fontSizes.subHeader,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
margin: '1rem 0',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
bodyItem: {
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
},
|
||||||
headerIcon: {
|
headerIcon: {
|
||||||
marginRight: '1rem',
|
marginRight: '1rem',
|
||||||
|
height: '40px',
|
||||||
|
width: '40px',
|
||||||
fill: theme.palette.primary.main,
|
fill: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
|
descriptionContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
}));
|
}));
|
@ -2,7 +2,7 @@ import { Button, useMediaQuery } from '@material-ui/core';
|
|||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { getHumanReadbleStrategyName } from '../../../../../../utils/strategy-names';
|
import { getHumanReadbleStrategyName } from '../../../../../../utils/strategy-names';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
|
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
|
||||||
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||||
@ -24,6 +24,7 @@ const FeatureStrategiesConfigure = ({
|
|||||||
setToastData,
|
setToastData,
|
||||||
}: IFeatureStrategiesConfigure) => {
|
}: IFeatureStrategiesConfigure) => {
|
||||||
const smallScreen = useMediaQuery('(max-width:900px)');
|
const smallScreen = useMediaQuery('(max-width:900px)');
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
const [productionGuard, setProductionGuard] = useState(false);
|
const [productionGuard, setProductionGuard] = useState(false);
|
||||||
@ -90,6 +91,7 @@ const FeatureStrategiesConfigure = ({
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
text: 'Successfully added strategy.',
|
text: 'Successfully added strategy.',
|
||||||
});
|
});
|
||||||
|
history.replace(history.location.pathname);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setToastData({
|
setToastData({
|
||||||
show: true,
|
show: true,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useContext, useEffect, useRef, useState } from 'react';
|
import { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
|
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
|
||||||
import useFeatureStrategyApi from '../../../../../../hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
import useFeatureStrategyApi from '../../../../../../hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
import useToast from '../../../../../../hooks/useToast';
|
import useToast from '../../../../../../hooks/useToast';
|
||||||
@ -11,7 +11,7 @@ const useFeatureStrategiesEnvironmentList = (
|
|||||||
strategies: IFeatureStrategy[]
|
strategies: IFeatureStrategy[]
|
||||||
) => {
|
) => {
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
|
const history = useHistory();
|
||||||
const { deleteStrategyFromFeature, updateStrategyOnFeature } =
|
const { deleteStrategyFromFeature, updateStrategyOnFeature } =
|
||||||
useFeatureStrategyApi();
|
useFeatureStrategyApi();
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ const useFeatureStrategiesEnvironmentList = (
|
|||||||
|
|
||||||
strategy.parameters = updateStrategyPayload.parameters;
|
strategy.parameters = updateStrategyPayload.parameters;
|
||||||
strategy.constraints = updateStrategyPayload.constraints;
|
strategy.constraints = updateStrategyPayload.constraints;
|
||||||
|
history.replace(history.location.pathname);
|
||||||
setFeatureCache(feature);
|
setFeatureCache(feature);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -109,6 +109,7 @@ const useFeatureStrategiesEnvironmentList = (
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
text: `Successfully deleted strategy from ${featureId}`,
|
text: `Successfully deleted strategy from ${featureId}`,
|
||||||
});
|
});
|
||||||
|
history.replace(history.location.pathname);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setToastData({
|
setToastData({
|
||||||
show: true,
|
show: true,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
import { useStyles } from './FeatureStrategiesEnvironments.styles';
|
import { useStyles } from './FeatureStrategiesEnvironments.styles';
|
||||||
import { Tabs, Tab, Button, useMediaQuery } from '@material-ui/core';
|
import { Tabs, Tab, Button, useMediaQuery } from '@material-ui/core';
|
||||||
@ -21,10 +21,12 @@ import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButt
|
|||||||
import { Add } from '@material-ui/icons';
|
import { Add } from '@material-ui/icons';
|
||||||
import AccessContext from '../../../../../contexts/AccessContext';
|
import AccessContext from '../../../../../contexts/AccessContext';
|
||||||
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
|
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
|
||||||
|
import useQueryParams from '../../../../../hooks/useQueryParams';
|
||||||
|
|
||||||
const FeatureStrategiesEnvironments = () => {
|
const FeatureStrategiesEnvironments = () => {
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const startingTabId = 0;
|
const startingTabId = 0;
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
@ -32,6 +34,10 @@ const FeatureStrategiesEnvironments = () => {
|
|||||||
const [showRefreshPrompt, setShowRefreshPrompt] = useState(false);
|
const [showRefreshPrompt, setShowRefreshPrompt] = useState(false);
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const query = useQueryParams();
|
||||||
|
const addStrategy = query.get('addStrategy');
|
||||||
|
const environmentTab = query.get('environment');
|
||||||
|
|
||||||
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(startingTabId);
|
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(startingTabId);
|
||||||
const {
|
const {
|
||||||
setActiveEnvironment,
|
setActiveEnvironment,
|
||||||
@ -49,6 +55,28 @@ const FeatureStrategiesEnvironments = () => {
|
|||||||
refreshInterval: 5000,
|
refreshInterval: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addStrategy) {
|
||||||
|
setExpandedSidebar(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environmentTab) {
|
||||||
|
const env = feature.environments.find(
|
||||||
|
env => env.name === environmentTab
|
||||||
|
);
|
||||||
|
const index = feature.environments.findIndex(
|
||||||
|
env => env.name === environmentTab
|
||||||
|
);
|
||||||
|
if (index < 0 || !env) return;
|
||||||
|
setActiveEnvironment(env);
|
||||||
|
setActiveTab(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveEnvironment(feature?.environments[activeTabIdx]);
|
||||||
|
/*eslint-disable-next-line */
|
||||||
|
}, [feature]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!feature) return;
|
if (!feature) return;
|
||||||
if (featureCache === null || !featureCache.createdAt) {
|
if (featureCache === null || !featureCache.createdAt) {
|
||||||
@ -70,12 +98,6 @@ const FeatureStrategiesEnvironments = () => {
|
|||||||
/*eslint-disable-next-line */
|
/*eslint-disable-next-line */
|
||||||
}, [feature]);
|
}, [feature]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!feature?.environments?.length > 0) return;
|
|
||||||
setActiveEnvironment(feature?.environments[activeTabIdx]);
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, [feature]);
|
|
||||||
|
|
||||||
const renderTabs = () => {
|
const renderTabs = () => {
|
||||||
return featureCache?.environments?.map((env, index) => {
|
return featureCache?.environments?.map((env, index) => {
|
||||||
return (
|
return (
|
||||||
@ -363,6 +385,7 @@ const FeatureStrategiesEnvironments = () => {
|
|||||||
setActiveEnvironment(
|
setActiveEnvironment(
|
||||||
featureCache?.environments[tabId]
|
featureCache?.environments[tabId]
|
||||||
);
|
);
|
||||||
|
history.replace(history.location.pathname);
|
||||||
}}
|
}}
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
textColor="primary"
|
textColor="primary"
|
||||||
|
@ -9,6 +9,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
margin: '0.5rem 0',
|
margin: '0.5rem 0',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
cursor: 'pointer',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
'&:active': {
|
'&:active': {
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
borderRadius: '12.5px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: '2rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,20 @@
|
|||||||
|
import { useStyles } from './FeatureVariants.styles';
|
||||||
|
import { useHistory, useParams } from 'react-router';
|
||||||
|
import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
|
import { IFeatureViewParams } from '../../../../interfaces/params';
|
||||||
|
import EditVariants from '../../variant/update-variant-container';
|
||||||
|
|
||||||
|
const FeatureVariants = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
|
const { feature } = useFeature(projectId, featureId);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<EditVariants featureToggle={feature} history={history} editable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureVariants;
|
@ -1,64 +1,46 @@
|
|||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
import { useEffect } from 'react';
|
import { Route, useHistory, useParams } from 'react-router-dom';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
|
||||||
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
||||||
import useTabs from '../../../hooks/useTabs';
|
import useTabs from '../../../hooks/useTabs';
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
import { IFeatureViewParams } from '../../../interfaces/params';
|
||||||
import TabPanel from '../../common/TabNav/TabPanel';
|
import FeatureLog from './FeatureLog/FeatureLog';
|
||||||
|
import FeatureMetrics from './FeatureMetrics/FeatureMetrics';
|
||||||
|
import FeatureOverview from './FeatureOverview/FeatureOverview';
|
||||||
import FeatureStrategies from './FeatureStrategies/FeatureStrategies';
|
import FeatureStrategies from './FeatureStrategies/FeatureStrategies';
|
||||||
|
import FeatureVariants from './FeatureVariants/FeatureVariants';
|
||||||
import { useStyles } from './FeatureView2.styles';
|
import { useStyles } from './FeatureView2.styles';
|
||||||
import FeatureViewEnvironment from './FeatureViewEnvironment/FeatureViewEnvironment';
|
|
||||||
import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
|
|
||||||
|
|
||||||
const FeatureView2 = () => {
|
const FeatureView2 = () => {
|
||||||
const { projectId, featureId, activeTab } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const { feature } = useFeature(projectId, featureId);
|
||||||
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(0);
|
const { a11yProps } = useTabs(0);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const basePath = `/projects/${projectId}/features2/${featureId}`;
|
const basePath = `/projects/${projectId}/features2/${featureId}`;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const tabIdx = tabData.findIndex(tab => tab.name === activeTab);
|
|
||||||
setActiveTab(tabIdx);
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderOverview = () => {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', width: '100%' }}>
|
|
||||||
<FeatureViewMetaData />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{feature?.environments?.map(env => {
|
|
||||||
return (
|
|
||||||
<FeatureViewEnvironment env={env} key={env.name} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabData = [
|
const tabData = [
|
||||||
{
|
{
|
||||||
title: 'Overview',
|
title: 'Overview',
|
||||||
component: renderOverview(),
|
|
||||||
path: `${basePath}/overview`,
|
path: `${basePath}/overview`,
|
||||||
name: 'overview',
|
name: 'overview',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Strategies',
|
title: 'Strategies',
|
||||||
component: <FeatureStrategies />,
|
|
||||||
path: `${basePath}/strategies`,
|
path: `${basePath}/strategies`,
|
||||||
name: 'strategies',
|
name: 'strategies',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Metrics',
|
||||||
|
path: `${basePath}/metrics`,
|
||||||
|
name: 'Metrics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Event log',
|
||||||
|
path: `${basePath}/logs`,
|
||||||
|
name: 'Event log',
|
||||||
|
},
|
||||||
|
{ title: 'Variants', path: `${basePath}/variants`, name: 'Variants' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderTabs = () => {
|
const renderTabs = () => {
|
||||||
@ -67,9 +49,9 @@ const FeatureView2 = () => {
|
|||||||
<Tab
|
<Tab
|
||||||
key={tab.title}
|
key={tab.title}
|
||||||
label={tab.title}
|
label={tab.title}
|
||||||
|
value={tab.path}
|
||||||
{...a11yProps(index)}
|
{...a11yProps(index)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab(index);
|
|
||||||
history.push(tab.path);
|
history.push(tab.path);
|
||||||
}}
|
}}
|
||||||
className={styles.tabButton}
|
className={styles.tabButton}
|
||||||
@ -78,16 +60,6 @@ const FeatureView2 = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTabContent = () => {
|
|
||||||
return tabData.map((tab, index) => {
|
|
||||||
return (
|
|
||||||
<TabPanel value={activeTabIdx} index={index} key={tab.path}>
|
|
||||||
{tab.component}
|
|
||||||
</TabPanel>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@ -97,10 +69,7 @@ const FeatureView2 = () => {
|
|||||||
<div className={styles.separator} />
|
<div className={styles.separator} />
|
||||||
<div className={styles.tabContainer}>
|
<div className={styles.tabContainer}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTabIdx}
|
value={history.location.pathname}
|
||||||
onChange={(_, tabId) => {
|
|
||||||
setActiveTab(tabId);
|
|
||||||
}}
|
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
textColor="primary"
|
textColor="primary"
|
||||||
className={styles.tabNavigation}
|
className={styles.tabNavigation}
|
||||||
@ -109,7 +78,26 @@ const FeatureView2 = () => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderTabContent()}
|
<Route
|
||||||
|
path={`/projects/:projectId/features2/:featureId/overview`}
|
||||||
|
component={FeatureOverview}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`/projects/:projectId/features2/:featureId/strategies`}
|
||||||
|
component={FeatureStrategies}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`/projects/:projectId/features2/:featureId/metrics`}
|
||||||
|
component={FeatureMetrics}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`/projects/:projectId/features2/:featureId/logs`}
|
||||||
|
component={FeatureLog}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`/projects/:projectId/features2/:featureId/variants`}
|
||||||
|
component={FeatureVariants}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
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;
|
|
@ -6,6 +6,7 @@ import { updateWeight } from '../../common/util';
|
|||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
variants: ownProps.featureToggle.variants || [],
|
variants: ownProps.featureToggle.variants || [],
|
||||||
|
features: state.features.toJS(),
|
||||||
stickinessOptions: [
|
stickinessOptions: [
|
||||||
'default',
|
'default',
|
||||||
...state.context.filter(c => c.stickiness).map(c => c.name),
|
...state.context.filter(c => c.stickiness).map(c => c.name),
|
||||||
@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
|||||||
if (currentVariants.length > 0) {
|
if (currentVariants.length > 0) {
|
||||||
stickiness = currentVariants[0].stickiness || 'default';
|
stickiness = currentVariants[0].stickiness || 'default';
|
||||||
} else {
|
} else {
|
||||||
stickiness = 'default'
|
stickiness = 'default';
|
||||||
}
|
}
|
||||||
variant.stickiness = stickiness;
|
variant.stickiness = stickiness;
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ Array [
|
|||||||
"layout": "main",
|
"layout": "main",
|
||||||
"menu": Object {},
|
"menu": Object {},
|
||||||
"parent": "/projects",
|
"parent": "/projects",
|
||||||
"path": "/projects/:projectId/features2/:featureId/:activeTab",
|
"path": "/projects/:projectId/features2/:featureId",
|
||||||
"title": "FeatureView2",
|
"title": "FeatureView2",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
@ -222,7 +222,7 @@ export const routes = [
|
|||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/:projectId/features2/:featureId/:activeTab',
|
path: '/projects/:projectId/features2/:featureId',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
title: 'FeatureView2',
|
title: 'FeatureView2',
|
||||||
component: FeatureView2,
|
component: FeatureView2,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ITag } from '../../../../interfaces/tags';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
const useFeatureApi = () => {
|
const useFeatureApi = () => {
|
||||||
@ -5,12 +6,54 @@ const useFeatureApi = () => {
|
|||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleFeatureEnvironmentOn = async (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string
|
||||||
|
) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/on`;
|
||||||
|
const req = createRequest(
|
||||||
|
path,
|
||||||
|
{ method: 'POST' },
|
||||||
|
'toggleFeatureEnvironmentOn'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFeatureEnvironmentOff = async (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string
|
||||||
|
) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/off`;
|
||||||
|
const req = createRequest(
|
||||||
|
path,
|
||||||
|
{ method: 'POST' },
|
||||||
|
'toggleFeatureEnvironmentOff'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const changeFeatureProject = async (
|
const changeFeatureProject = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureName: string,
|
featureId: string,
|
||||||
newProjectId: string
|
newProjectId: string
|
||||||
) => {
|
) => {
|
||||||
const path = `api/admin/projects/${projectId}/features/${featureName}/changeProject`;
|
const path = `api/admin/projects/${projectId}/features/${featureId}/changeProject`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ newProjectId }),
|
body: JSON.stringify({ newProjectId }),
|
||||||
@ -25,7 +68,51 @@ const useFeatureApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { changeFeatureProject, errors };
|
const addTag = async (featureId: string, tag: ITag) => {
|
||||||
|
// TODO: Change this path to the new API when moved.
|
||||||
|
const path = `api/admin/features/${featureId}/tags`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ ...tag }),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTag = async (
|
||||||
|
featureId: string,
|
||||||
|
type: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
// TODO: Change this path to the new API when moved.
|
||||||
|
const path = `api/admin/features/${featureId}/tags/${type}/${value}`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeFeatureProject,
|
||||||
|
errors,
|
||||||
|
toggleFeatureEnvironmentOn,
|
||||||
|
toggleFeatureEnvironmentOff,
|
||||||
|
addTag,
|
||||||
|
deleteTag,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useFeatureApi;
|
export default useFeatureApi;
|
||||||
|
@ -15,7 +15,7 @@ interface IUseFeatureOptions {
|
|||||||
const useFeature = (
|
const useFeature = (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
id: string,
|
id: string,
|
||||||
options: IUseFeatureOptions
|
options: IUseFeatureOptions = {}
|
||||||
) => {
|
) => {
|
||||||
const fetcher = () => {
|
const fetcher = () => {
|
||||||
const path = formatApiPath(
|
const path = formatApiPath(
|
||||||
|
36
frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
Normal file
36
frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import { ITagType } from '../../../../interfaces/tags';
|
||||||
|
|
||||||
|
const useTagTypes = () => {
|
||||||
|
const fetcher = async () => {
|
||||||
|
const path = formatApiPath(`api/admin/tag-types`);
|
||||||
|
const res = await fetch(path, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY = `api/admin/tag-types`;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(KEY, fetcher);
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const refetch = () => {
|
||||||
|
mutate(KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagTypes: (data?.tagTypes as ITagType[]) || [],
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTagTypes;
|
36
frontend/src/hooks/api/getters/useTags/useTags.ts
Normal file
36
frontend/src/hooks/api/getters/useTags/useTags.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import { ITag } from '../../../../interfaces/tags';
|
||||||
|
|
||||||
|
const useTags = (featureId: string) => {
|
||||||
|
const fetcher = async () => {
|
||||||
|
const path = formatApiPath(`api/admin/features/${featureId}/tags`);
|
||||||
|
const res = await fetch(path, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY = `api/admin/features/${featureId}/tags`;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(KEY, fetcher);
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const refetch = () => {
|
||||||
|
mutate(KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: (data?.tags as ITag[]) || [],
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTags;
|
10
frontend/src/interfaces/tags.ts
Normal file
10
frontend/src/interfaces/tags.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface ITag {
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITagType {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user