diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx index 8a51cccb94..8ba05c14ee 100644 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx @@ -56,6 +56,7 @@ const ReportToggleList = ({ features, selectedProject }) => { )); diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListContainer.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListContainer.jsx deleted file mode 100644 index cbf146aa9f..0000000000 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListContainer.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx index 83047f72f9..d6948e63c0 100644 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx @@ -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, { diff --git a/frontend/src/component/Reporting/Reporting.jsx b/frontend/src/component/Reporting/Reporting.jsx index dda39df3f3..d7465092db 100644 --- a/frontend/src/component/Reporting/Reporting.jsx +++ b/frontend/src/component/Reporting/Reporting.jsx @@ -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} /> - diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap index bebdb4496c..a3024b8d79 100644 --- a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap +++ b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap @@ -324,7 +324,7 @@ exports[`renders correctly with permissions 1`] = ` className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" > ToggleA @@ -362,7 +362,7 @@ exports[`renders correctly with permissions 1`] = ` className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" > ToggleB diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.js index 9c58edbd57..7c184d3f7a 100644 --- a/frontend/src/component/application/__tests__/application-edit-component-test.js +++ b/frontend/src/component/application/__tests__/application-edit-component-test.js @@ -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', diff --git a/frontend/src/component/application/application-view.jsx b/frontend/src/component/application/application-view.jsx index 99bbd12528..e068b0e278 100644 --- a/frontend/src/component/application/application-view.jsx +++ b/frontend/src/component/application/application-view.jsx @@ -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({
{seenToggles.map( - ({ name, description, enabled, notFound }, i) => ( + ( + { name, description, enabled, notFound, project }, + i + ) => ( { item !== 'variants' && item !== 'logs' && item !== 'metrics' && - item !== 'copy' + item !== 'copy' && + item !== 'strategies' && + item !== 'features' ); return ( @@ -49,11 +51,22 @@ const BreadcrumbNav = () => {

); } + + let link = '/'; + + paths.forEach((path, i) => { + if (i !== index && i < index) { + link += path + '/'; + } else if (i === index) { + link += path; + } + }); + return ( {path} diff --git a/frontend/src/component/common/DropdownMenu/DropdownMenu.jsx b/frontend/src/component/common/DropdownMenu/DropdownMenu.jsx index 41d6fc322a..9e51e217c4 100644 --- a/frontend/src/component/common/DropdownMenu/DropdownMenu.jsx +++ b/frontend/src/component/common/DropdownMenu/DropdownMenu.jsx @@ -14,6 +14,7 @@ const DropdownMenu = ({ callback, icon = , 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} diff --git a/frontend/src/component/common/PageContent/PageContent.jsx b/frontend/src/component/common/PageContent/PageContent.jsx index 026c0aa648..f04454a1ae 100644 --- a/frontend/src/component/common/PageContent/PageContent.jsx +++ b/frontend/src/component/common/PageContent/PageContent.jsx @@ -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; diff --git a/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx b/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx index c034d2e5e8..f1866aeffa 100644 --- a/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx +++ b/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx @@ -32,6 +32,7 @@ const ProjectSelect = ({ disabled={selectedId === item.id} data-target={item.id} key={item.id} + style={{ fontSize: '14px' }} > {item.name} @@ -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} , diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx index 72a815b546..f4be94aeca 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx @@ -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 = ({ } @@ -155,7 +158,7 @@ const FeatureToggleList = ({ sortingOptions.map(option => ( [ , diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx index 93473a1ef3..d7011a884d 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx @@ -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 ( revive(feature.name)}> - - + revive(feature.name)}> + + + } + elseShow={} + /> } - elseShow={} /> ); diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap index 51202aee47..fe706a6270 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap @@ -63,7 +63,7 @@ exports[`renders correctly with one feature 1`] = ` >
- `; @@ -164,7 +163,7 @@ exports[`renders correctly with one feature without permission 1`] = ` > - `; diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap index 44152a9777..96cc31e468 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap @@ -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]} diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx index 4a0e2345ef..a9325a9d36 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx @@ -15,6 +15,7 @@ test('renders correctly with one feature', () => { description: "another's description", enabled: false, stale: false, + project: 'default', strategies: [ { name: 'gradualRolloutRandom', diff --git a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.styles.ts b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.styles.ts index 4784b6e01d..37c8b7d55a 100644 --- a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.styles.ts +++ b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.styles.ts @@ -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')]: { diff --git a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx index 02f539b3ac..786de58387 100644 --- a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx +++ b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNew.tsx @@ -109,7 +109,7 @@ const FeatureToggleListNew = ({ > {env.name === ':global:' - ? 'global' + ? 'status' : env.name} diff --git a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNewItem/FeatureToggleListNewItem.tsx b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNewItem/FeatureToggleListNewItem.tsx index 0fb5ba54dd..ec534b4ab7 100644 --- a/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNewItem/FeatureToggleListNewItem.tsx +++ b/frontend/src/component/feature/FeatureToggleListNew/FeatureToggleListNewItem/FeatureToggleListNewItem.tsx @@ -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)); } }; diff --git a/frontend/src/component/feature/FeatureView/FeatureView.jsx b/frontend/src/component/feature/FeatureView/FeatureView.jsx index 4d60c4e12e..288fabf202 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.jsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.jsx @@ -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={ @@ -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 = ({ {featureToggle.name} - + Archived} + elseShow={ + + } + />
Clone @@ -348,7 +397,7 @@ const FeatureView = ({ } elseShow={ + + + + ); +}; + +CopyFeature.propTypes = { + copyToggle: PropTypes.object, + history: PropTypes.object.isRequired, + createFeatureToggle: PropTypes.func.isRequired, + fetchFeatureToggles: PropTypes.func.isRequired, + validateName: PropTypes.func.isRequired, +}; + +export default CopyFeature; diff --git a/frontend/src/component/feature/create/copy-feature-component.module.scss b/frontend/src/component/feature/create/CopyFeature/CopyFeature.module.scss similarity index 100% rename from frontend/src/component/feature/create/copy-feature-component.module.scss rename to frontend/src/component/feature/create/CopyFeature/CopyFeature.module.scss diff --git a/frontend/src/component/feature/create/CopyFeature/index.jsx b/frontend/src/component/feature/create/CopyFeature/index.jsx new file mode 100644 index 0000000000..427bec1b56 --- /dev/null +++ b/frontend/src/component/feature/create/CopyFeature/index.jsx @@ -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; diff --git a/frontend/src/component/feature/create/CreateFeature/index.jsx b/frontend/src/component/feature/create/CreateFeature/index.jsx index f7c7b97bcc..6367076169 100644 --- a/frontend/src/component/feature/create/CreateFeature/index.jsx +++ b/frontend/src/component/feature/create/CreateFeature/index.jsx @@ -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() { diff --git a/frontend/src/component/feature/create/copy-feature-component.jsx b/frontend/src/component/feature/create/copy-feature-component.jsx deleted file mode 100644 index a36b10c21d..0000000000 --- a/frontend/src/component/feature/create/copy-feature-component.jsx +++ /dev/null @@ -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 Toggle not found; - - const { newToggleName, nameError, replaceGroupId } = this.state; - - return ( - -
-

Copy {copyToggle.name}

-
- {this.state.apiError}} - /> -
-

- You are about to create a new feature toggle by cloning - the configuration of feature toggle  - - {copyToggle.name} - - . You must give the new feature toggle a unique name - before you can proceed. -

-
- - - } - label="Replace groupId" - /> - - - -
-
- ); - } -} - -CopyFeatureComponent.propTypes = { - copyToggle: PropTypes.object, - history: PropTypes.object.isRequired, - createFeatureToggle: PropTypes.func.isRequired, - fetchFeatureToggles: PropTypes.func.isRequired, - validateName: PropTypes.func.isRequired, -}; - -export default CopyFeatureComponent; diff --git a/frontend/src/component/feature/create/copy-feature-container.jsx b/frontend/src/component/feature/create/copy-feature-container.jsx deleted file mode 100644 index 8a54bdee02..0000000000 --- a/frontend/src/component/feature/create/copy-feature-container.jsx +++ /dev/null @@ -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; diff --git a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap index 11841023e0..4c12f61dda 100644 --- a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap +++ b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap @@ -77,6 +77,9 @@ exports[`renders correctly with one feature 1`] = ` /> +

@@ -94,8 +97,8 @@ exports[`renders correctly with one feature 1`] = ` className="MuiFormControl-root" > @@ -114,13 +117,7 @@ exports[`renders correctly with one feature 1`] = ` role="button" tabIndex={0} > - + default Project @@ -205,6 +203,9 @@ exports[`renders correctly with one feature 1`] = ` className="MuiSwitch-thumb" /> + Clone + @@ -377,8 +387,7 @@ exports[`renders correctly with one feature 1`] = ` Activation + @@ -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 { diff --git a/frontend/src/component/feature/view/__tests__/view-component-test.jsx b/frontend/src/component/feature/view/__tests__/view-component-test.jsx index 611c8a01d2..e84581b05d 100644 --- a/frontend/src/component/feature/view/__tests__/view-component-test.jsx +++ b/frontend/src/component/feature/view/__tests__/view-component-test.jsx @@ -34,6 +34,7 @@ test('renders correctly with one feature', () => { enabled: false, stale: false, type: 'release', + project: 'default', strategies: [ { name: 'gradualRolloutRandom', diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index f642d116b2..1de66179d3 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -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', diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 23f196d5c2..6480c8adca 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -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 = () => {

{project?.name}{' '} - +

diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.styles.ts b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.styles.ts index 4a7839d6bd..69f48f7efe 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.styles.ts +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.styles.ts @@ -11,6 +11,7 @@ export const useStyles = makeStyles(theme => ({ paddingBottom: '4rem', }, }, + bodyClass: { padding: '0.5rem 2rem' }, header: { padding: '1rem', }, diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index d479d8542e..3864493944 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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 ( } /> - - history.push( - `/features/create?project=${id}` - ) + + history.push( + getCreateTogglePath(id) + ) + } + maxWidth="700px" + tooltip="New feature toggle" + Icon={Add} + > + New feature toggle + } - maxWidth="700px" - tooltip="New feature toggle" - Icon={Add} - > - New feature toggle - + /> } /> @@ -78,13 +89,18 @@ const ProjectFeatureToggles = ({

No feature toggles added yet.

- - Add your first toggle - + + Add your first toggle + + } + /> } /> diff --git a/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.tsx b/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.tsx index dc0fa4e04e..aa72a7e8c5 100644 --- a/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.tsx +++ b/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.tsx @@ -50,7 +50,7 @@ const ProjectInfo = ({ commonStyles.justifyCenter, styles.infoLink )} - to="/reporting" + to={`/reporting?project=${id}`} > view more{' '} diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.tsx b/frontend/src/component/project/ProjectCard/ProjectCard.tsx index 114abb9ee0..23b64a7de9 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectCard.tsx @@ -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 = ({ { e.preventDefault(); - history.push(`/projects/edit/${id}`); + + history.push(getProjectEditPath(id)); }} > diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index 7b091fdb12..0ee70d54ef 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -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({}); 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 (
{ - const { hasAccess } = useContext(AccessContext); - - useEffect(() => { - fetchFeatureToggles(); - /* eslint-disable-next-line */ - }, []); - - const renderProjectFeatures = () => { - return features.map(feature => { - return ( - - ); - }); - }; - - return ( -
- - - - - } - /> - } - > - - - Description - - {project.description} -
- } - /> - - - Feature toggles in this project - - - 0} - show={renderProjectFeatures()} - elseShow={ - - } - /> - -
-
- ); -}; - -export default ProjectView; diff --git a/frontend/src/component/project/ProjectView/index.js b/frontend/src/component/project/ProjectView/index.js deleted file mode 100644 index cc645ecbfc..0000000000 --- a/frontend/src/component/project/ProjectView/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/component/project/access-add-user.js b/frontend/src/component/project/access-add-user.js index ba6c08c4e4..1277f95e4d 100644 --- a/frontend/src/component/project/access-add-user.js +++ b/frontend/src/component/project/access-add-user.js @@ -67,7 +67,7 @@ function AddUserComponent({ roles, addUserToRole }) { }; return ( - + { variant="outlined" autoComplete="true" size="small" + data-test="LI_EMAIL_ID" /> { variant="outlined" autoComplete="true" size="small" + data-test="LI_PASSWORD_ID" /> diff --git a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts new file mode 100644 index 0000000000..3d5f61b1da --- /dev/null +++ b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts @@ -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; diff --git a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts index d7c91b9b7b..44c28578c1 100644 --- a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts +++ b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts @@ -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, }; }; diff --git a/frontend/src/page/features/copy.js b/frontend/src/page/features/copy.js index bce9a49d68..935c55fe38 100644 --- a/frontend/src/page/features/copy.js +++ b/frontend/src/page/features/copy.js @@ -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 } }) => ( - + ); render.propTypes = { diff --git a/frontend/src/page/features/redirect.js b/frontend/src/page/features/redirect.js new file mode 100644 index 0000000000..9e123be7af --- /dev/null +++ b/frontend/src/page/features/redirect.js @@ -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 ( + + ); + } +} diff --git a/frontend/src/page/features/show.js b/frontend/src/page/features/show.js index cab66b3059..76f743f9d3 100644 --- a/frontend/src/page/features/show.js +++ b/frontend/src/page/features/show.js @@ -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 ; + return ( + + ); } } diff --git a/frontend/src/themes/main-theme.js b/frontend/src/themes/main-theme.js index 96238d5091..baea88c57d 100644 --- a/frontend/src/themes/main-theme.js +++ b/frontend/src/themes/main-theme.js @@ -19,6 +19,7 @@ const theme = createMuiTheme({ }, grey: { main: '#6C6C6C', + light: '#7e7e7e', }, neutral: { main: '#18243e', diff --git a/frontend/src/utils/route-path-helpers.ts b/frontend/src/utils/route-path-helpers.ts new file mode 100644 index 0000000000..4a7db71b32 --- /dev/null +++ b/frontend/src/utils/route-path-helpers.ts @@ -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`; +};