mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
Feat/feature routes (#327)
* fix: setup new routes * fix: copy toggle * fix: link to correct project * fix: redirect oss to default * fix: update tests * fix: edit path * fix: remove invalid property * fix: add project to test data * fix: update paths to use features * fix: update test data * fix: update snapshots * fix: only show button to add toggle if you have access * fix: change heading * fix: use new route * fix: archive view * fix: update snapshots * fix: sorting headers * fix: list headers * fix: only show span if revive is present * fix: add border to list * fix: update snapshots * fix: remove console log
This commit is contained in:
parent
03665ed8db
commit
728477e238
@ -56,6 +56,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
|
||||
<ReportToggleListItem
|
||||
key={feature.name}
|
||||
{...feature}
|
||||
project={selectedProject}
|
||||
bulkActionsOn={BULK_ACTIONS_ON}
|
||||
/>
|
||||
));
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ReportToggleList from './ReportToggleList';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {};
|
||||
|
||||
const ReportToggleListContainer = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(ReportToggleList);
|
||||
|
||||
export default ReportToggleListContainer;
|
@ -21,12 +21,14 @@ import {
|
||||
} from '../../../../constants/featureToggleTypes';
|
||||
|
||||
import styles from '../ReportToggleList.module.scss';
|
||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||
|
||||
const ReportToggleListItem = ({
|
||||
name,
|
||||
stale,
|
||||
lastSeenAt,
|
||||
createdAt,
|
||||
project,
|
||||
type,
|
||||
checked,
|
||||
bulkActionsOn,
|
||||
@ -121,7 +123,7 @@ const ReportToggleListItem = ({
|
||||
};
|
||||
|
||||
const navigateToFeature = () => {
|
||||
history.push(`/features/strategies/${name}`);
|
||||
history.push(getTogglePath(project, name));
|
||||
};
|
||||
|
||||
const statusClasses = classnames(styles.active, {
|
||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import Select from '../common/select';
|
||||
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
||||
import ReportToggleListContainer from './ReportToggleList/ReportToggleListContainer';
|
||||
import ReportToggleList from './ReportToggleList/ReportToggleList';
|
||||
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
@ -14,6 +14,7 @@ import { REPORTING_SELECT_ID } from '../../testIds';
|
||||
import styles from './Reporting.module.scss';
|
||||
import useHealthReport from '../../hooks/api/getters/useHealthReport/useHealthReport';
|
||||
import ApiError from '../common/ApiError/ApiError';
|
||||
import useQueryParams from '../../hooks/useQueryParams';
|
||||
|
||||
const Reporting = ({ projects }) => {
|
||||
const [projectOptions, setProjectOptions] = useState([
|
||||
@ -22,8 +23,15 @@ const Reporting = ({ projects }) => {
|
||||
const [selectedProject, setSelectedProject] = useState('default');
|
||||
const { project, error, refetch } = useHealthReport(selectedProject);
|
||||
|
||||
const params = useQueryParams();
|
||||
const projectId = params.get('project');
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
return setSelectedProject(projectId);
|
||||
}
|
||||
setSelectedProject(projects[0].id);
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
@ -82,7 +90,7 @@ const Reporting = ({ projects }) => {
|
||||
potentiallyStaleCount={project?.potentiallyStaleCount}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
<ReportToggleListContainer
|
||||
<ReportToggleList
|
||||
features={project.features}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
|
@ -324,7 +324,7 @@ exports[`renders correctly with permissions 1`] = `
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/features/strategies/ToggleA"
|
||||
href="/projects/default/features/ToggleA/strategies/ToggleA"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleA
|
||||
@ -362,7 +362,7 @@ exports[`renders correctly with permissions 1`] = `
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/features/create?name=ToggleB"
|
||||
href="/projects/default/create-toggle?name=ToggleB?name=ToggleB"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleB
|
||||
|
@ -64,12 +64,14 @@ test('renders correctly without permission', () => {
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
project: 'default',
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
project: 'default',
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
@ -125,12 +127,14 @@ test('renders correctly with permissions', () => {
|
||||
name: 'ToggleA',
|
||||
description: 'this is A toggle',
|
||||
enabled: true,
|
||||
project: 'default',
|
||||
},
|
||||
{
|
||||
name: 'ToggleB',
|
||||
description: 'this is B toggle',
|
||||
enabled: false,
|
||||
notFound: true,
|
||||
project: 'default',
|
||||
},
|
||||
],
|
||||
url: 'http://example.org',
|
||||
|
@ -15,7 +15,7 @@ import { Report, Extension, Timeline } from '@material-ui/icons';
|
||||
import { shorten } from '../common';
|
||||
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions';
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
import { getTogglePath } from '../../utils/route-path-helpers';
|
||||
function ApplicationView({
|
||||
seenToggles,
|
||||
hasAccess,
|
||||
@ -89,18 +89,21 @@ function ApplicationView({
|
||||
<hr />
|
||||
<List>
|
||||
{seenToggles.map(
|
||||
({ name, description, enabled, notFound }, i) => (
|
||||
(
|
||||
{ name, description, enabled, notFound, project },
|
||||
i
|
||||
) => (
|
||||
<ConditionallyRender
|
||||
key={`toggle_conditional_${name}`}
|
||||
condition={notFound}
|
||||
show={notFoundListItem({
|
||||
createUrl: '/features/create',
|
||||
createUrl: `/projects/${project}/create-toggle?name=${name}`,
|
||||
name,
|
||||
permission: CREATE_FEATURE,
|
||||
i,
|
||||
})}
|
||||
elseShow={foundListItem({
|
||||
viewUrl: '/features/strategies',
|
||||
viewUrl: getTogglePath(project, name),
|
||||
name,
|
||||
showSwitch: true,
|
||||
enabled,
|
||||
|
@ -21,7 +21,9 @@ const BreadcrumbNav = () => {
|
||||
item !== 'variants' &&
|
||||
item !== 'logs' &&
|
||||
item !== 'metrics' &&
|
||||
item !== 'copy'
|
||||
item !== 'copy' &&
|
||||
item !== 'strategies' &&
|
||||
item !== 'features'
|
||||
);
|
||||
|
||||
return (
|
||||
@ -49,11 +51,22 @@ const BreadcrumbNav = () => {
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
let link = '/';
|
||||
|
||||
paths.forEach((path, i) => {
|
||||
if (i !== index && i < index) {
|
||||
link += path + '/';
|
||||
} else if (i === index) {
|
||||
link += path;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={path}
|
||||
className={styles.breadcrumbLink}
|
||||
to={`/${path}`}
|
||||
to={link}
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
|
@ -14,6 +14,7 @@ const DropdownMenu = ({
|
||||
callback,
|
||||
icon = <ArrowDropDown />,
|
||||
label,
|
||||
style,
|
||||
startIcon,
|
||||
...rest
|
||||
}) => {
|
||||
@ -37,6 +38,7 @@ const DropdownMenu = ({
|
||||
title={title}
|
||||
startIcon={startIcon}
|
||||
onClick={handleOpen}
|
||||
style={style}
|
||||
aria-controls={id}
|
||||
aria-haspopup="true"
|
||||
icon={icon}
|
||||
|
@ -11,6 +11,7 @@ const PageContent = ({
|
||||
headerContent,
|
||||
disablePadding,
|
||||
disableBorder,
|
||||
bodyClass,
|
||||
...rest
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
@ -23,6 +24,7 @@ const PageContent = ({
|
||||
const bodyClasses = classnames(styles.bodyContainer, {
|
||||
[styles.paddingDisabled]: disablePadding,
|
||||
[styles.borderDisabled]: disableBorder,
|
||||
[bodyClass]: bodyClass,
|
||||
});
|
||||
|
||||
let header = null;
|
||||
|
@ -32,6 +32,7 @@ const ProjectSelect = ({
|
||||
disabled={selectedId === item.id}
|
||||
data-target={item.id}
|
||||
key={item.id}
|
||||
style={{ fontSize: '14px' }}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
@ -43,6 +44,7 @@ const ProjectSelect = ({
|
||||
disabled={curentProject === ALL_PROJECTS}
|
||||
data-target={ALL_PROJECTS.id}
|
||||
key={ALL_PROJECTS.id}
|
||||
style={{ fontSize: '14px' }}
|
||||
>
|
||||
{ALL_PROJECTS.name}
|
||||
</MenuItem>,
|
||||
|
@ -21,6 +21,7 @@ import AccessContext from '../../../contexts/AccessContext';
|
||||
|
||||
import { useStyles } from './styles';
|
||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
|
||||
|
||||
const FeatureToggleList = ({
|
||||
fetcher,
|
||||
@ -51,6 +52,8 @@ const FeatureToggleList = ({
|
||||
updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
||||
};
|
||||
|
||||
const createURL = getCreateTogglePath(currentProjectId);
|
||||
|
||||
const renderFeatures = () => {
|
||||
features.forEach(e => {
|
||||
e.reviveName = e.name;
|
||||
@ -101,7 +104,7 @@ const FeatureToggleList = ({
|
||||
<ListPlaceholder
|
||||
text="No features available. Get started by adding a
|
||||
new feature toggle."
|
||||
link="/features/create"
|
||||
link={createURL}
|
||||
linkText="Add your first toggle"
|
||||
/>
|
||||
}
|
||||
@ -155,7 +158,7 @@ const FeatureToggleList = ({
|
||||
<Tooltip title="Create feature toggle">
|
||||
<IconButton
|
||||
component={Link}
|
||||
to="/features/create"
|
||||
to={createURL}
|
||||
data-test="add-feature-btn"
|
||||
disabled={
|
||||
!hasAccess(
|
||||
@ -170,7 +173,7 @@ const FeatureToggleList = ({
|
||||
}
|
||||
elseShow={
|
||||
<Button
|
||||
to="/features/create"
|
||||
to={createURL}
|
||||
data-test="add-feature-btn"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
|
@ -41,6 +41,7 @@ const FeatureToggleListActions = ({
|
||||
const renderSortingOptions = () =>
|
||||
sortingOptions.map(option => (
|
||||
<MenuItem
|
||||
style={{ fontSize: '14px' }}
|
||||
key={option.type}
|
||||
disabled={isDisabled(option.type)}
|
||||
data-target={option.type}
|
||||
@ -51,6 +52,7 @@ const FeatureToggleListActions = ({
|
||||
|
||||
const renderMetricsOptions = () => [
|
||||
<MenuItemWithIcon
|
||||
style={{ fontSize: '14px' }}
|
||||
icon={HourglassEmpty}
|
||||
disabled={!settings.showLastHour}
|
||||
data-target="minute"
|
||||
@ -58,6 +60,7 @@ const FeatureToggleListActions = ({
|
||||
key={1}
|
||||
/>,
|
||||
<MenuItemWithIcon
|
||||
style={{ fontSize: '14px' }}
|
||||
icon={HourglassFull}
|
||||
disabled={settings.showLastHour}
|
||||
data-target="hour"
|
||||
@ -78,7 +81,7 @@ const FeatureToggleListActions = ({
|
||||
callback={toggleMetrics}
|
||||
renderOptions={renderMetricsOptions}
|
||||
className=""
|
||||
style={{ textTransform: 'lowercase' }}
|
||||
style={{ textTransform: 'lowercase', fontWeight: 'normal' }}
|
||||
data-loading
|
||||
/>
|
||||
<DropdownMenu
|
||||
@ -88,13 +91,16 @@ const FeatureToggleListActions = ({
|
||||
renderOptions={renderSortingOptions}
|
||||
title="Sort by"
|
||||
className=""
|
||||
style={{ textTransform: 'lowercase' }}
|
||||
style={{ textTransform: 'lowercase', fontWeight: 'normal' }}
|
||||
data-loading
|
||||
/>
|
||||
<ProjectSelect
|
||||
settings={settings}
|
||||
updateSetting={updateSetting}
|
||||
style={{ textTransform: 'lowercase' }}
|
||||
style={{
|
||||
textTransform: 'lowercase',
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
data-loading
|
||||
/>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ import { UPDATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||
import { calc, styles as commonStyles } from '../../../common';
|
||||
|
||||
import { useStyles } from './styles';
|
||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||
|
||||
const FeatureToggleListItem = ({
|
||||
feature,
|
||||
@ -50,8 +51,8 @@ const FeatureToggleListItem = ({
|
||||
));
|
||||
const featureUrl =
|
||||
toggleFeature === undefined
|
||||
? `/archive/strategies/${name}`
|
||||
: `/features/strategies/${name}`;
|
||||
? `/projects/${feature.project}/archived/${name}/metrics`
|
||||
: getTogglePath(feature.project, name);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@ -118,13 +119,18 @@ const FeatureToggleListItem = ({
|
||||
<FeatureToggleListItemChip type={type} />
|
||||
</span>
|
||||
<ConditionallyRender
|
||||
condition={revive && hasAccess(UPDATE_FEATURE, project)}
|
||||
condition={revive}
|
||||
show={
|
||||
<IconButton onClick={() => revive(feature.name)}>
|
||||
<Undo />
|
||||
</IconButton>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_FEATURE, project)}
|
||||
show={
|
||||
<IconButton onClick={() => revive(feature.name)}>
|
||||
<Undo />
|
||||
</IconButton>
|
||||
}
|
||||
elseShow={<span style={{ width: '48px ' }} />}
|
||||
/>
|
||||
}
|
||||
elseShow={<span />}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
|
@ -63,7 +63,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<a
|
||||
className="listLink truncate"
|
||||
href="/features/strategies/Another"
|
||||
href="/projects/default/features/Another/strategies"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
@ -94,7 +94,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
<span
|
||||
className="makeStyles-listItemStrategies-5 hideLt920"
|
||||
/>
|
||||
<span />
|
||||
</li>
|
||||
`;
|
||||
|
||||
@ -164,7 +163,7 @@ exports[`renders correctly with one feature without permission 1`] = `
|
||||
>
|
||||
<a
|
||||
className="listLink truncate"
|
||||
href="/features/strategies/Another"
|
||||
href="/projects/undefined/features/Another/strategies"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
@ -195,6 +194,5 @@ exports[`renders correctly with one feature without permission 1`] = `
|
||||
<span
|
||||
className="makeStyles-listItemStrategies-5 hideLt920"
|
||||
/>
|
||||
<span />
|
||||
</li>
|
||||
`;
|
||||
|
@ -106,6 +106,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "normal",
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
@ -159,6 +160,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "normal",
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
@ -196,7 +198,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
aria-disabled={true}
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||
data-test="add-feature-btn"
|
||||
href="/features/create"
|
||||
href="/projects/default/create-toggle?project=default"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
@ -355,6 +357,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "normal",
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
@ -411,6 +414,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "normal",
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
@ -451,7 +455,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
aria-disabled={true}
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||
data-test="add-feature-btn"
|
||||
href="/features/create"
|
||||
href="/projects/default/create-toggle?project=default"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
|
@ -15,6 +15,7 @@ test('renders correctly with one feature', () => {
|
||||
description: "another's description",
|
||||
enabled: false,
|
||||
stale: false,
|
||||
project: 'default',
|
||||
strategies: [
|
||||
{
|
||||
name: 'gradualRolloutRandom',
|
||||
|
@ -10,6 +10,9 @@ export const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
tableCellHeader: {
|
||||
paddingBottom: '0.5rem',
|
||||
fontWeight: 'normal',
|
||||
color: theme.palette.grey[600],
|
||||
borderBottom: '1px solid ' + theme.palette.grey[200],
|
||||
},
|
||||
typeHeader: {
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
|
@ -109,7 +109,7 @@ const FeatureToggleListNew = ({
|
||||
>
|
||||
<span data-loading>
|
||||
{env.name === ':global:'
|
||||
? 'global'
|
||||
? 'status'
|
||||
: env.name}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
@ -13,6 +13,7 @@ import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatur
|
||||
import { IEnvironments } from '../../../../interfaces/featureToggle';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||
|
||||
interface IFeatureToggleListNewItemProps {
|
||||
name: string;
|
||||
@ -41,7 +42,7 @@ const FeatureToggleListNewItem = ({
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
history.push(`/features/strategies/${name}`);
|
||||
history.push(getTogglePath(projectId, name));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -37,6 +37,9 @@ 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,
|
||||
@ -62,6 +65,9 @@ const FeatureView = ({
|
||||
const commonStyles = useCommonStyles();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { project } = featureToggle || {};
|
||||
const { changeFeatureProject } = useFeatureApi();
|
||||
const { toast, setToastData } = useToast();
|
||||
const archive = !Boolean(isFeatureView);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToTop();
|
||||
@ -112,31 +118,56 @@ const FeatureView = ({
|
||||
};
|
||||
|
||||
const getTabData = () => {
|
||||
const path = !!isFeatureView ? 'features' : 'archive';
|
||||
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}/strategies/${featureToggleName}`,
|
||||
path: `/${path}/${featureToggleName}/strategies`,
|
||||
},
|
||||
{
|
||||
label: 'Metrics',
|
||||
component: getTabComponent('metrics'),
|
||||
name: 'metrics',
|
||||
path: `/${path}/metrics/${featureToggleName}`,
|
||||
path: `/${path}/${featureToggleName}/metrics`,
|
||||
},
|
||||
{
|
||||
label: 'Variants',
|
||||
component: getTabComponent('variants'),
|
||||
name: 'variants',
|
||||
path: `/${path}/variants/${featureToggleName}`,
|
||||
path: `/${path}/${featureToggleName}/variants`,
|
||||
},
|
||||
{
|
||||
label: 'Log',
|
||||
component: getTabComponent('log'),
|
||||
name: 'logs',
|
||||
path: `/${path}/logs/${featureToggleName}`,
|
||||
path: `/${path}/${featureToggleName}/logs`,
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -153,7 +184,7 @@ const FeatureView = ({
|
||||
show={
|
||||
<Link
|
||||
to={{
|
||||
pathname: '/features/create',
|
||||
pathname: `/projects/${project}/toggles`,
|
||||
query: { name: featureToggleName },
|
||||
}}
|
||||
>
|
||||
@ -168,11 +199,11 @@ const FeatureView = ({
|
||||
|
||||
const removeToggle = () => {
|
||||
removeFeatureToggle(featureToggle.name);
|
||||
history.push('/features');
|
||||
history.push(`/projects/${featureToggle.project}`);
|
||||
};
|
||||
const reviveToggle = () => {
|
||||
revive(featureToggle.name);
|
||||
history.push('/features');
|
||||
history.push(`/projects/${featureToggle.project}`);
|
||||
};
|
||||
const updateDescription = description => {
|
||||
let feature = { ...featureToggle, description };
|
||||
@ -198,16 +229,25 @@ const FeatureView = ({
|
||||
};
|
||||
|
||||
const updateProject = evt => {
|
||||
evt.preventDefault();
|
||||
const project = evt.target.value;
|
||||
let feature = { ...featureToggle, project };
|
||||
if (Array.isArray(feature.strategies)) {
|
||||
feature.strategies.forEach(s => {
|
||||
delete s.id;
|
||||
});
|
||||
}
|
||||
const { project, name } = featureToggle;
|
||||
const newProjectId = evt.target.value;
|
||||
|
||||
editFeatureToggle(feature);
|
||||
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 => {
|
||||
@ -233,7 +273,13 @@ const FeatureView = ({
|
||||
<Typography variant="h1" className={styles.heading}>
|
||||
{featureToggle.name}
|
||||
</Typography>
|
||||
<StatusComponent stale={featureToggle.stale} />
|
||||
<ConditionallyRender
|
||||
condition={archive}
|
||||
show={<span>Archived</span>}
|
||||
elseShow={
|
||||
<StatusComponent stale={featureToggle.stale} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
@ -325,7 +371,10 @@ const FeatureView = ({
|
||||
<Button
|
||||
title="Create new feature toggle by cloning configuration"
|
||||
component={Link}
|
||||
to={`/features/copy/${featureToggle.name}`}
|
||||
to={getToggleCopyPath(
|
||||
featureToggle.project,
|
||||
featureToggle.name
|
||||
)}
|
||||
>
|
||||
Clone
|
||||
</Button>
|
||||
@ -348,7 +397,7 @@ const FeatureView = ({
|
||||
}
|
||||
elseShow={
|
||||
<Button
|
||||
disabled={!hasAccess(UPDATE_FEATURE, hasAccess)}
|
||||
disabled={!hasAccess(UPDATE_FEATURE, project)}
|
||||
onClick={reviveToggle}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
@ -374,6 +423,7 @@ const FeatureView = ({
|
||||
}}
|
||||
onClose={() => setDelDialog(false)}
|
||||
/>
|
||||
{toast}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
const RedirectArchive = () => {
|
||||
return <Redirect to="/archive" />;
|
||||
};
|
||||
|
||||
export default RedirectArchive;
|
@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { getTogglePath } from '../../../utils/route-path-helpers';
|
||||
|
||||
interface IRedirectFeatureViewProps {
|
||||
featureToggle: any;
|
||||
features: any;
|
||||
fetchFeatureToggles: () => void;
|
||||
}
|
||||
|
||||
const RedirectFeatureView = ({
|
||||
featureToggle,
|
||||
fetchFeatureToggles,
|
||||
}: IRedirectFeatureViewProps) => {
|
||||
useEffect(() => {
|
||||
if (!featureToggle) {
|
||||
fetchFeatureToggles();
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
if (!featureToggle) return null;
|
||||
return (
|
||||
<Redirect
|
||||
to={getTogglePath(featureToggle?.project, featureToggle?.name)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedirectFeatureView;
|
16
frontend/src/component/feature/RedirectFeatureView/index.ts
Normal file
16
frontend/src/component/feature/RedirectFeatureView/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFeatureToggles } from '../../../store/feature-toggle/actions';
|
||||
|
||||
import RedirectFeatureView from './RedirectFeatureView';
|
||||
|
||||
export default connect(
|
||||
(state, props) => ({
|
||||
featureToggle: state.features
|
||||
.toJS()
|
||||
.find(toggle => toggle.name === props.featureToggleName),
|
||||
}),
|
||||
{
|
||||
fetchFeatureToggles,
|
||||
}
|
||||
)(RedirectFeatureView);
|
@ -0,0 +1,167 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
Switch,
|
||||
Paper,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
import { FileCopy } from '@material-ui/icons';
|
||||
|
||||
import { styles as commonStyles } from '../../../common';
|
||||
import styles from './CopyFeature.module.scss';
|
||||
|
||||
import { trim } from '../../../common/util';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||
|
||||
const CopyFeature = props => {
|
||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||
const [replaceGroupId, setReplaceGroupId] = useState(true);
|
||||
const [apiError, setApiError] = useState('');
|
||||
const [copyToggle, setCopyToggle] = useState();
|
||||
const [nameError, setNameError] = useState(undefined);
|
||||
const [newToggleName, setNewToggleName] = useState();
|
||||
const inputRef = useRef();
|
||||
const { name } = useParams();
|
||||
const copyToggleName = name;
|
||||
|
||||
const { features } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const copyToggle = features.find(item => item.name === copyToggleName);
|
||||
if (copyToggle) {
|
||||
setCopyToggle(copyToggle);
|
||||
|
||||
inputRef.current?.focus();
|
||||
} else {
|
||||
props.fetchFeatureToggles();
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, [features.length]);
|
||||
|
||||
const setValue = evt => {
|
||||
const value = trim(evt.target.value);
|
||||
setNewToggleName(value);
|
||||
};
|
||||
|
||||
const toggleReplaceGroupId = () => {
|
||||
setReplaceGroupId(prev => !prev);
|
||||
};
|
||||
|
||||
const onValidateName = async () => {
|
||||
try {
|
||||
await props.validateName(newToggleName);
|
||||
|
||||
setNameError(undefined);
|
||||
} catch (err) {
|
||||
setNameError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async evt => {
|
||||
evt.preventDefault();
|
||||
|
||||
if (nameError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { history } = props;
|
||||
copyToggle.name = newToggleName;
|
||||
|
||||
if (replaceGroupId) {
|
||||
copyToggle.strategies.forEach(s => {
|
||||
if (s.parameters && s.parameters.groupId) {
|
||||
s.parameters.groupId = newToggleName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
props
|
||||
.createFeatureToggle(copyToggle)
|
||||
.then(() =>
|
||||
history.push(
|
||||
getTogglePath(copyToggle.project, copyToggle.name)
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
setApiError(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (!copyToggle) return <span>Toggle not found</span>;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h1>Copy {copyToggle.name}</h1>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={apiError}
|
||||
show={<Alert severity="error">{apiError}</Alert>}
|
||||
/>
|
||||
<section className={styles.content}>
|
||||
<p className={styles.text}>
|
||||
You are about to create a new feature toggle by cloning the
|
||||
configuration of feature toggle
|
||||
<Link
|
||||
to={getTogglePath(copyToggle.project, copyToggle.name)}
|
||||
>
|
||||
{copyToggle.name}
|
||||
</Link>
|
||||
. You must give the new feature toggle a unique name before
|
||||
you can proceed.
|
||||
</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<TextField
|
||||
label="Feature toggle name"
|
||||
name="name"
|
||||
value={newToggleName || ''}
|
||||
onBlur={onValidateName}
|
||||
onChange={setValue}
|
||||
error={nameError !== undefined}
|
||||
helperText={nameError}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
value={replaceGroupId}
|
||||
checked={replaceGroupId}
|
||||
label="Replace groupId"
|
||||
onChange={toggleReplaceGroupId}
|
||||
/>
|
||||
}
|
||||
label="Replace groupId"
|
||||
/>
|
||||
|
||||
<Button type="submit" color="primary" variant="contained">
|
||||
<FileCopy />
|
||||
Create from copy
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
CopyFeature.propTypes = {
|
||||
copyToggle: PropTypes.object,
|
||||
history: PropTypes.object.isRequired,
|
||||
createFeatureToggle: PropTypes.func.isRequired,
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
validateName: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CopyFeature;
|
29
frontend/src/component/feature/create/CopyFeature/index.jsx
Normal file
29
frontend/src/component/feature/create/CopyFeature/index.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { connect } from 'react-redux';
|
||||
import CopyFeatureComponent from './CopyFeature';
|
||||
import {
|
||||
createFeatureToggles,
|
||||
validateName,
|
||||
fetchFeatureToggles,
|
||||
} from '../../../../store/feature-toggle/actions';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
history: props.history,
|
||||
features: state.features.toJS(),
|
||||
copyToggle: state.features
|
||||
.toJS()
|
||||
.find(toggle => toggle.name === props.copyToggleName),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
validateName,
|
||||
createFeatureToggle: featureToggle =>
|
||||
createFeatureToggles(featureToggle)(dispatch),
|
||||
fetchFeatureToggles: () => fetchFeatureToggles()(dispatch),
|
||||
});
|
||||
|
||||
const FormAddContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CopyFeatureComponent);
|
||||
|
||||
export default FormAddContainer;
|
@ -8,6 +8,7 @@ import {
|
||||
import CreateFeature from './CreateFeature';
|
||||
import { loadNameFromUrl, showPnpsFeedback } from '../../../common/util';
|
||||
import { showFeedback } from '../../../../store/feedback/actions';
|
||||
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||
|
||||
const defaultStrategy = {
|
||||
name: 'default',
|
||||
@ -80,7 +81,9 @@ class WrapperComponent extends Component {
|
||||
|
||||
try {
|
||||
await createFeatureToggles(featureToggle).then(() =>
|
||||
history.push(`/features/strategies/${featureToggle.name}`)
|
||||
history.push(
|
||||
getTogglePath(featureToggle.project, featureToggle.name)
|
||||
)
|
||||
);
|
||||
|
||||
if (showPnpsFeedback(user)) {
|
||||
@ -98,7 +101,7 @@ class WrapperComponent extends Component {
|
||||
|
||||
onCancel = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.history.push('/features');
|
||||
this.props.history.goBack();
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -1,175 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
Switch,
|
||||
Paper,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
import { FileCopy } from '@material-ui/icons';
|
||||
|
||||
import { styles as commonStyles } from '../../common';
|
||||
import styles from './copy-feature-component.module.scss';
|
||||
|
||||
import { trim } from '../../common/util';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
class CopyFeatureComponent extends Component {
|
||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { newToggleName: '', replaceGroupId: true };
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
// TODO unwind this stuff
|
||||
if (this.props.copyToggle) {
|
||||
this.setState({ featureToggle: this.props.copyToggle });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.copyToggle) {
|
||||
this.inputRef.current.focus();
|
||||
} else {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
}
|
||||
|
||||
setValue = evt => {
|
||||
const value = trim(evt.target.value);
|
||||
this.setState({ newToggleName: value });
|
||||
};
|
||||
|
||||
toggleReplaceGroupId = () => {
|
||||
const { replaceGroupId } = !!this.state;
|
||||
this.setState({ replaceGroupId });
|
||||
};
|
||||
|
||||
onValidateName = async () => {
|
||||
const { newToggleName } = this.state;
|
||||
try {
|
||||
await this.props.validateName(newToggleName);
|
||||
this.setState({ nameError: undefined });
|
||||
} catch (err) {
|
||||
this.setState({ nameError: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = async evt => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { nameError, newToggleName, replaceGroupId } = this.state;
|
||||
if (nameError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { copyToggle, history } = this.props;
|
||||
|
||||
copyToggle.name = newToggleName;
|
||||
|
||||
if (replaceGroupId) {
|
||||
copyToggle.strategies.forEach(s => {
|
||||
if (s.parameters && s.parameters.groupId) {
|
||||
s.parameters.groupId = newToggleName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.props
|
||||
.createFeatureToggle(copyToggle)
|
||||
.then(() =>
|
||||
history.push(`/features/strategies/${copyToggle.name}`)
|
||||
);
|
||||
} catch (e) {
|
||||
this.setState({ apiError: e });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { copyToggle } = this.props;
|
||||
|
||||
if (!copyToggle) return <span>Toggle not found</span>;
|
||||
|
||||
const { newToggleName, nameError, replaceGroupId } = this.state;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h1>Copy {copyToggle.name}</h1>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={this.state.apiError}
|
||||
show={<Alert severity="error">{this.state.apiError}</Alert>}
|
||||
/>
|
||||
<section className={styles.content}>
|
||||
<p className={styles.text}>
|
||||
You are about to create a new feature toggle by cloning
|
||||
the configuration of feature toggle
|
||||
<Link to={`/features/strategies/${copyToggle.name}`}>
|
||||
{copyToggle.name}
|
||||
</Link>
|
||||
. You must give the new feature toggle a unique name
|
||||
before you can proceed.
|
||||
</p>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<TextField
|
||||
label="Feature toggle name"
|
||||
name="name"
|
||||
value={newToggleName}
|
||||
onBlur={this.onValidateName}
|
||||
onChange={this.setValue}
|
||||
error={nameError !== undefined}
|
||||
helperText={nameError}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
inputRef={this.inputRef}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
value={replaceGroupId}
|
||||
checked={replaceGroupId}
|
||||
label="Replace groupId"
|
||||
onChange={this.toggleReplaceGroupId}
|
||||
/>
|
||||
}
|
||||
label="Replace groupId"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
<FileCopy />
|
||||
Create from copy
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CopyFeatureComponent.propTypes = {
|
||||
copyToggle: PropTypes.object,
|
||||
history: PropTypes.object.isRequired,
|
||||
createFeatureToggle: PropTypes.func.isRequired,
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
validateName: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CopyFeatureComponent;
|
@ -1,18 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import CopyFeatureComponent from './copy-feature-component';
|
||||
import { createFeatureToggles, validateName, fetchFeatureToggles } from '../../../store/feature-toggle/actions';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
history: props.history,
|
||||
copyToggle: state.features.toJS().find(toggle => toggle.name === props.copyToggleName),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
validateName,
|
||||
createFeatureToggle: featureToggle => createFeatureToggles(featureToggle)(dispatch),
|
||||
fetchFeatureToggles: () => fetchFeatureToggles()(dispatch),
|
||||
});
|
||||
|
||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(CopyFeatureComponent);
|
||||
|
||||
export default FormAddContainer;
|
@ -77,6 +77,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@ -94,8 +97,8 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiFormControl-root"
|
||||
>
|
||||
<label
|
||||
className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-marginDense MuiInputLabel-outlined"
|
||||
data-shrink={false}
|
||||
className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-marginDense MuiInputLabel-outlined MuiFormLabel-filled"
|
||||
data-shrink={true}
|
||||
>
|
||||
Project
|
||||
</label>
|
||||
@ -114,13 +117,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "​",
|
||||
}
|
||||
}
|
||||
/>
|
||||
default
|
||||
</div>
|
||||
<input
|
||||
aria-hidden={true}
|
||||
@ -129,6 +126,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onChange={[Function]}
|
||||
required={false}
|
||||
tabIndex={-1}
|
||||
value="default"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
@ -145,7 +143,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="PrivateNotchedOutline-root-20 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-22"
|
||||
className="PrivateNotchedOutline-legendLabelled-22 PrivateNotchedOutline-legendNotched-23"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -205,6 +203,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiSwitch-thumb"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="MuiSwitch-track"
|
||||
@ -221,7 +222,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
<a
|
||||
aria-disabled={false}
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
href="/features/copy/Another"
|
||||
href="/projects/default/features/Another/strategies/copy"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
@ -243,6 +244,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Clone
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
<button
|
||||
aria-controls="feature-stale-dropdown"
|
||||
@ -294,6 +298,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
@ -324,6 +331,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Archive
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -377,8 +387,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-29 PrivateTabIndicator-colorPrimary-30 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
@ -408,6 +417,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Metrics
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-2"
|
||||
@ -436,6 +448,9 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Variants
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-3"
|
||||
@ -464,8 +479,20 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Log
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-29 PrivateTabIndicator-colorPrimary-30 MuiTabs-indicator"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"width": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -486,6 +513,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
"description": "another's description",
|
||||
"enabled": false,
|
||||
"name": "Another",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"strategies": Array [
|
||||
Object {
|
||||
@ -505,6 +533,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
"description": "another's description",
|
||||
"enabled": false,
|
||||
"name": "Another",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"strategies": Array [
|
||||
Object {
|
||||
|
@ -34,6 +34,7 @@ test('renders correctly with one feature', () => {
|
||||
enabled: false,
|
||||
stale: false,
|
||||
type: 'release',
|
||||
project: 'default',
|
||||
strategies: [
|
||||
{
|
||||
name: 'gradualRolloutRandom',
|
||||
|
@ -16,7 +16,6 @@ import CreateContextField from '../../page/context/create';
|
||||
import EditContextField from '../../page/context/edit';
|
||||
import CreateProject from '../../page/project/create';
|
||||
import EditProject from '../../page/project/edit';
|
||||
import ViewProject from '../../page/project/view';
|
||||
import EditProjectAccess from '../../page/project/access';
|
||||
import ListTagTypes from '../../page/tag-types';
|
||||
import CreateTagType from '../../page/tag-types/create';
|
||||
@ -39,32 +38,16 @@ import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||
import ProjectListNew from '../project/ProjectList/ProjectList';
|
||||
import Project from '../project/Project/Project';
|
||||
import RedirectFeatureViewPage from '../../page/features/redirect';
|
||||
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
|
||||
|
||||
export const routes = [
|
||||
// Features
|
||||
{
|
||||
path: '/features/create',
|
||||
parent: '/features',
|
||||
title: 'Create',
|
||||
component: CreateFeatureToggle,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/features/copy/:copyToggle',
|
||||
parent: '/features',
|
||||
title: 'Copy',
|
||||
component: CopyFeatureToggle,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/features/:activeTab/:name',
|
||||
parent: '/features',
|
||||
title: ':name',
|
||||
component: ViewFeatureToggle,
|
||||
component: RedirectFeatureViewPage,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
@ -127,7 +110,7 @@ export const routes = [
|
||||
|
||||
// Archive
|
||||
{
|
||||
path: '/archive/:activeTab/:name',
|
||||
path: '/projects/:id/archived/:name/:activeTab',
|
||||
title: ':name',
|
||||
parent: '/archive',
|
||||
component: ShowArchive,
|
||||
@ -203,7 +186,7 @@ export const routes = [
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/edit/:id',
|
||||
path: '/projects/:id/edit',
|
||||
parent: '/projects',
|
||||
title: ':id',
|
||||
component: EditProject,
|
||||
@ -211,15 +194,6 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/view/:id',
|
||||
parent: '/projects',
|
||||
title: ':id',
|
||||
component: ViewProject,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/access',
|
||||
parent: '/projects',
|
||||
@ -229,6 +203,42 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/archived',
|
||||
title: ':name',
|
||||
parent: '/archive',
|
||||
component: RedirectArchive,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/features/:name/:activeTab/copy',
|
||||
parent: '/projects/:id/features/:name/:activeTab',
|
||||
title: 'Copy',
|
||||
component: CopyFeatureToggle,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/features/:name/:activeTab',
|
||||
parent: '/projects',
|
||||
title: ':name',
|
||||
component: ViewFeatureToggle,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/create-toggle',
|
||||
parent: '/projects',
|
||||
title: 'Create',
|
||||
component: CreateFeatureToggle,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id',
|
||||
parent: '/projects',
|
||||
|
@ -13,6 +13,7 @@ import { Link } from 'react-router-dom';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import useQueryParams from '../../../hooks/useQueryParams';
|
||||
import { useEffect } from 'react';
|
||||
import { getProjectEditPath } from '../../../utils/route-path-helpers';
|
||||
|
||||
const Project = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@ -43,7 +44,7 @@ const Project = () => {
|
||||
<div ref={ref}>
|
||||
<h1 data-loading className={commonStyles.title}>
|
||||
{project?.name}{' '}
|
||||
<IconButton component={Link} to={`/projects/edit/${id}`}>
|
||||
<IconButton component={Link} to={getProjectEditPath(id)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</h1>
|
||||
|
@ -11,6 +11,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
paddingBottom: '4rem',
|
||||
},
|
||||
},
|
||||
bodyClass: { padding: '0.5rem 2rem' },
|
||||
header: {
|
||||
padding: '1rem',
|
||||
},
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { useContext } from 'react';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import { Add } from '@material-ui/icons';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import { useParams } from 'react-router';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import AccessContext from '../../../../contexts/AccessContext';
|
||||
import { IFeatureToggleListItem } from '../../../../interfaces/featureToggle';
|
||||
import { getCreateTogglePath } from '../../../../utils/route-path-helpers';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import { PROJECTFILTERING } from '../../../common/flags';
|
||||
import HeaderTitle from '../../../common/HeaderTitle';
|
||||
@ -11,6 +14,7 @@ import PageContent from '../../../common/PageContent';
|
||||
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
|
||||
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||
|
||||
interface IProjectFeatureToggles {
|
||||
features: IFeatureToggleListItem[];
|
||||
@ -24,10 +28,12 @@ const ProjectFeatureToggles = ({
|
||||
const styles = useStyles();
|
||||
const { id } = useParams();
|
||||
const history = useHistory();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
className={styles.container}
|
||||
bodyClass={styles.bodyClass}
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
className={styles.title}
|
||||
@ -47,18 +53,23 @@ const ProjectFeatureToggles = ({
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<ResponsiveButton
|
||||
onClick={() =>
|
||||
history.push(
|
||||
`/features/create?project=${id}`
|
||||
)
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_FEATURE, id)}
|
||||
show={
|
||||
<ResponsiveButton
|
||||
onClick={() =>
|
||||
history.push(
|
||||
getCreateTogglePath(id)
|
||||
)
|
||||
}
|
||||
maxWidth="700px"
|
||||
tooltip="New feature toggle"
|
||||
Icon={Add}
|
||||
>
|
||||
New feature toggle
|
||||
</ResponsiveButton>
|
||||
}
|
||||
maxWidth="700px"
|
||||
tooltip="New feature toggle"
|
||||
Icon={Add}
|
||||
>
|
||||
New feature toggle
|
||||
</ResponsiveButton>
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -78,13 +89,18 @@ const ProjectFeatureToggles = ({
|
||||
<p data-loading className={styles.noTogglesFound}>
|
||||
No feature toggles added yet.
|
||||
</p>
|
||||
<Link
|
||||
to={`/features/create?project=${id}`}
|
||||
className={styles.link}
|
||||
data-loading
|
||||
>
|
||||
Add your first toggle
|
||||
</Link>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_FEATURE, id)}
|
||||
show={
|
||||
<Link
|
||||
to={getCreateTogglePath(id)}
|
||||
className={styles.link}
|
||||
data-loading
|
||||
>
|
||||
Add your first toggle
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -50,7 +50,7 @@ const ProjectInfo = ({
|
||||
commonStyles.justifyCenter,
|
||||
styles.infoLink
|
||||
)}
|
||||
to="/reporting"
|
||||
to={`/reporting?project=${id}`}
|
||||
>
|
||||
<span className={styles.linkText} data-loading>
|
||||
view more{' '}
|
||||
|
@ -11,6 +11,7 @@ import Dialogue from '../../common/Dialogue';
|
||||
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
|
||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
||||
import { Delete, Edit } from '@material-ui/icons';
|
||||
import { getProjectEditPath } from '../../../utils/route-path-helpers';
|
||||
interface IProjectCardProps {
|
||||
name: string;
|
||||
featureCount: number;
|
||||
@ -77,7 +78,8 @@ const ProjectCard = ({
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
history.push(`/projects/edit/${id}`);
|
||||
|
||||
history.push(getProjectEditPath(id));
|
||||
}}
|
||||
>
|
||||
<Edit className={styles.icon} />
|
||||
|
@ -19,6 +19,7 @@ import { CREATE_PROJECT } from '../../AccessProvider/permissions';
|
||||
import { Add } from '@material-ui/icons';
|
||||
import ApiError from '../../common/ApiError/ApiError';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
type projectMap = {
|
||||
[index: string]: boolean;
|
||||
@ -28,11 +29,11 @@ const ProjectListNew = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const history = useHistory();
|
||||
const { toast, setToastData } = useToast();
|
||||
|
||||
const styles = useStyles();
|
||||
const { projects, loading, error, refetch } = useProjects();
|
||||
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
||||
const ref = useLoading(loading);
|
||||
const { loading: configLoading, isOss } = useUiConfig();
|
||||
|
||||
const handleHover = (projectId: string) => {
|
||||
if (fetchedProjects[projectId]) {
|
||||
@ -103,6 +104,12 @@ const ProjectListNew = () => {
|
||||
});
|
||||
};
|
||||
|
||||
if (!configLoading) {
|
||||
if (isOss()) {
|
||||
history.push('projects/default');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<PageContent
|
||||
|
@ -1,102 +0,0 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { Typography, Button, List } from '@material-ui/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import PageContent from '../../common/PageContent';
|
||||
|
||||
import FeatureToggleListItem from '../../feature/FeatureToggleList/FeatureToggleListItem';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||
|
||||
const ProjectView = ({
|
||||
project,
|
||||
features,
|
||||
settings,
|
||||
toggleFeature,
|
||||
featureMetrics,
|
||||
revive,
|
||||
fetchFeatureToggles,
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeatureToggles();
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
const renderProjectFeatures = () => {
|
||||
return features.map(feature => {
|
||||
return (
|
||||
<FeatureToggleListItem
|
||||
key={feature.name}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={toggleFeature}
|
||||
revive={revive}
|
||||
hasAccess={hasAccess}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title={`${project.name}`}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
component={Link}
|
||||
to={`/projects/edit/${project.id}`}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
to={`/projects/${project.id}/access`}
|
||||
>
|
||||
Manage access
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={project.description}
|
||||
show={
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<Typography variant="subtitle2">
|
||||
Description
|
||||
</Typography>
|
||||
<Typography>{project.description}</Typography>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography variant="subtitle2">
|
||||
Feature toggles in this project
|
||||
</Typography>
|
||||
<List>
|
||||
<ConditionallyRender
|
||||
condition={features.length > 0}
|
||||
show={renderProjectFeatures()}
|
||||
elseShow={
|
||||
<ListPlaceholder
|
||||
text="No features available. Get started by adding a
|
||||
new feature toggle."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
</PageContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectView;
|
@ -1,39 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchFeatureToggles,
|
||||
toggleFeature,
|
||||
} from '../../../store/feature-toggle/actions';
|
||||
import ViewProject from './ProjectView';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const projectBase = { id: '', name: '', description: '' };
|
||||
const realProject = state.projects
|
||||
.toJS()
|
||||
.find(n => n.id === props.projectId);
|
||||
const project = Object.assign(projectBase, realProject);
|
||||
const features = state.features
|
||||
.toJS()
|
||||
.filter(feature => feature.project === project.id);
|
||||
|
||||
const settings = state.settings.toJS();
|
||||
const featureMetrics = state.featureMetrics.toJS();
|
||||
|
||||
return {
|
||||
project,
|
||||
features,
|
||||
settings,
|
||||
featureMetrics,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleFeature,
|
||||
fetchFeatureToggles,
|
||||
};
|
||||
|
||||
const FormAddContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ViewProject);
|
||||
|
||||
export default FormAddContainer;
|
@ -67,7 +67,7 @@ function AddUserComponent({ roles, addUserToRole }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container justify="left" spacing={3} alignItems="flex-end">
|
||||
<Grid container spacing={3} alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Autocomplete
|
||||
id="add-user-component"
|
||||
|
@ -10,10 +10,10 @@ class ScrollToTop extends Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location !== prevProps.location) {
|
||||
if (
|
||||
this.props.location.pathname.includes('/features/metrics') ||
|
||||
this.props.location.pathname.includes('/features/variants') ||
|
||||
this.props.location.pathname.includes('/features/strategies') ||
|
||||
this.props.location.pathname.includes('/features/logs') ||
|
||||
this.props.location.pathname.includes('/metrics') ||
|
||||
this.props.location.pathname.includes('/variants') ||
|
||||
this.props.location.pathname.includes('/strategies') ||
|
||||
this.props.location.pathname.includes('/logs') ||
|
||||
this.props.location.pathname.includes('/admin/api') ||
|
||||
this.props.location.pathname.includes('/admin/users') ||
|
||||
this.props.location.pathname.includes('/admin/auth')
|
||||
|
@ -101,6 +101,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
variant="outlined"
|
||||
autoComplete="true"
|
||||
size="small"
|
||||
data-test="LI_EMAIL_ID"
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
@ -113,12 +114,14 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
variant="outlined"
|
||||
autoComplete="true"
|
||||
size="small"
|
||||
data-test="LI_PASSWORD_ID"
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
style={{ width: '150px', margin: '1rem auto' }}
|
||||
data-test="LI_BTN"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
|
@ -0,0 +1,31 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
const useFeatureApi = () => {
|
||||
const { makeRequest, createRequest, errors } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const changeFeatureProject = async (
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
newProjectId: string
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/features/${featureName}/changeProject`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ newProjectId }),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
return { changeFeatureProject, errors };
|
||||
};
|
||||
|
||||
export default useFeatureApi;
|
@ -23,6 +23,13 @@ const useUiConfig = () => {
|
||||
mutate(REQUEST_KEY);
|
||||
};
|
||||
|
||||
const isOss = () => {
|
||||
if (data?.versionInfo?.current?.enterprise) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
@ -32,6 +39,7 @@ const useUiConfig = () => {
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
isOss,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,13 @@
|
||||
import React from 'react';
|
||||
import CopyFeatureToggleForm from '../../component/feature/create/copy-feature-container';
|
||||
import CopyFeatureToggleForm from '../../component/feature/create/CopyFeature';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const render = ({ history, match: { params } }) => (
|
||||
<CopyFeatureToggleForm title="Copy feature toggle" history={history} copyToggleName={params.copyToggle} />
|
||||
<CopyFeatureToggleForm
|
||||
title="Copy feature toggle"
|
||||
history={history}
|
||||
copyToggleName={params.copyToggle}
|
||||
/>
|
||||
);
|
||||
|
||||
render.propTypes = {
|
||||
|
24
frontend/src/page/features/redirect.js
Normal file
24
frontend/src/page/features/redirect.js
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RedirectFeatureView from '../../component/feature/RedirectFeatureView';
|
||||
|
||||
export default class RedirectFeatureViewPage extends PureComponent {
|
||||
static propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
match: { params },
|
||||
history,
|
||||
} = this.props;
|
||||
return (
|
||||
<RedirectFeatureView
|
||||
featureToggleName={params.name}
|
||||
activeTab={params.activeTab}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ViewFeatureToggle from '../../component/feature/FeatureView';
|
||||
import FeatureView from '../../component/feature/FeatureView';
|
||||
|
||||
export default class Features extends PureComponent {
|
||||
static propTypes = {
|
||||
@ -13,6 +13,12 @@ export default class Features extends PureComponent {
|
||||
match: { params },
|
||||
history,
|
||||
} = this.props;
|
||||
return <ViewFeatureToggle featureToggleName={params.name} activeTab={params.activeTab} history={history} />;
|
||||
return (
|
||||
<FeatureView
|
||||
featureToggleName={params.name}
|
||||
activeTab={params.activeTab}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ const theme = createMuiTheme({
|
||||
},
|
||||
grey: {
|
||||
main: '#6C6C6C',
|
||||
light: '#7e7e7e',
|
||||
},
|
||||
neutral: {
|
||||
main: '#18243e',
|
||||
|
18
frontend/src/utils/route-path-helpers.ts
Normal file
18
frontend/src/utils/route-path-helpers.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export const getTogglePath = (projectId: string, featureToggleName: string) => {
|
||||
return `/projects/${projectId}/features/${featureToggleName}/strategies`;
|
||||
};
|
||||
|
||||
export const getToggleCopyPath = (
|
||||
projectId: string,
|
||||
featureToggleName: string
|
||||
) => {
|
||||
return `/projects/${projectId}/features/${featureToggleName}/strategies/copy`;
|
||||
};
|
||||
|
||||
export const getCreateTogglePath = (projectId: string) => {
|
||||
return `/projects/${projectId}/create-toggle?project=${projectId}`;
|
||||
};
|
||||
|
||||
export const getProjectEditPath = (projectId: string) => {
|
||||
return `/projects/${projectId}/edit`;
|
||||
};
|
Loading…
Reference in New Issue
Block a user