mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01: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;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: 'Sen', sans-serif;
|
||||
}
|
||||
|
||||
.MuiButton-root {
|
||||
border-radius: 3px;
|
||||
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',
|
||||
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: {
|
||||
marginRight: '1rem',
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
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 { useContext, useState } from 'react';
|
||||
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 ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||
@ -24,6 +24,7 @@ const FeatureStrategiesConfigure = ({
|
||||
setToastData,
|
||||
}: IFeatureStrategiesConfigure) => {
|
||||
const smallScreen = useMediaQuery('(max-width:900px)');
|
||||
const history = useHistory();
|
||||
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
const [productionGuard, setProductionGuard] = useState(false);
|
||||
@ -90,6 +91,7 @@ const FeatureStrategiesConfigure = ({
|
||||
type: 'success',
|
||||
text: 'Successfully added strategy.',
|
||||
});
|
||||
history.replace(history.location.pathname);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 useFeatureStrategyApi from '../../../../../../hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||
import useToast from '../../../../../../hooks/useToast';
|
||||
@ -11,7 +11,7 @@ const useFeatureStrategiesEnvironmentList = (
|
||||
strategies: IFeatureStrategy[]
|
||||
) => {
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
|
||||
const history = useHistory();
|
||||
const { deleteStrategyFromFeature, updateStrategyOnFeature } =
|
||||
useFeatureStrategyApi();
|
||||
|
||||
@ -71,7 +71,7 @@ const useFeatureStrategiesEnvironmentList = (
|
||||
|
||||
strategy.parameters = updateStrategyPayload.parameters;
|
||||
strategy.constraints = updateStrategyPayload.constraints;
|
||||
|
||||
history.replace(history.location.pathname);
|
||||
setFeatureCache(feature);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
@ -109,6 +109,7 @@ const useFeatureStrategiesEnvironmentList = (
|
||||
type: 'success',
|
||||
text: `Successfully deleted strategy from ${featureId}`,
|
||||
});
|
||||
history.replace(history.location.pathname);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
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 { useStyles } from './FeatureStrategiesEnvironments.styles';
|
||||
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 AccessContext from '../../../../../contexts/AccessContext';
|
||||
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
|
||||
import useQueryParams from '../../../../../hooks/useQueryParams';
|
||||
|
||||
const FeatureStrategiesEnvironments = () => {
|
||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const history = useHistory();
|
||||
|
||||
const startingTabId = 0;
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
@ -32,6 +34,10 @@ const FeatureStrategiesEnvironments = () => {
|
||||
const [showRefreshPrompt, setShowRefreshPrompt] = useState(false);
|
||||
|
||||
const styles = useStyles();
|
||||
const query = useQueryParams();
|
||||
const addStrategy = query.get('addStrategy');
|
||||
const environmentTab = query.get('environment');
|
||||
|
||||
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(startingTabId);
|
||||
const {
|
||||
setActiveEnvironment,
|
||||
@ -49,6 +55,28 @@ const FeatureStrategiesEnvironments = () => {
|
||||
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(() => {
|
||||
if (!feature) return;
|
||||
if (featureCache === null || !featureCache.createdAt) {
|
||||
@ -70,12 +98,6 @@ const FeatureStrategiesEnvironments = () => {
|
||||
/*eslint-disable-next-line */
|
||||
}, [feature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!feature?.environments?.length > 0) return;
|
||||
setActiveEnvironment(feature?.environments[activeTabIdx]);
|
||||
/* eslint-disable-next-line */
|
||||
}, [feature]);
|
||||
|
||||
const renderTabs = () => {
|
||||
return featureCache?.environments?.map((env, index) => {
|
||||
return (
|
||||
@ -363,6 +385,7 @@ const FeatureStrategiesEnvironments = () => {
|
||||
setActiveEnvironment(
|
||||
featureCache?.environments[tabId]
|
||||
);
|
||||
history.replace(history.location.pathname);
|
||||
}}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
|
@ -9,6 +9,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
margin: '0.5rem 0',
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
'&:active': {
|
||||
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 { useEffect } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Route, useHistory, useParams } from 'react-router-dom';
|
||||
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
||||
import useTabs from '../../../hooks/useTabs';
|
||||
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 FeatureVariants from './FeatureVariants/FeatureVariants';
|
||||
import { useStyles } from './FeatureView2.styles';
|
||||
import FeatureViewEnvironment from './FeatureViewEnvironment/FeatureViewEnvironment';
|
||||
import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
|
||||
|
||||
const FeatureView2 = () => {
|
||||
const { projectId, featureId, activeTab } = useParams<IFeatureViewParams>();
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(0);
|
||||
const { a11yProps } = useTabs(0);
|
||||
const styles = useStyles();
|
||||
const history = useHistory();
|
||||
|
||||
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 = [
|
||||
{
|
||||
title: 'Overview',
|
||||
component: renderOverview(),
|
||||
path: `${basePath}/overview`,
|
||||
name: 'overview',
|
||||
},
|
||||
{
|
||||
title: 'Strategies',
|
||||
component: <FeatureStrategies />,
|
||||
path: `${basePath}/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 = () => {
|
||||
@ -67,9 +49,9 @@ const FeatureView2 = () => {
|
||||
<Tab
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
{...a11yProps(index)}
|
||||
onClick={() => {
|
||||
setActiveTab(index);
|
||||
history.push(tab.path);
|
||||
}}
|
||||
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 (
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
@ -97,10 +69,7 @@ const FeatureView2 = () => {
|
||||
<div className={styles.separator} />
|
||||
<div className={styles.tabContainer}>
|
||||
<Tabs
|
||||
value={activeTabIdx}
|
||||
onChange={(_, tabId) => {
|
||||
setActiveTab(tabId);
|
||||
}}
|
||||
value={history.location.pathname}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
className={styles.tabNavigation}
|
||||
@ -109,7 +78,26 @@ const FeatureView2 = () => {
|
||||
</Tabs>
|
||||
</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) => ({
|
||||
variants: ownProps.featureToggle.variants || [],
|
||||
features: state.features.toJS(),
|
||||
stickinessOptions: [
|
||||
'default',
|
||||
...state.context.filter(c => c.stickiness).map(c => c.name),
|
||||
@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
if (currentVariants.length > 0) {
|
||||
stickiness = currentVariants[0].stickiness || 'default';
|
||||
} else {
|
||||
stickiness = 'default'
|
||||
stickiness = 'default';
|
||||
}
|
||||
variant.stickiness = stickiness;
|
||||
|
||||
|
@ -190,7 +190,7 @@ Array [
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/projects",
|
||||
"path": "/projects/:projectId/features2/:featureId/:activeTab",
|
||||
"path": "/projects/:projectId/features2/:featureId",
|
||||
"title": "FeatureView2",
|
||||
"type": "protected",
|
||||
},
|
||||
|
@ -222,7 +222,7 @@ export const routes = [
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/features2/:featureId/:activeTab',
|
||||
path: '/projects/:projectId/features2/:featureId',
|
||||
parent: '/projects',
|
||||
title: 'FeatureView2',
|
||||
component: FeatureView2,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ITag } from '../../../../interfaces/tags';
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
const useFeatureApi = () => {
|
||||
@ -5,12 +6,54 @@ const useFeatureApi = () => {
|
||||
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 (
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
featureId: 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, {
|
||||
method: 'POST',
|
||||
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;
|
||||
|
@ -15,7 +15,7 @@ interface IUseFeatureOptions {
|
||||
const useFeature = (
|
||||
projectId: string,
|
||||
id: string,
|
||||
options: IUseFeatureOptions
|
||||
options: IUseFeatureOptions = {}
|
||||
) => {
|
||||
const fetcher = () => {
|
||||
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