mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +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
|
<ReportToggleListItem
|
||||||
key={feature.name}
|
key={feature.name}
|
||||||
{...feature}
|
{...feature}
|
||||||
|
project={selectedProject}
|
||||||
bulkActionsOn={BULK_ACTIONS_ON}
|
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';
|
} from '../../../../constants/featureToggleTypes';
|
||||||
|
|
||||||
import styles from '../ReportToggleList.module.scss';
|
import styles from '../ReportToggleList.module.scss';
|
||||||
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
|
|
||||||
const ReportToggleListItem = ({
|
const ReportToggleListItem = ({
|
||||||
name,
|
name,
|
||||||
stale,
|
stale,
|
||||||
lastSeenAt,
|
lastSeenAt,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
project,
|
||||||
type,
|
type,
|
||||||
checked,
|
checked,
|
||||||
bulkActionsOn,
|
bulkActionsOn,
|
||||||
@ -121,7 +123,7 @@ const ReportToggleListItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateToFeature = () => {
|
const navigateToFeature = () => {
|
||||||
history.push(`/features/strategies/${name}`);
|
history.push(getTogglePath(project, name));
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusClasses = classnames(styles.active, {
|
const statusClasses = classnames(styles.active, {
|
||||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import Select from '../common/select';
|
import Select from '../common/select';
|
||||||
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
||||||
import ReportToggleListContainer from './ReportToggleList/ReportToggleListContainer';
|
import ReportToggleList from './ReportToggleList/ReportToggleList';
|
||||||
|
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { REPORTING_SELECT_ID } from '../../testIds';
|
|||||||
import styles from './Reporting.module.scss';
|
import styles from './Reporting.module.scss';
|
||||||
import useHealthReport from '../../hooks/api/getters/useHealthReport/useHealthReport';
|
import useHealthReport from '../../hooks/api/getters/useHealthReport/useHealthReport';
|
||||||
import ApiError from '../common/ApiError/ApiError';
|
import ApiError from '../common/ApiError/ApiError';
|
||||||
|
import useQueryParams from '../../hooks/useQueryParams';
|
||||||
|
|
||||||
const Reporting = ({ projects }) => {
|
const Reporting = ({ projects }) => {
|
||||||
const [projectOptions, setProjectOptions] = useState([
|
const [projectOptions, setProjectOptions] = useState([
|
||||||
@ -22,8 +23,15 @@ const Reporting = ({ projects }) => {
|
|||||||
const [selectedProject, setSelectedProject] = useState('default');
|
const [selectedProject, setSelectedProject] = useState('default');
|
||||||
const { project, error, refetch } = useHealthReport(selectedProject);
|
const { project, error, refetch } = useHealthReport(selectedProject);
|
||||||
|
|
||||||
|
const params = useQueryParams();
|
||||||
|
const projectId = params.get('project');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (projectId) {
|
||||||
|
return setSelectedProject(projectId);
|
||||||
|
}
|
||||||
setSelectedProject(projects[0].id);
|
setSelectedProject(projects[0].id);
|
||||||
|
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -82,7 +90,7 @@ const Reporting = ({ projects }) => {
|
|||||||
potentiallyStaleCount={project?.potentiallyStaleCount}
|
potentiallyStaleCount={project?.potentiallyStaleCount}
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
/>
|
/>
|
||||||
<ReportToggleListContainer
|
<ReportToggleList
|
||||||
features={project.features}
|
features={project.features}
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
/>
|
/>
|
||||||
|
@ -324,7 +324,7 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/features/strategies/ToggleA"
|
href="/projects/default/features/ToggleA/strategies/ToggleA"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
ToggleA
|
ToggleA
|
||||||
@ -362,7 +362,7 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/features/create?name=ToggleB"
|
href="/projects/default/create-toggle?name=ToggleB?name=ToggleB"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
ToggleB
|
ToggleB
|
||||||
|
@ -64,12 +64,14 @@ test('renders correctly without permission', () => {
|
|||||||
name: 'ToggleA',
|
name: 'ToggleA',
|
||||||
description: 'this is A toggle',
|
description: 'this is A toggle',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
project: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ToggleB',
|
name: 'ToggleB',
|
||||||
description: 'this is B toggle',
|
description: 'this is B toggle',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
project: 'default',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
url: 'http://example.org',
|
url: 'http://example.org',
|
||||||
@ -125,12 +127,14 @@ test('renders correctly with permissions', () => {
|
|||||||
name: 'ToggleA',
|
name: 'ToggleA',
|
||||||
description: 'this is A toggle',
|
description: 'this is A toggle',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
project: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ToggleB',
|
name: 'ToggleB',
|
||||||
description: 'this is B toggle',
|
description: 'this is B toggle',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
project: 'default',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
url: 'http://example.org',
|
url: 'http://example.org',
|
||||||
|
@ -15,7 +15,7 @@ import { Report, Extension, Timeline } from '@material-ui/icons';
|
|||||||
import { shorten } from '../common';
|
import { shorten } from '../common';
|
||||||
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions';
|
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { getTogglePath } from '../../utils/route-path-helpers';
|
||||||
function ApplicationView({
|
function ApplicationView({
|
||||||
seenToggles,
|
seenToggles,
|
||||||
hasAccess,
|
hasAccess,
|
||||||
@ -89,18 +89,21 @@ function ApplicationView({
|
|||||||
<hr />
|
<hr />
|
||||||
<List>
|
<List>
|
||||||
{seenToggles.map(
|
{seenToggles.map(
|
||||||
({ name, description, enabled, notFound }, i) => (
|
(
|
||||||
|
{ name, description, enabled, notFound, project },
|
||||||
|
i
|
||||||
|
) => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
key={`toggle_conditional_${name}`}
|
key={`toggle_conditional_${name}`}
|
||||||
condition={notFound}
|
condition={notFound}
|
||||||
show={notFoundListItem({
|
show={notFoundListItem({
|
||||||
createUrl: '/features/create',
|
createUrl: `/projects/${project}/create-toggle?name=${name}`,
|
||||||
name,
|
name,
|
||||||
permission: CREATE_FEATURE,
|
permission: CREATE_FEATURE,
|
||||||
i,
|
i,
|
||||||
})}
|
})}
|
||||||
elseShow={foundListItem({
|
elseShow={foundListItem({
|
||||||
viewUrl: '/features/strategies',
|
viewUrl: getTogglePath(project, name),
|
||||||
name,
|
name,
|
||||||
showSwitch: true,
|
showSwitch: true,
|
||||||
enabled,
|
enabled,
|
||||||
|
@ -21,7 +21,9 @@ const BreadcrumbNav = () => {
|
|||||||
item !== 'variants' &&
|
item !== 'variants' &&
|
||||||
item !== 'logs' &&
|
item !== 'logs' &&
|
||||||
item !== 'metrics' &&
|
item !== 'metrics' &&
|
||||||
item !== 'copy'
|
item !== 'copy' &&
|
||||||
|
item !== 'strategies' &&
|
||||||
|
item !== 'features'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -49,11 +51,22 @@ const BreadcrumbNav = () => {
|
|||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let link = '/';
|
||||||
|
|
||||||
|
paths.forEach((path, i) => {
|
||||||
|
if (i !== index && i < index) {
|
||||||
|
link += path + '/';
|
||||||
|
} else if (i === index) {
|
||||||
|
link += path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={path}
|
key={path}
|
||||||
className={styles.breadcrumbLink}
|
className={styles.breadcrumbLink}
|
||||||
to={`/${path}`}
|
to={link}
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -14,6 +14,7 @@ const DropdownMenu = ({
|
|||||||
callback,
|
callback,
|
||||||
icon = <ArrowDropDown />,
|
icon = <ArrowDropDown />,
|
||||||
label,
|
label,
|
||||||
|
style,
|
||||||
startIcon,
|
startIcon,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
@ -37,6 +38,7 @@ const DropdownMenu = ({
|
|||||||
title={title}
|
title={title}
|
||||||
startIcon={startIcon}
|
startIcon={startIcon}
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
|
style={style}
|
||||||
aria-controls={id}
|
aria-controls={id}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
|
@ -11,6 +11,7 @@ const PageContent = ({
|
|||||||
headerContent,
|
headerContent,
|
||||||
disablePadding,
|
disablePadding,
|
||||||
disableBorder,
|
disableBorder,
|
||||||
|
bodyClass,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
@ -23,6 +24,7 @@ const PageContent = ({
|
|||||||
const bodyClasses = classnames(styles.bodyContainer, {
|
const bodyClasses = classnames(styles.bodyContainer, {
|
||||||
[styles.paddingDisabled]: disablePadding,
|
[styles.paddingDisabled]: disablePadding,
|
||||||
[styles.borderDisabled]: disableBorder,
|
[styles.borderDisabled]: disableBorder,
|
||||||
|
[bodyClass]: bodyClass,
|
||||||
});
|
});
|
||||||
|
|
||||||
let header = null;
|
let header = null;
|
||||||
|
@ -32,6 +32,7 @@ const ProjectSelect = ({
|
|||||||
disabled={selectedId === item.id}
|
disabled={selectedId === item.id}
|
||||||
data-target={item.id}
|
data-target={item.id}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -43,6 +44,7 @@ const ProjectSelect = ({
|
|||||||
disabled={curentProject === ALL_PROJECTS}
|
disabled={curentProject === ALL_PROJECTS}
|
||||||
data-target={ALL_PROJECTS.id}
|
data-target={ALL_PROJECTS.id}
|
||||||
key={ALL_PROJECTS.id}
|
key={ALL_PROJECTS.id}
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
>
|
>
|
||||||
{ALL_PROJECTS.name}
|
{ALL_PROJECTS.name}
|
||||||
</MenuItem>,
|
</MenuItem>,
|
||||||
|
@ -21,6 +21,7 @@ import AccessContext from '../../../contexts/AccessContext';
|
|||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||||
|
import { getCreateTogglePath } from '../../../utils/route-path-helpers';
|
||||||
|
|
||||||
const FeatureToggleList = ({
|
const FeatureToggleList = ({
|
||||||
fetcher,
|
fetcher,
|
||||||
@ -51,6 +52,8 @@ const FeatureToggleList = ({
|
|||||||
updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createURL = getCreateTogglePath(currentProjectId);
|
||||||
|
|
||||||
const renderFeatures = () => {
|
const renderFeatures = () => {
|
||||||
features.forEach(e => {
|
features.forEach(e => {
|
||||||
e.reviveName = e.name;
|
e.reviveName = e.name;
|
||||||
@ -101,7 +104,7 @@ const FeatureToggleList = ({
|
|||||||
<ListPlaceholder
|
<ListPlaceholder
|
||||||
text="No features available. Get started by adding a
|
text="No features available. Get started by adding a
|
||||||
new feature toggle."
|
new feature toggle."
|
||||||
link="/features/create"
|
link={createURL}
|
||||||
linkText="Add your first toggle"
|
linkText="Add your first toggle"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -155,7 +158,7 @@ const FeatureToggleList = ({
|
|||||||
<Tooltip title="Create feature toggle">
|
<Tooltip title="Create feature toggle">
|
||||||
<IconButton
|
<IconButton
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/features/create"
|
to={createURL}
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
disabled={
|
disabled={
|
||||||
!hasAccess(
|
!hasAccess(
|
||||||
@ -170,7 +173,7 @@ const FeatureToggleList = ({
|
|||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Button
|
<Button
|
||||||
to="/features/create"
|
to={createURL}
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -41,6 +41,7 @@ const FeatureToggleListActions = ({
|
|||||||
const renderSortingOptions = () =>
|
const renderSortingOptions = () =>
|
||||||
sortingOptions.map(option => (
|
sortingOptions.map(option => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
key={option.type}
|
key={option.type}
|
||||||
disabled={isDisabled(option.type)}
|
disabled={isDisabled(option.type)}
|
||||||
data-target={option.type}
|
data-target={option.type}
|
||||||
@ -51,6 +52,7 @@ const FeatureToggleListActions = ({
|
|||||||
|
|
||||||
const renderMetricsOptions = () => [
|
const renderMetricsOptions = () => [
|
||||||
<MenuItemWithIcon
|
<MenuItemWithIcon
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
icon={HourglassEmpty}
|
icon={HourglassEmpty}
|
||||||
disabled={!settings.showLastHour}
|
disabled={!settings.showLastHour}
|
||||||
data-target="minute"
|
data-target="minute"
|
||||||
@ -58,6 +60,7 @@ const FeatureToggleListActions = ({
|
|||||||
key={1}
|
key={1}
|
||||||
/>,
|
/>,
|
||||||
<MenuItemWithIcon
|
<MenuItemWithIcon
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
icon={HourglassFull}
|
icon={HourglassFull}
|
||||||
disabled={settings.showLastHour}
|
disabled={settings.showLastHour}
|
||||||
data-target="hour"
|
data-target="hour"
|
||||||
@ -78,7 +81,7 @@ const FeatureToggleListActions = ({
|
|||||||
callback={toggleMetrics}
|
callback={toggleMetrics}
|
||||||
renderOptions={renderMetricsOptions}
|
renderOptions={renderMetricsOptions}
|
||||||
className=""
|
className=""
|
||||||
style={{ textTransform: 'lowercase' }}
|
style={{ textTransform: 'lowercase', fontWeight: 'normal' }}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
@ -88,13 +91,16 @@ const FeatureToggleListActions = ({
|
|||||||
renderOptions={renderSortingOptions}
|
renderOptions={renderSortingOptions}
|
||||||
title="Sort by"
|
title="Sort by"
|
||||||
className=""
|
className=""
|
||||||
style={{ textTransform: 'lowercase' }}
|
style={{ textTransform: 'lowercase', fontWeight: 'normal' }}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<ProjectSelect
|
<ProjectSelect
|
||||||
settings={settings}
|
settings={settings}
|
||||||
updateSetting={updateSetting}
|
updateSetting={updateSetting}
|
||||||
style={{ textTransform: 'lowercase' }}
|
style={{
|
||||||
|
textTransform: 'lowercase',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
}}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,6 +16,7 @@ import { UPDATE_FEATURE } from '../../../AccessProvider/permissions';
|
|||||||
import { calc, styles as commonStyles } from '../../../common';
|
import { calc, styles as commonStyles } from '../../../common';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
|
|
||||||
const FeatureToggleListItem = ({
|
const FeatureToggleListItem = ({
|
||||||
feature,
|
feature,
|
||||||
@ -50,8 +51,8 @@ const FeatureToggleListItem = ({
|
|||||||
));
|
));
|
||||||
const featureUrl =
|
const featureUrl =
|
||||||
toggleFeature === undefined
|
toggleFeature === undefined
|
||||||
? `/archive/strategies/${name}`
|
? `/projects/${feature.project}/archived/${name}/metrics`
|
||||||
: `/features/strategies/${name}`;
|
: getTogglePath(feature.project, name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -118,13 +119,18 @@ const FeatureToggleListItem = ({
|
|||||||
<FeatureToggleListItemChip type={type} />
|
<FeatureToggleListItemChip type={type} />
|
||||||
</span>
|
</span>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={revive && hasAccess(UPDATE_FEATURE, project)}
|
condition={revive}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(UPDATE_FEATURE, project)}
|
||||||
show={
|
show={
|
||||||
<IconButton onClick={() => revive(feature.name)}>
|
<IconButton onClick={() => revive(feature.name)}>
|
||||||
<Undo />
|
<Undo />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
elseShow={<span />}
|
elseShow={<span style={{ width: '48px ' }} />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
@ -63,7 +63,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="listLink truncate"
|
className="listLink truncate"
|
||||||
href="/features/strategies/Another"
|
href="/projects/default/features/Another/strategies"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -94,7 +94,6 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="makeStyles-listItemStrategies-5 hideLt920"
|
className="makeStyles-listItemStrategies-5 hideLt920"
|
||||||
/>
|
/>
|
||||||
<span />
|
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -164,7 +163,7 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="listLink truncate"
|
className="listLink truncate"
|
||||||
href="/features/strategies/Another"
|
href="/projects/undefined/features/Another/strategies"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -195,6 +194,5 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="makeStyles-listItemStrategies-5 hideLt920"
|
className="makeStyles-listItemStrategies-5 hideLt920"
|
||||||
/>
|
/>
|
||||||
<span />
|
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
@ -106,6 +106,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"fontWeight": "normal",
|
||||||
"textTransform": "lowercase",
|
"textTransform": "lowercase",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,6 +160,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"fontWeight": "normal",
|
||||||
"textTransform": "lowercase",
|
"textTransform": "lowercase",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,7 +198,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
href="/features/create"
|
href="/projects/default/create-toggle?project=default"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
@ -355,6 +357,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"fontWeight": "normal",
|
||||||
"textTransform": "lowercase",
|
"textTransform": "lowercase",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -411,6 +414,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"fontWeight": "normal",
|
||||||
"textTransform": "lowercase",
|
"textTransform": "lowercase",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -451,7 +455,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
href="/features/create"
|
href="/projects/default/create-toggle?project=default"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
|
@ -15,6 +15,7 @@ test('renders correctly with one feature', () => {
|
|||||||
description: "another's description",
|
description: "another's description",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
|
project: 'default',
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
name: 'gradualRolloutRandom',
|
name: 'gradualRolloutRandom',
|
||||||
|
@ -10,6 +10,9 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
tableCellHeader: {
|
tableCellHeader: {
|
||||||
paddingBottom: '0.5rem',
|
paddingBottom: '0.5rem',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
borderBottom: '1px solid ' + theme.palette.grey[200],
|
||||||
},
|
},
|
||||||
typeHeader: {
|
typeHeader: {
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
@ -109,7 +109,7 @@ const FeatureToggleListNew = ({
|
|||||||
>
|
>
|
||||||
<span data-loading>
|
<span data-loading>
|
||||||
{env.name === ':global:'
|
{env.name === ':global:'
|
||||||
? 'global'
|
? 'status'
|
||||||
: env.name}
|
: env.name}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
@ -13,6 +13,7 @@ import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatur
|
|||||||
import { IEnvironments } from '../../../../interfaces/featureToggle';
|
import { IEnvironments } from '../../../../interfaces/featureToggle';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
import useToast from '../../../../hooks/useToast';
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
|
|
||||||
interface IFeatureToggleListNewItemProps {
|
interface IFeatureToggleListNewItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -41,7 +42,7 @@ const FeatureToggleListNewItem = ({
|
|||||||
|
|
||||||
const onClick = (e: Event) => {
|
const onClick = (e: Event) => {
|
||||||
if (!ref.current?.contains(e.target)) {
|
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 classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -37,6 +37,9 @@ import ConfirmDialogue from '../../common/Dialogue';
|
|||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
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 = ({
|
const FeatureView = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
@ -62,6 +65,9 @@ const FeatureView = ({
|
|||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { project } = featureToggle || {};
|
const { project } = featureToggle || {};
|
||||||
|
const { changeFeatureProject } = useFeatureApi();
|
||||||
|
const { toast, setToastData } = useToast();
|
||||||
|
const archive = !Boolean(isFeatureView);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
@ -112,31 +118,56 @@ const FeatureView = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTabData = () => {
|
const getTabData = () => {
|
||||||
const path = !!isFeatureView ? 'features' : 'archive';
|
const path = !!isFeatureView
|
||||||
|
? `projects/${project}/features`
|
||||||
|
: `projects/${project}/archived`;
|
||||||
|
|
||||||
|
if (archive) {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
label: 'Activation',
|
|
||||||
component: getTabComponent('activation'),
|
|
||||||
name: 'strategies',
|
|
||||||
path: `/${path}/strategies/${featureToggleName}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Metrics',
|
label: 'Metrics',
|
||||||
component: getTabComponent('metrics'),
|
component: getTabComponent('metrics'),
|
||||||
name: 'metrics',
|
name: 'metrics',
|
||||||
path: `/${path}/metrics/${featureToggleName}`,
|
path: `/${path}/${featureToggleName}/metrics`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Variants',
|
label: 'Variants',
|
||||||
component: getTabComponent('variants'),
|
component: getTabComponent('variants'),
|
||||||
name: 'variants',
|
name: 'variants',
|
||||||
path: `/${path}/variants/${featureToggleName}`,
|
path: `/${path}/${featureToggleName}/variants`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Log',
|
label: 'Log',
|
||||||
component: getTabComponent('log'),
|
component: getTabComponent('log'),
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
path: `/${path}/logs/${featureToggleName}`,
|
path: `/${path}/${featureToggleName}/logs`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Activation',
|
||||||
|
component: getTabComponent('activation'),
|
||||||
|
name: 'strategies',
|
||||||
|
path: `/${path}/${featureToggleName}/strategies`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Metrics',
|
||||||
|
component: getTabComponent('metrics'),
|
||||||
|
name: 'metrics',
|
||||||
|
path: `/${path}/${featureToggleName}/metrics`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Variants',
|
||||||
|
component: getTabComponent('variants'),
|
||||||
|
name: 'variants',
|
||||||
|
path: `/${path}/${featureToggleName}/variants`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Log',
|
||||||
|
component: getTabComponent('log'),
|
||||||
|
name: 'logs',
|
||||||
|
path: `/${path}/${featureToggleName}/logs`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@ -153,7 +184,7 @@ const FeatureView = ({
|
|||||||
show={
|
show={
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
pathname: '/features/create',
|
pathname: `/projects/${project}/toggles`,
|
||||||
query: { name: featureToggleName },
|
query: { name: featureToggleName },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -168,11 +199,11 @@ const FeatureView = ({
|
|||||||
|
|
||||||
const removeToggle = () => {
|
const removeToggle = () => {
|
||||||
removeFeatureToggle(featureToggle.name);
|
removeFeatureToggle(featureToggle.name);
|
||||||
history.push('/features');
|
history.push(`/projects/${featureToggle.project}`);
|
||||||
};
|
};
|
||||||
const reviveToggle = () => {
|
const reviveToggle = () => {
|
||||||
revive(featureToggle.name);
|
revive(featureToggle.name);
|
||||||
history.push('/features');
|
history.push(`/projects/${featureToggle.project}`);
|
||||||
};
|
};
|
||||||
const updateDescription = description => {
|
const updateDescription = description => {
|
||||||
let feature = { ...featureToggle, description };
|
let feature = { ...featureToggle, description };
|
||||||
@ -198,16 +229,25 @@ const FeatureView = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateProject = evt => {
|
const updateProject = evt => {
|
||||||
evt.preventDefault();
|
const { project, name } = featureToggle;
|
||||||
const project = evt.target.value;
|
const newProjectId = evt.target.value;
|
||||||
let feature = { ...featureToggle, project };
|
|
||||||
if (Array.isArray(feature.strategies)) {
|
|
||||||
feature.strategies.forEach(s => {
|
|
||||||
delete s.id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 => {
|
const updateStale = stale => {
|
||||||
@ -233,7 +273,13 @@ const FeatureView = ({
|
|||||||
<Typography variant="h1" className={styles.heading}>
|
<Typography variant="h1" className={styles.heading}>
|
||||||
{featureToggle.name}
|
{featureToggle.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={archive}
|
||||||
|
show={<span>Archived</span>}
|
||||||
|
elseShow={
|
||||||
<StatusComponent stale={featureToggle.stale} />
|
<StatusComponent stale={featureToggle.stale} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
@ -325,7 +371,10 @@ const FeatureView = ({
|
|||||||
<Button
|
<Button
|
||||||
title="Create new feature toggle by cloning configuration"
|
title="Create new feature toggle by cloning configuration"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/features/copy/${featureToggle.name}`}
|
to={getToggleCopyPath(
|
||||||
|
featureToggle.project,
|
||||||
|
featureToggle.name
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Clone
|
Clone
|
||||||
</Button>
|
</Button>
|
||||||
@ -348,7 +397,7 @@ const FeatureView = ({
|
|||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Button
|
<Button
|
||||||
disabled={!hasAccess(UPDATE_FEATURE, hasAccess)}
|
disabled={!hasAccess(UPDATE_FEATURE, project)}
|
||||||
onClick={reviveToggle}
|
onClick={reviveToggle}
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
@ -374,6 +423,7 @@ const FeatureView = ({
|
|||||||
}}
|
}}
|
||||||
onClose={() => setDelDialog(false)}
|
onClose={() => setDelDialog(false)}
|
||||||
/>
|
/>
|
||||||
|
{toast}
|
||||||
</Paper>
|
</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 CreateFeature from './CreateFeature';
|
||||||
import { loadNameFromUrl, showPnpsFeedback } from '../../../common/util';
|
import { loadNameFromUrl, showPnpsFeedback } from '../../../common/util';
|
||||||
import { showFeedback } from '../../../../store/feedback/actions';
|
import { showFeedback } from '../../../../store/feedback/actions';
|
||||||
|
import { getTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
|
|
||||||
const defaultStrategy = {
|
const defaultStrategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -80,7 +81,9 @@ class WrapperComponent extends Component {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await createFeatureToggles(featureToggle).then(() =>
|
await createFeatureToggles(featureToggle).then(() =>
|
||||||
history.push(`/features/strategies/${featureToggle.name}`)
|
history.push(
|
||||||
|
getTogglePath(featureToggle.project, featureToggle.name)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (showPnpsFeedback(user)) {
|
if (showPnpsFeedback(user)) {
|
||||||
@ -98,7 +101,7 @@ class WrapperComponent extends Component {
|
|||||||
|
|
||||||
onCancel = evt => {
|
onCancel = evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.props.history.push('/features');
|
this.props.history.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -94,8 +97,8 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
className="MuiFormControl-root"
|
className="MuiFormControl-root"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-marginDense MuiInputLabel-outlined"
|
className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-marginDense MuiInputLabel-outlined MuiFormLabel-filled"
|
||||||
data-shrink={false}
|
data-shrink={true}
|
||||||
>
|
>
|
||||||
Project
|
Project
|
||||||
</label>
|
</label>
|
||||||
@ -114,13 +117,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<span
|
default
|
||||||
dangerouslySetInnerHTML={
|
|
||||||
Object {
|
|
||||||
"__html": "​",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
@ -129,6 +126,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={false}
|
required={false}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
value="default"
|
||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
@ -145,7 +143,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
className="PrivateNotchedOutline-root-20 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-20 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-22"
|
className="PrivateNotchedOutline-legendLabelled-22 PrivateNotchedOutline-legendNotched-23"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Project
|
Project
|
||||||
@ -205,6 +203,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
className="MuiSwitch-thumb"
|
className="MuiSwitch-thumb"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="MuiSwitch-track"
|
className="MuiSwitch-track"
|
||||||
@ -221,7 +222,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<a
|
<a
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
href="/features/copy/Another"
|
href="/projects/default/features/Another/strategies/copy"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
@ -243,6 +244,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
Clone
|
Clone
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
aria-controls="feature-stale-dropdown"
|
aria-controls="feature-stale-dropdown"
|
||||||
@ -294,6 +298,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
@ -324,6 +331,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
Archive
|
Archive
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -377,8 +387,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
Activation
|
Activation
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="PrivateTabIndicator-root-29 PrivateTabIndicator-colorPrimary-30 MuiTabs-indicator"
|
className="MuiTouchRipple-root"
|
||||||
style={Object {}}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -408,6 +417,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
Metrics
|
Metrics
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-controls="tabpanel-2"
|
aria-controls="tabpanel-2"
|
||||||
@ -436,6 +448,9 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
Variants
|
Variants
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-controls="tabpanel-3"
|
aria-controls="tabpanel-3"
|
||||||
@ -464,8 +479,20 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
Log
|
Log
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
className="PrivateTabIndicator-root-29 PrivateTabIndicator-colorPrimary-30 MuiTabs-indicator"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"left": 0,
|
||||||
|
"width": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -486,6 +513,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
"description": "another's description",
|
"description": "another's description",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"name": "Another",
|
"name": "Another",
|
||||||
|
"project": "default",
|
||||||
"stale": false,
|
"stale": false,
|
||||||
"strategies": Array [
|
"strategies": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -505,6 +533,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
"description": "another's description",
|
"description": "another's description",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"name": "Another",
|
"name": "Another",
|
||||||
|
"project": "default",
|
||||||
"stale": false,
|
"stale": false,
|
||||||
"strategies": Array [
|
"strategies": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -34,6 +34,7 @@ test('renders correctly with one feature', () => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
stale: false,
|
stale: false,
|
||||||
type: 'release',
|
type: 'release',
|
||||||
|
project: 'default',
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
name: 'gradualRolloutRandom',
|
name: 'gradualRolloutRandom',
|
||||||
|
@ -16,7 +16,6 @@ import CreateContextField from '../../page/context/create';
|
|||||||
import EditContextField from '../../page/context/edit';
|
import EditContextField from '../../page/context/edit';
|
||||||
import CreateProject from '../../page/project/create';
|
import CreateProject from '../../page/project/create';
|
||||||
import EditProject from '../../page/project/edit';
|
import EditProject from '../../page/project/edit';
|
||||||
import ViewProject from '../../page/project/view';
|
|
||||||
import EditProjectAccess from '../../page/project/access';
|
import EditProjectAccess from '../../page/project/access';
|
||||||
import ListTagTypes from '../../page/tag-types';
|
import ListTagTypes from '../../page/tag-types';
|
||||||
import CreateTagType from '../../page/tag-types/create';
|
import CreateTagType from '../../page/tag-types/create';
|
||||||
@ -39,32 +38,16 @@ import ResetPassword from '../user/ResetPassword/ResetPassword';
|
|||||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||||
import ProjectListNew from '../project/ProjectList/ProjectList';
|
import ProjectListNew from '../project/ProjectList/ProjectList';
|
||||||
import Project from '../project/Project/Project';
|
import Project from '../project/Project/Project';
|
||||||
|
import RedirectFeatureViewPage from '../../page/features/redirect';
|
||||||
|
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Features
|
// 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',
|
path: '/features/:activeTab/:name',
|
||||||
parent: '/features',
|
parent: '/features',
|
||||||
title: ':name',
|
title: ':name',
|
||||||
component: ViewFeatureToggle,
|
component: RedirectFeatureViewPage,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
@ -127,7 +110,7 @@ export const routes = [
|
|||||||
|
|
||||||
// Archive
|
// Archive
|
||||||
{
|
{
|
||||||
path: '/archive/:activeTab/:name',
|
path: '/projects/:id/archived/:name/:activeTab',
|
||||||
title: ':name',
|
title: ':name',
|
||||||
parent: '/archive',
|
parent: '/archive',
|
||||||
component: ShowArchive,
|
component: ShowArchive,
|
||||||
@ -203,7 +186,7 @@ export const routes = [
|
|||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/edit/:id',
|
path: '/projects/:id/edit',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
title: ':id',
|
title: ':id',
|
||||||
component: EditProject,
|
component: EditProject,
|
||||||
@ -211,15 +194,6 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/projects/view/:id',
|
|
||||||
parent: '/projects',
|
|
||||||
title: ':id',
|
|
||||||
component: ViewProject,
|
|
||||||
type: 'protected',
|
|
||||||
layout: 'main',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/projects/:id/access',
|
path: '/projects/:id/access',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
@ -229,6 +203,42 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
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',
|
path: '/projects/:id',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
|
@ -13,6 +13,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import useQueryParams from '../../../hooks/useQueryParams';
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { getProjectEditPath } from '../../../utils/route-path-helpers';
|
||||||
|
|
||||||
const Project = () => {
|
const Project = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@ -43,7 +44,7 @@ const Project = () => {
|
|||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<h1 data-loading className={commonStyles.title}>
|
<h1 data-loading className={commonStyles.title}>
|
||||||
{project?.name}{' '}
|
{project?.name}{' '}
|
||||||
<IconButton component={Link} to={`/projects/edit/${id}`}>
|
<IconButton component={Link} to={getProjectEditPath(id)}>
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -11,6 +11,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
paddingBottom: '4rem',
|
paddingBottom: '4rem',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bodyClass: { padding: '0.5rem 2rem' },
|
||||||
header: {
|
header: {
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
import { IconButton } from '@material-ui/core';
|
import { IconButton } from '@material-ui/core';
|
||||||
import { Add } from '@material-ui/icons';
|
import { Add } from '@material-ui/icons';
|
||||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
import { IFeatureToggleListItem } from '../../../../interfaces/featureToggle';
|
import { IFeatureToggleListItem } from '../../../../interfaces/featureToggle';
|
||||||
|
import { getCreateTogglePath } from '../../../../utils/route-path-helpers';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
import { PROJECTFILTERING } from '../../../common/flags';
|
import { PROJECTFILTERING } from '../../../common/flags';
|
||||||
import HeaderTitle from '../../../common/HeaderTitle';
|
import HeaderTitle from '../../../common/HeaderTitle';
|
||||||
@ -11,6 +14,7 @@ import PageContent from '../../../common/PageContent';
|
|||||||
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
|
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
|
||||||
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
|
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||||
|
|
||||||
interface IProjectFeatureToggles {
|
interface IProjectFeatureToggles {
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
@ -24,10 +28,12 @@ const ProjectFeatureToggles = ({
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
|
bodyClass={styles.bodyClass}
|
||||||
headerContent={
|
headerContent={
|
||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
@ -47,10 +53,13 @@ const ProjectFeatureToggles = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(CREATE_FEATURE, id)}
|
||||||
|
show={
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
history.push(
|
history.push(
|
||||||
`/features/create?project=${id}`
|
getCreateTogglePath(id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
maxWidth="700px"
|
maxWidth="700px"
|
||||||
@ -59,6 +68,8 @@ const ProjectFeatureToggles = ({
|
|||||||
>
|
>
|
||||||
New feature toggle
|
New feature toggle
|
||||||
</ResponsiveButton>
|
</ResponsiveButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -78,13 +89,18 @@ const ProjectFeatureToggles = ({
|
|||||||
<p data-loading className={styles.noTogglesFound}>
|
<p data-loading className={styles.noTogglesFound}>
|
||||||
No feature toggles added yet.
|
No feature toggles added yet.
|
||||||
</p>
|
</p>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(CREATE_FEATURE, id)}
|
||||||
|
show={
|
||||||
<Link
|
<Link
|
||||||
to={`/features/create?project=${id}`}
|
to={getCreateTogglePath(id)}
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
data-loading
|
data-loading
|
||||||
>
|
>
|
||||||
Add your first toggle
|
Add your first toggle
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -50,7 +50,7 @@ const ProjectInfo = ({
|
|||||||
commonStyles.justifyCenter,
|
commonStyles.justifyCenter,
|
||||||
styles.infoLink
|
styles.infoLink
|
||||||
)}
|
)}
|
||||||
to="/reporting"
|
to={`/reporting?project=${id}`}
|
||||||
>
|
>
|
||||||
<span className={styles.linkText} data-loading>
|
<span className={styles.linkText} data-loading>
|
||||||
view more{' '}
|
view more{' '}
|
||||||
|
@ -11,6 +11,7 @@ import Dialogue from '../../common/Dialogue';
|
|||||||
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
|
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
||||||
import { Delete, Edit } from '@material-ui/icons';
|
import { Delete, Edit } from '@material-ui/icons';
|
||||||
|
import { getProjectEditPath } from '../../../utils/route-path-helpers';
|
||||||
interface IProjectCardProps {
|
interface IProjectCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
featureCount: number;
|
featureCount: number;
|
||||||
@ -77,7 +78,8 @@ const ProjectCard = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
history.push(`/projects/edit/${id}`);
|
|
||||||
|
history.push(getProjectEditPath(id));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Edit className={styles.icon} />
|
<Edit className={styles.icon} />
|
||||||
|
@ -19,6 +19,7 @@ import { CREATE_PROJECT } from '../../AccessProvider/permissions';
|
|||||||
import { Add } from '@material-ui/icons';
|
import { Add } from '@material-ui/icons';
|
||||||
import ApiError from '../../common/ApiError/ApiError';
|
import ApiError from '../../common/ApiError/ApiError';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
type projectMap = {
|
type projectMap = {
|
||||||
[index: string]: boolean;
|
[index: string]: boolean;
|
||||||
@ -28,11 +29,11 @@ const ProjectListNew = () => {
|
|||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { toast, setToastData } = useToast();
|
const { toast, setToastData } = useToast();
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { projects, loading, error, refetch } = useProjects();
|
const { projects, loading, error, refetch } = useProjects();
|
||||||
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
|
const { loading: configLoading, isOss } = useUiConfig();
|
||||||
|
|
||||||
const handleHover = (projectId: string) => {
|
const handleHover = (projectId: string) => {
|
||||||
if (fetchedProjects[projectId]) {
|
if (fetchedProjects[projectId]) {
|
||||||
@ -103,6 +104,12 @@ const ProjectListNew = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!configLoading) {
|
||||||
|
if (isOss()) {
|
||||||
|
history.push('projects/default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<PageContent
|
<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 (
|
return (
|
||||||
<Grid container justify="left" spacing={3} alignItems="flex-end">
|
<Grid container spacing={3} alignItems="flex-end">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id="add-user-component"
|
id="add-user-component"
|
||||||
|
@ -10,10 +10,10 @@ class ScrollToTop extends Component {
|
|||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.location !== prevProps.location) {
|
if (this.props.location !== prevProps.location) {
|
||||||
if (
|
if (
|
||||||
this.props.location.pathname.includes('/features/metrics') ||
|
this.props.location.pathname.includes('/metrics') ||
|
||||||
this.props.location.pathname.includes('/features/variants') ||
|
this.props.location.pathname.includes('/variants') ||
|
||||||
this.props.location.pathname.includes('/features/strategies') ||
|
this.props.location.pathname.includes('/strategies') ||
|
||||||
this.props.location.pathname.includes('/features/logs') ||
|
this.props.location.pathname.includes('/logs') ||
|
||||||
this.props.location.pathname.includes('/admin/api') ||
|
this.props.location.pathname.includes('/admin/api') ||
|
||||||
this.props.location.pathname.includes('/admin/users') ||
|
this.props.location.pathname.includes('/admin/users') ||
|
||||||
this.props.location.pathname.includes('/admin/auth')
|
this.props.location.pathname.includes('/admin/auth')
|
||||||
|
@ -101,6 +101,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
autoComplete="true"
|
autoComplete="true"
|
||||||
size="small"
|
size="small"
|
||||||
|
data-test="LI_EMAIL_ID"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Password"
|
label="Password"
|
||||||
@ -113,12 +114,14 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
autoComplete="true"
|
autoComplete="true"
|
||||||
size="small"
|
size="small"
|
||||||
|
data-test="LI_PASSWORD_ID"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
style={{ width: '150px', margin: '1rem auto' }}
|
style={{ width: '150px', margin: '1rem auto' }}
|
||||||
|
data-test="LI_BTN"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</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);
|
mutate(REQUEST_KEY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOss = () => {
|
||||||
|
if (data?.versionInfo?.current?.enterprise) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(!error && !data);
|
setLoading(!error && !data);
|
||||||
}, [data, error]);
|
}, [data, error]);
|
||||||
@ -32,6 +39,7 @@ const useUiConfig = () => {
|
|||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
refetch,
|
refetch,
|
||||||
|
isOss,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
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';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const render = ({ history, match: { params } }) => (
|
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 = {
|
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 React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ViewFeatureToggle from '../../component/feature/FeatureView';
|
import FeatureView from '../../component/feature/FeatureView';
|
||||||
|
|
||||||
export default class Features extends PureComponent {
|
export default class Features extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -13,6 +13,12 @@ export default class Features extends PureComponent {
|
|||||||
match: { params },
|
match: { params },
|
||||||
history,
|
history,
|
||||||
} = this.props;
|
} = 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: {
|
grey: {
|
||||||
main: '#6C6C6C',
|
main: '#6C6C6C',
|
||||||
|
light: '#7e7e7e',
|
||||||
},
|
},
|
||||||
neutral: {
|
neutral: {
|
||||||
main: '#18243e',
|
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