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