mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
451 lines
15 KiB
JavaScript
451 lines
15 KiB
JavaScript
import { useContext, useEffect, useLayoutEffect, useState } from 'react';
|
|
import classnames from 'classnames';
|
|
import PropTypes from 'prop-types';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
Paper,
|
|
Typography,
|
|
Button,
|
|
Switch,
|
|
LinearProgress,
|
|
} from '@material-ui/core';
|
|
|
|
import HistoryComponent from '../../history/FeatureEventHistory';
|
|
import MetricComponent from '../view/metric-container';
|
|
import UpdateStrategies from '../view/update-strategies-container';
|
|
import EditVariants from '../variant/update-variant-container';
|
|
import FeatureTypeSelect from '../feature-type-select-container';
|
|
import ProjectSelect from '../project-select-container';
|
|
import UpdateDescriptionComponent from '../view/update-description-component';
|
|
import {
|
|
CREATE_FEATURE,
|
|
DELETE_FEATURE,
|
|
UPDATE_FEATURE,
|
|
} from '../../providers/AccessProvider/permissions';
|
|
import StatusComponent from '../status-component';
|
|
import FeatureTagComponent from '../feature-tag-component';
|
|
import StatusUpdateComponent from '../view/status-update-component';
|
|
import AddTagDialog from '../add-tag-dialog-container';
|
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
|
import TabNav from '../../common/TabNav';
|
|
|
|
import { scrollToTop } from '../../common/util';
|
|
|
|
import styles from './FeatureView.module.scss';
|
|
import ConfirmDialogue from '../../common/Dialogue';
|
|
|
|
import { useCommonStyles } from '../../../common.styles';
|
|
import AccessContext from '../../../contexts/AccessContext';
|
|
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
|
import { getToggleCopyPath } from '../../../utils/route-path-helpers';
|
|
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
|
import useToast from '../../../hooks/useToast';
|
|
|
|
const FeatureView = ({
|
|
activeTab,
|
|
featureToggleName,
|
|
features,
|
|
toggleFeature,
|
|
setStale,
|
|
removeFeatureToggle,
|
|
revive,
|
|
fetchArchive,
|
|
fetchFeatureToggles,
|
|
editFeatureToggle,
|
|
featureToggle,
|
|
history,
|
|
untagFeature,
|
|
featureTags,
|
|
fetchTags,
|
|
tagTypes,
|
|
user,
|
|
}) => {
|
|
const isFeatureView = !!fetchFeatureToggles;
|
|
const [delDialog, setDelDialog] = useState(false);
|
|
const commonStyles = useCommonStyles();
|
|
const { hasAccess } = useContext(AccessContext);
|
|
const { project } = featureToggle || {};
|
|
const { changeFeatureProject } = useFeatureApi();
|
|
const { toast, setToastData } = useToast();
|
|
const archive = !Boolean(isFeatureView);
|
|
|
|
useEffect(() => {
|
|
scrollToTop();
|
|
fetchTags(featureToggleName);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
if (isFeatureView) {
|
|
fetchFeatureToggles();
|
|
} else {
|
|
fetchArchive();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const editable = isFeatureView && hasAccess(UPDATE_FEATURE, project);
|
|
|
|
const getTabComponent = key => {
|
|
switch (key) {
|
|
case 'activation':
|
|
return (
|
|
<UpdateStrategies
|
|
featureToggle={featureToggle}
|
|
features={features}
|
|
history={history}
|
|
editable={editable}
|
|
/>
|
|
);
|
|
case 'metrics':
|
|
return <MetricComponent featureToggle={featureToggle} />;
|
|
case 'variants':
|
|
return (
|
|
<EditVariants
|
|
featureToggle={featureToggle}
|
|
features={features}
|
|
history={history}
|
|
editable={editable}
|
|
/>
|
|
);
|
|
case 'log':
|
|
return <HistoryComponent toggleName={featureToggleName} />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getTabData = () => {
|
|
const path = !!isFeatureView
|
|
? `projects/${project}/features`
|
|
: `projects/${project}/archived`;
|
|
|
|
if (archive) {
|
|
return [
|
|
{
|
|
label: 'Metrics',
|
|
component: getTabComponent('metrics'),
|
|
name: 'metrics',
|
|
path: `/${path}/${featureToggleName}/metrics`,
|
|
},
|
|
{
|
|
label: 'Variants',
|
|
component: getTabComponent('variants'),
|
|
name: 'variants',
|
|
path: `/${path}/${featureToggleName}/variants`,
|
|
},
|
|
{
|
|
label: 'Log',
|
|
component: getTabComponent('log'),
|
|
name: 'logs',
|
|
path: `/${path}/${featureToggleName}/logs`,
|
|
},
|
|
];
|
|
}
|
|
return [
|
|
{
|
|
label: 'Activation',
|
|
component: getTabComponent('activation'),
|
|
name: 'strategies',
|
|
path: `/${path}/${featureToggleName}/strategies`,
|
|
},
|
|
{
|
|
label: 'Metrics',
|
|
component: getTabComponent('metrics'),
|
|
name: 'metrics',
|
|
path: `/${path}/${featureToggleName}/metrics`,
|
|
},
|
|
{
|
|
label: 'Variants',
|
|
component: getTabComponent('variants'),
|
|
name: 'variants',
|
|
path: `/${path}/${featureToggleName}/variants`,
|
|
},
|
|
{
|
|
label: 'Log',
|
|
component: getTabComponent('log'),
|
|
name: 'logs',
|
|
path: `/${path}/${featureToggleName}/logs`,
|
|
},
|
|
];
|
|
};
|
|
|
|
if (!featureToggle) {
|
|
if (features.length === 0) {
|
|
return <LinearProgress />;
|
|
}
|
|
return (
|
|
<span>
|
|
Could not find the toggle{' '}
|
|
<ConditionallyRender
|
|
condition={editable}
|
|
show={
|
|
<Link
|
|
to={{
|
|
pathname: `/projects/${project}/toggles`,
|
|
query: { name: featureToggleName },
|
|
}}
|
|
>
|
|
{featureToggleName}
|
|
</Link>
|
|
}
|
|
elseShow={<span>featureToggleName</span>}
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const removeToggle = () => {
|
|
removeFeatureToggle(featureToggle.name);
|
|
history.push(`/projects/${featureToggle.project}`);
|
|
};
|
|
const reviveToggle = () => {
|
|
revive(featureToggle.name);
|
|
history.push(`/projects/${featureToggle.project}`);
|
|
};
|
|
const updateDescription = description => {
|
|
let feature = { ...featureToggle, description };
|
|
if (Array.isArray(feature.strategies)) {
|
|
feature.strategies.forEach(s => {
|
|
delete s.id;
|
|
});
|
|
}
|
|
|
|
editFeatureToggle(feature);
|
|
};
|
|
const updateType = evt => {
|
|
evt.preventDefault();
|
|
const type = evt.target.value;
|
|
let feature = { ...featureToggle, type };
|
|
if (Array.isArray(feature.strategies)) {
|
|
feature.strategies.forEach(s => {
|
|
delete s.id;
|
|
});
|
|
}
|
|
|
|
editFeatureToggle(feature);
|
|
};
|
|
|
|
const updateProject = evt => {
|
|
const { project, name } = featureToggle;
|
|
const newProjectId = evt.target.value;
|
|
|
|
changeFeatureProject(project, name, newProjectId)
|
|
.then(() => {
|
|
fetchFeatureToggles();
|
|
setToastData({
|
|
show: true,
|
|
type: 'success',
|
|
text: 'Successfully updated toggle project.',
|
|
});
|
|
})
|
|
.catch(e => {
|
|
setToastData({
|
|
show: true,
|
|
type: 'error',
|
|
text: e.toString(),
|
|
});
|
|
});
|
|
};
|
|
|
|
const updateStale = stale => {
|
|
setStale(stale, featureToggleName);
|
|
};
|
|
|
|
const tabs = getTabData();
|
|
|
|
const findActiveTab = activeTab =>
|
|
tabs.findIndex(tab => tab.name === activeTab);
|
|
|
|
return (
|
|
<Paper
|
|
className={commonStyles.fullwidth}
|
|
style={{
|
|
overflow: 'visible',
|
|
borderRadius: '10px',
|
|
boxShadow: 'none',
|
|
}}
|
|
>
|
|
<div>
|
|
<div className={styles.header}>
|
|
<Typography variant="h1" className={styles.heading}>
|
|
{featureToggle.name}
|
|
</Typography>
|
|
<ConditionallyRender
|
|
condition={archive}
|
|
show={<span>Archived</span>}
|
|
elseShow={
|
|
<StatusComponent stale={featureToggle.stale} />
|
|
}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={classnames(
|
|
styles.featureInfoContainer,
|
|
commonStyles.contentSpacingY
|
|
)}
|
|
>
|
|
<UpdateDescriptionComponent
|
|
isFeatureView={isFeatureView}
|
|
description={featureToggle.description}
|
|
update={updateDescription}
|
|
editable={editable}
|
|
/>
|
|
<div className={styles.selectContainer}>
|
|
<FeatureTypeSelect
|
|
value={featureToggle.type}
|
|
onChange={updateType}
|
|
label="Feature type"
|
|
editable={editable}
|
|
/>
|
|
|
|
<ProjectSelect
|
|
value={featureToggle.project}
|
|
onChange={updateProject}
|
|
label="Project"
|
|
filled
|
|
editable={editable}
|
|
filter={projectFilterGenerator(
|
|
user,
|
|
CREATE_FEATURE
|
|
)}
|
|
/>
|
|
</div>
|
|
<FeatureTagComponent
|
|
featureToggleName={featureToggle.name}
|
|
tags={featureTags}
|
|
tagTypes={tagTypes}
|
|
untagFeature={untagFeature}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.actions}>
|
|
<span style={{ paddingRight: '24px' }}>
|
|
<ConditionallyRender
|
|
condition={editable}
|
|
show={
|
|
<>
|
|
<Switch
|
|
disabled={!isFeatureView}
|
|
checked={featureToggle.enabled}
|
|
onChange={() =>
|
|
toggleFeature(
|
|
!featureToggle.enabled,
|
|
featureToggle.name
|
|
)
|
|
}
|
|
/>
|
|
<span>
|
|
{featureToggle.enabled
|
|
? 'Enabled'
|
|
: 'Disabled'}
|
|
</span>
|
|
</>
|
|
}
|
|
elseShow={
|
|
<>
|
|
<Switch
|
|
disabled
|
|
checked={featureToggle.enabled}
|
|
/>
|
|
<span>
|
|
{featureToggle.enabled
|
|
? 'Enabled'
|
|
: 'Disabled'}
|
|
</span>
|
|
</>
|
|
}
|
|
/>
|
|
</span>
|
|
|
|
<ConditionallyRender
|
|
condition={isFeatureView}
|
|
show={
|
|
<div>
|
|
<AddTagDialog
|
|
featureToggleName={featureToggle.name}
|
|
/>
|
|
<Button
|
|
disabled={!hasAccess(CREATE_FEATURE, project)}
|
|
title="Create new feature toggle by cloning configuration"
|
|
component={Link}
|
|
to={getToggleCopyPath(
|
|
featureToggle.project,
|
|
featureToggle.name
|
|
)}
|
|
>
|
|
Clone
|
|
</Button>
|
|
<StatusUpdateComponent
|
|
stale={featureToggle.stale}
|
|
updateStale={updateStale}
|
|
/>
|
|
|
|
<Button
|
|
disabled={!hasAccess(DELETE_FEATURE, project)}
|
|
onClick={() => {
|
|
setDelDialog(true);
|
|
}}
|
|
title="Archive feature toggle"
|
|
style={{ flexShrink: 0 }}
|
|
>
|
|
Archive
|
|
</Button>
|
|
</div>
|
|
}
|
|
elseShow={
|
|
<Button
|
|
disabled={!hasAccess(UPDATE_FEATURE, project)}
|
|
onClick={reviveToggle}
|
|
style={{ flexShrink: 0 }}
|
|
>
|
|
Revive
|
|
</Button>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<TabNav
|
|
tabData={tabs}
|
|
className={styles.tabContentContainer}
|
|
startingTab={findActiveTab(activeTab)}
|
|
/>
|
|
<ConfirmDialogue
|
|
open={delDialog}
|
|
title="Are you sure you want to archive this toggle"
|
|
onClick={() => {
|
|
setDelDialog(false);
|
|
removeToggle();
|
|
}}
|
|
onClose={() => setDelDialog(false)}
|
|
/>
|
|
{toast}
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
FeatureView.propTypes = {
|
|
activeTab: PropTypes.string.isRequired,
|
|
featureToggleName: PropTypes.string.isRequired,
|
|
features: PropTypes.array.isRequired,
|
|
toggleFeature: PropTypes.func,
|
|
setStale: PropTypes.func,
|
|
removeFeatureToggle: PropTypes.func,
|
|
revive: PropTypes.func,
|
|
fetchArchive: PropTypes.func,
|
|
fetchFeatureToggles: PropTypes.func,
|
|
fetchFeatureToggle: PropTypes.func,
|
|
editFeatureToggle: PropTypes.func,
|
|
featureToggle: PropTypes.object,
|
|
history: PropTypes.object.isRequired,
|
|
fetchTags: PropTypes.func,
|
|
untagFeature: PropTypes.func,
|
|
featureTags: PropTypes.array,
|
|
tagTypes: PropTypes.array,
|
|
};
|
|
|
|
export default FeatureView;
|