1
0
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:
Fredrik Strand Oseberg 2021-08-25 13:37:22 +02:00 committed by GitHub
parent 03665ed8db
commit 728477e238
51 changed files with 653 additions and 476 deletions

View File

@ -56,6 +56,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
<ReportToggleListItem
key={feature.name}
{...feature}
project={selectedProject}
bulkActionsOn={BULK_ACTIONS_ON}
/>
));

View File

@ -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;

View File

@ -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, {

View File

@ -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}
/>

View File

@ -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

View File

@ -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',

View File

@ -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,

View File

@ -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>

View File

@ -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}

View File

@ -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;

View File

@ -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>,

View File

@ -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"

View File

@ -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>

View File

@ -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>
);

View File

@ -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>
`;

View File

@ -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]}

View File

@ -15,6 +15,7 @@ test('renders correctly with one feature', () => {
description: "another's description",
enabled: false,
stale: false,
project: 'default',
strategies: [
{
name: 'gradualRolloutRandom',

View File

@ -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')]: {

View File

@ -109,7 +109,7 @@ const FeatureToggleListNew = ({
>
<span data-loading>
{env.name === ':global:'
? 'global'
? 'status'
: env.name}
</span>
</TableCell>

View File

@ -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));
}
};

View File

@ -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>
);
};

View File

@ -0,0 +1,7 @@
import { Redirect } from 'react-router-dom';
const RedirectArchive = () => {
return <Redirect to="/archive" />;
};
export default RedirectArchive;

View File

@ -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;

View 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);

View File

@ -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&nbsp;{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&nbsp;
<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 />
&nbsp;&nbsp;&nbsp; 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;

View 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;

View File

@ -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() {

View File

@ -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&nbsp;{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&nbsp;
<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 />
&nbsp;&nbsp;&nbsp; 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;

View File

@ -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;

View File

@ -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": "&#8203;",
}
}
/>
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 {

View File

@ -34,6 +34,7 @@ test('renders correctly with one feature', () => {
enabled: false,
stale: false,
type: 'release',
project: 'default',
strategies: [
{
name: 'gradualRolloutRandom',

View File

@ -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',

View File

@ -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>

View File

@ -11,6 +11,7 @@ export const useStyles = makeStyles(theme => ({
paddingBottom: '4rem',
},
},
bodyClass: { padding: '0.5rem 2rem' },
header: {
padding: '1rem',
},

View File

@ -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>
}
/>
</>
}
/>

View File

@ -50,7 +50,7 @@ const ProjectInfo = ({
commonStyles.justifyCenter,
styles.infoLink
)}
to="/reporting"
to={`/reporting?project=${id}`}
>
<span className={styles.linkText} data-loading>
view more{' '}

View File

@ -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} />

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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"

View File

@ -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')

View File

@ -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>

View File

@ -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;

View File

@ -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,
};
};

View File

@ -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 = {

View 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}
/>
);
}
}

View File

@ -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}
/>
);
}
}

View File

@ -19,6 +19,7 @@ const theme = createMuiTheme({
},
grey: {
main: '#6C6C6C',
light: '#7e7e7e',
},
neutral: {
main: '#18243e',

View 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`;
};