mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
Feat/new navigation (#314)
* feat: change color scheme * feat: add navigation menu * fix: add bg image * fix: add archive and strategies to navigation * fix: round corners * feat: mobile view project details * feat: mobile view navigation * fix: only show menu if user is admin * fix: rename navigation * fix: only render relevant routes for oss context * feat: add project actions * feat: add icons * feat: add breadcrumbs * fix: place breadcrumbs absolutely * fix: adjust breadcrumbs * fix: toast * fix: cleanup * fix login * fix: breadcrumbs * fix: add billing link * fix: links * fix: feature view * fix: path to go back * fix: remove default value * fix: remove unused imports * refactor: delete outdated test * fix: add item to filter in breadcrumb * fix: remove console log
This commit is contained in:
parent
16e0a7b4de
commit
1a63d91f95
@ -22,6 +22,24 @@
|
|||||||
src: url('./assets/fonts/Roboto-700.ttf');
|
src: url('./assets/fonts/Roboto-700.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Sen';
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('./assets/fonts/Sen-Regular.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Sen';
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('./assets/fonts/Sen-Bold.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Sen';
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('./assets/fonts/Sen-ExtraBold.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -34,10 +52,12 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
/* font-family: 'Sen'; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiButton-root {
|
.MuiButton-root {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton {
|
.skeleton {
|
||||||
|
BIN
frontend/src/assets/fonts/Sen-Bold.ttf
Normal file
BIN
frontend/src/assets/fonts/Sen-Bold.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Sen-ExtraBold.ttf
Normal file
BIN
frontend/src/assets/fonts/Sen-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Sen-Regular.ttf
Normal file
BIN
frontend/src/assets/fonts/Sen-Regular.ttf
Normal file
Binary file not shown.
1
frontend/src/assets/img/logo-dark-with-text.svg
Normal file
1
frontend/src/assets/img/logo-dark-with-text.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="bg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 653.4 251.24"><defs><style>.cls-1{fill:#1a4049;}.cls-2{fill:#fff;}.cls-3{fill:#817afe;}</style></defs><circle class="cls-1" cx="125.62" cy="125.62" r="80"/><polygon class="cls-2" points="137.05 91.33 137.05 114.19 137.05 137.05 159.9 137.05 159.9 114.19 159.9 91.33 137.05 91.33"/><polygon class="cls-2" points="114.19 114.19 114.19 91.33 91.33 91.33 91.33 114.19 91.33 137.05 91.33 159.9 114.19 159.9 137.05 159.9 137.05 137.05 114.19 137.05 114.19 114.19"/><polygon class="cls-2" points="137.05 91.33 137.05 114.19 137.05 137.05 159.9 137.05 159.9 114.19 159.9 91.33 137.05 91.33"/><polygon class="cls-2" points="114.19 114.19 114.19 91.33 91.33 91.33 91.33 114.19 91.33 137.05 91.33 159.9 114.19 159.9 137.05 159.9 137.05 137.05 114.19 137.05 114.19 114.19"/><rect class="cls-3" x="137.05" y="137.05" width="22.86" height="22.86"/><path class="cls-1" d="M251.58,139.13V112.77h11.93v25.06c0,7.36,3.91,12.2,11.27,12.2s11.27-4.84,11.27-12.2V112.77h12v26.36c0,12.67-8.48,21.8-23.1,21.8C260.06,160.93,251.58,151.8,251.58,139.13Z"/><path class="cls-1" d="M321.91,159.9H310.08V112.77h11.83v7.92a17.93,17.93,0,0,1,15.65-9c11.83,0,19.66,8.67,19.66,20.68V159.9h-12V134.75c0-7.45-4.38-12.39-11.46-12.39s-11.83,5-11.83,12.39Z"/><path class="cls-1" d="M369.42,91.34h11.92V159.9H369.42Z"/><path class="cls-1" d="M441.24,137v1.3H403.79c.47,7.36,5.87,13,13.69,13,7.55,0,10.62-3.82,11.46-5.12h11.74c-.75,4.84-7.17,15-23.2,15-15.27,0-25.61-10.61-25.61-24.77,0-14.63,10.24-24.78,24.68-24.78S441.24,121.62,441.24,137Zm-37.08-6.9h24.6c-1.77-6-6.15-9.31-12.21-9.31C410.12,120.78,405.65,124.23,404.16,130.09Z"/><path class="cls-1" d="M467.78,130.37h15.47c0-5.68-4.29-9.31-11.27-9.31-6.62,0-9,3.54-9.78,4.75h-12c1-4.94,6.89-14.25,21.8-14.25,14.62,0,22.73,7.64,22.73,18.81V146.3c0,2.89,1,4,3.72,4.29v9.59h-3.72c-6.06-.09-9.69-2.33-11-6.52-2.23,3.45-7.26,7.27-15.27,7.27-11.09,0-19.47-6.24-19.47-15.93S456.14,130.37,467.78,130.37Zm15.47,12.11v-4H469.74c-5.59,0-9,2-9,6.33,0,4.57,4.29,7.27,10.25,7.27C477.66,152.08,483.25,148.82,483.25,142.48Z"/><path class="cls-1" d="M523.12,140.62c-10.34-.74-17.33-5.59-17.33-14.72,0-8.85,8.1-14.44,20.77-14.44,17,0,21.8,8.76,23.1,13.32H537.09c-.84-1-3.54-4.28-10.62-4.28-5.68,0-8.66,1.86-8.66,4.75,0,2.61,2,4.29,6.7,4.94l8,.84c12.77,1.11,18,6,18,15.27,0,8.85-7.36,14.91-21.8,14.91-17.51,0-23.19-10.16-24.12-14.53h12.76c.47,1.11,3.35,5.31,11.36,5.31,6.62,0,9.69-2.24,9.69-5.22s-1.67-4.66-7-5.31C528.05,141.18,526.38,141,523.12,140.62Z"/><path class="cls-1" d="M571.55,159.9H559.72V91.34h11.83v29.35a17.93,17.93,0,0,1,15.65-9c11.83,0,19.66,8.67,19.66,20.68V159.9h-12V134.75c0-7.45-4.38-12.39-11.46-12.39s-11.83,5-11.83,12.39Z"/></svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
frontend/src/assets/img/logo-dark.svg
Normal file
1
frontend/src/assets/img/logo-dark.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="bg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 251.43 251.03"><defs><style>.cls-1{fill:#1a4049;}.cls-2{fill:#fff;}.cls-3{fill:#817afe;}</style></defs><circle class="cls-1" cx="125.71" cy="125.31" r="80"/><polygon class="cls-2" points="137.14 91.03 137.14 113.88 137.14 136.74 160 136.74 160 113.88 160 91.03 137.14 91.03"/><polygon class="cls-2" points="114.29 113.88 114.29 91.03 91.43 91.03 91.43 113.88 91.43 136.74 91.43 159.6 114.29 159.6 137.14 159.6 137.14 136.74 114.29 136.74 114.29 113.88"/><rect class="cls-3" x="137.14" y="136.74" width="22.86" height="22.86"/></svg>
|
After Width: | Height: | Size: 593 B |
9
frontend/src/assets/img/logo-with-name.svg
Normal file
9
frontend/src/assets/img/logo-with-name.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 47 KiB |
14
frontend/src/assets/img/texture.svg
Normal file
14
frontend/src/assets/img/texture.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 127 KiB |
@ -69,6 +69,12 @@ export const useCommonStyles = makeStyles(theme => ({
|
|||||||
bottom: '40px',
|
bottom: '40px',
|
||||||
transform: 'translateY(400px)',
|
transform: 'translateY(400px)',
|
||||||
},
|
},
|
||||||
|
fadeInBottomStartWithoutFixed: {
|
||||||
|
opacity: '0',
|
||||||
|
right: '40px',
|
||||||
|
bottom: '40px',
|
||||||
|
transform: 'translateY(400px)',
|
||||||
|
},
|
||||||
fadeInBottomEnter: {
|
fadeInBottomEnter: {
|
||||||
transform: 'translateY(0)',
|
transform: 'translateY(0)',
|
||||||
opacity: '1',
|
opacity: '1',
|
||||||
|
@ -14,6 +14,25 @@ interface IPermission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
||||||
|
const isAdminHigherOrder = () => {
|
||||||
|
let called = false;
|
||||||
|
let result = false;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (called) return result;
|
||||||
|
const permissions = store.getState().user.get('permissions') || [];
|
||||||
|
result = permissions.some(
|
||||||
|
(p: IPermission) => p.permission === ADMIN
|
||||||
|
);
|
||||||
|
|
||||||
|
if (permissions.length > 0) {
|
||||||
|
called = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAdmin = isAdminHigherOrder();
|
||||||
|
|
||||||
const hasAccess = (permission: string, project: string) => {
|
const hasAccess = (permission: string, project: string) => {
|
||||||
const permissions = store.getState().user.get('permissions') || [];
|
const permissions = store.getState().user.get('permissions') || [];
|
||||||
|
|
||||||
@ -36,7 +55,7 @@ const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const context = { hasAccess };
|
const context = { hasAccess, isAdmin };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessContext.Provider value={context}>
|
<AccessContext.Provider value={context}>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export const ADMIN = 'ADMIN';
|
export const ADMIN = 'ADMIN';
|
||||||
|
export const EDITOR = 'EDITOR';
|
||||||
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
||||||
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
|
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
|
||||||
export const DELETE_FEATURE = 'DELETE_FEATURE';
|
export const DELETE_FEATURE = 'DELETE_FEATURE';
|
||||||
|
@ -32,7 +32,6 @@ const AddonFormComponent = ({
|
|||||||
}, [fetch, provider]); // empty array => fetch only first time
|
}, [fetch, provider]); // empty array => fetch only first time
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(addon);
|
|
||||||
setConfig({ ...addon });
|
setConfig({ ...addon });
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [addon.description, addon.provider]);
|
}, [addon.description, addon.provider]);
|
||||||
|
@ -24,6 +24,12 @@ exports[`renders correctly if no application 1`] = `
|
|||||||
exports[`renders correctly with permissions 1`] = `
|
exports[`renders correctly with permissions 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
@ -543,6 +549,12 @@ exports[`renders correctly with permissions 1`] = `
|
|||||||
exports[`renders correctly without permission 1`] = `
|
exports[`renders correctly without permission 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
breadcrumbNav: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '4px',
|
||||||
|
},
|
||||||
|
breadcrumbNavParagraph: { textTransform: 'capitalize', color: 'inherit' },
|
||||||
|
breadcrumbLink: {
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,54 @@
|
|||||||
|
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
|
import { useStyles } from './BreadcrumbNav.styles';
|
||||||
|
|
||||||
|
const BreadcrumbNav = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const paths = location.pathname
|
||||||
|
.split('/')
|
||||||
|
.filter(item => item)
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item !== 'create' &&
|
||||||
|
item !== 'edit' &&
|
||||||
|
item !== 'access' &&
|
||||||
|
item !== 'view' &&
|
||||||
|
item !== 'variants' &&
|
||||||
|
item !== 'logs' &&
|
||||||
|
item !== 'metrics' &&
|
||||||
|
item !== 'copy'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={paths.length > 1}
|
||||||
|
show={
|
||||||
|
<Breadcrumbs className={styles.breadcrumbNav}>
|
||||||
|
{paths.map((path, index) => {
|
||||||
|
const lastItem = index === paths.length - 1;
|
||||||
|
if (lastItem) {
|
||||||
|
return (
|
||||||
|
<p className={styles.breadcrumbNavParagraph}>
|
||||||
|
{path}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={styles.breadcrumbLink}
|
||||||
|
to={`/${path}`}
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreadcrumbNav;
|
@ -41,7 +41,11 @@ const PageContent = ({
|
|||||||
const paperProps = disableBorder ? { elevation: 0 } : {};
|
const paperProps = disableBorder ? { elevation: 0 } : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper {...rest} {...paperProps}>
|
<Paper
|
||||||
|
{...rest}
|
||||||
|
{...paperProps}
|
||||||
|
style={{ borderRadius: '10px', boxShadow: 'none' }}
|
||||||
|
>
|
||||||
{header}
|
{header}
|
||||||
<div className={bodyClasses}>{children}</div>
|
<div className={bodyClasses}>{children}</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useStyles } from './PaginationUI.styles';
|
import { useStyles } from './PaginationUI.styles';
|
||||||
@ -7,6 +7,7 @@ import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
|
|||||||
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
|
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
|
||||||
|
|
||||||
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
|
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
|
||||||
|
import { useMediaQuery, useTheme } from '@material-ui/core';
|
||||||
|
|
||||||
interface IPaginateUIProps {
|
interface IPaginateUIProps {
|
||||||
pages: any[];
|
pages: any[];
|
||||||
@ -24,9 +25,17 @@ const PaginateUI = ({
|
|||||||
nextPage,
|
nextPage,
|
||||||
}: IPaginateUIProps) => {
|
}: IPaginateUIProps) => {
|
||||||
const STARTLIMIT = 6;
|
const STARTLIMIT = 6;
|
||||||
|
const theme = useTheme();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [limit, setLimit] = useState(STARTLIMIT);
|
const [limit, setLimit] = useState(STARTLIMIT);
|
||||||
const [start, setStart] = useState(0);
|
const [start, setStart] = useState(0);
|
||||||
|
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (matches) {
|
||||||
|
setLimit(4);
|
||||||
|
}
|
||||||
|
}, [matches]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -23,7 +23,6 @@ const renderProclamation = (id: string) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('RETURNING TRUE');
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,12 +8,13 @@ interface IResponsiveButtonProps {
|
|||||||
maxWidth: string;
|
maxWidth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResponsiveButton = ({
|
const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||||
Icon,
|
Icon,
|
||||||
onClick,
|
onClick,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
tooltip,
|
tooltip,
|
||||||
}: IResponsiveButtonProps) => {
|
children,
|
||||||
|
}) => {
|
||||||
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
|
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -21,14 +22,19 @@ const ResponsiveButton = ({
|
|||||||
condition={smallScreen}
|
condition={smallScreen}
|
||||||
show={
|
show={
|
||||||
<Tooltip title={tooltip ? tooltip : ''}>
|
<Tooltip title={tooltip ? tooltip : ''}>
|
||||||
<IconButton onClick={onClick}>
|
<IconButton onClick={onClick} data-loading>
|
||||||
<Icon />
|
<Icon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Button onClick={onClick} color="primary" variant="contained">
|
<Button
|
||||||
Add new project
|
onClick={onClick}
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -5,7 +5,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: theme.palette.searchField.main,
|
backgroundColor: theme.palette.searchField.main,
|
||||||
borderRadius: theme.borders.radius.main,
|
borderRadius: '25px',
|
||||||
padding: '0.25rem 0.5rem',
|
padding: '0.25rem 0.5rem',
|
||||||
maxWidth: '450px',
|
maxWidth: '450px',
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
@ -24,7 +24,7 @@ const Toast = ({
|
|||||||
<Portal>
|
<Portal>
|
||||||
<AnimateOnMount
|
<AnimateOnMount
|
||||||
mounted={show}
|
mounted={show}
|
||||||
start={styles.fadeInBottomStart}
|
start={styles.fadeInBottomStartWithoutFixed}
|
||||||
enter={styles.fadeInBottomEnter}
|
enter={styles.fadeInBottomEnter}
|
||||||
leave={styles.fadeInBottomLeave}
|
leave={styles.fadeInBottomLeave}
|
||||||
container={styles.fullWidth}
|
container={styles.fullWidth}
|
||||||
@ -33,6 +33,7 @@ const Toast = ({
|
|||||||
open={show}
|
open={show}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
autoHideDuration={autoHideDuration}
|
autoHideDuration={autoHideDuration}
|
||||||
|
style={{ bottom: '40px' }}
|
||||||
>
|
>
|
||||||
<Alert variant="filled" severity={type} onClose={onClose}>
|
<Alert variant="filled" severity={type} onClose={onClose}>
|
||||||
{text}
|
{text}
|
||||||
|
@ -3,5 +3,4 @@ export const C = 'C';
|
|||||||
export const RBAC = 'RBAC';
|
export const RBAC = 'RBAC';
|
||||||
export const OIDC = 'OIDC';
|
export const OIDC = 'OIDC';
|
||||||
|
|
||||||
export const PROJECTCARDACTIONS = false;
|
|
||||||
export const PROJECTFILTERING = false;
|
export const PROJECTFILTERING = false;
|
||||||
|
@ -16,6 +16,13 @@ const dateOptions = {
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const filterByFlags = flags => r => {
|
||||||
|
if (r.flag && !flags[r.flag]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const scrollToTop = () => {
|
export const scrollToTop = () => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,8 @@ import { makeStyles } from '@material-ui/styles';
|
|||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
typeChip: {
|
typeChip: {
|
||||||
margin: '0 8px',
|
margin: '0 8px',
|
||||||
boxShadow: theme.boxShadows.chip.main,
|
background: 'transparent',
|
||||||
backgroundColor: theme.palette.chips.main,
|
border: `1px solid ${theme.palette.primary.main}`,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -41,6 +41,12 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-8"
|
className="makeStyles-headerContainer-8"
|
||||||
@ -278,6 +284,12 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-8"
|
className="makeStyles-headerContainer-8"
|
||||||
|
@ -11,6 +11,11 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
tableCellHeader: {
|
tableCellHeader: {
|
||||||
paddingBottom: '0.5rem',
|
paddingBottom: '0.5rem',
|
||||||
},
|
},
|
||||||
|
typeHeader: {
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
tableCellName: {
|
tableCellName: {
|
||||||
width: '250px',
|
width: '250px',
|
||||||
},
|
},
|
||||||
@ -20,6 +25,9 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
tableCellType: {
|
tableCellType: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
marginRight: '0.3rem',
|
marginRight: '0.3rem',
|
||||||
|
@ -89,7 +89,8 @@ const FeatureToggleListNew = ({
|
|||||||
<TableCell
|
<TableCell
|
||||||
className={classnames(
|
className={classnames(
|
||||||
styles.tableCell,
|
styles.tableCell,
|
||||||
styles.tableCellHeader
|
styles.tableCellHeader,
|
||||||
|
styles.typeHeader
|
||||||
)}
|
)}
|
||||||
align="left"
|
align="left"
|
||||||
>
|
>
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Switch, TableCell, TableRow } from '@material-ui/core';
|
import {
|
||||||
|
Switch,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from '@material-ui/core';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { getFeatureTypeIcons } from '../../../../utils/get-feature-type-icons';
|
import { getFeatureTypeIcons } from '../../../../utils/get-feature-type-icons';
|
||||||
import { useStyles } from '../FeatureToggleListNew.styles';
|
import { useStyles } from '../FeatureToggleListNew.styles';
|
||||||
import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv';
|
import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv';
|
||||||
import { IEnvironments } from '../../../../interfaces/featureToggle';
|
import { IEnvironments } from '../../../../interfaces/featureToggle';
|
||||||
import Toast from '../../../common/Toast/Toast';
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
|
||||||
interface IFeatureToggleListNewItemProps {
|
interface IFeatureToggleListNewItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -20,15 +27,14 @@ const FeatureToggleListNewItem = ({
|
|||||||
environments,
|
environments,
|
||||||
projectId,
|
projectId,
|
||||||
}: IFeatureToggleListNewItemProps) => {
|
}: IFeatureToggleListNewItemProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { toast, setToastData } = useToast();
|
||||||
|
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const { toggleFeatureByEnvironment } = useToggleFeatureByEnv(
|
const { toggleFeatureByEnvironment } = useToggleFeatureByEnv(
|
||||||
projectId,
|
projectId,
|
||||||
name
|
name
|
||||||
);
|
);
|
||||||
const [snackbarData, setSnackbardata] = useState({
|
|
||||||
show: false,
|
|
||||||
type: 'success',
|
|
||||||
text: '',
|
|
||||||
});
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@ -42,14 +48,14 @@ const FeatureToggleListNewItem = ({
|
|||||||
const handleToggle = (env: IEnvironments) => {
|
const handleToggle = (env: IEnvironments) => {
|
||||||
toggleFeatureByEnvironment(env.name, env.enabled)
|
toggleFeatureByEnvironment(env.name, env.enabled)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setSnackbardata({
|
setToastData({
|
||||||
show: true,
|
show: true,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
text: 'Successfully updated toggle status.',
|
text: 'Successfully updated toggle status.',
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
setSnackbardata({
|
setToastData({
|
||||||
show: true,
|
show: true,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: e.toString(),
|
text: e.toString(),
|
||||||
@ -57,10 +63,6 @@ const FeatureToggleListNewItem = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideSnackbar = () => {
|
|
||||||
setSnackbardata(prev => ({ ...prev, show: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconComponent = getFeatureTypeIcons(type);
|
const IconComponent = getFeatureTypeIcons(type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -69,12 +71,21 @@ const FeatureToggleListNewItem = ({
|
|||||||
<TableCell className={styles.tableCell} align="left">
|
<TableCell className={styles.tableCell} align="left">
|
||||||
<span data-loading>{name}</span>
|
<span data-loading>{name}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={styles.tableCell} align="left">
|
<ConditionallyRender
|
||||||
<div className={styles.tableCellType}>
|
condition={!smallScreen}
|
||||||
<IconComponent data-loading className={styles.icon} />{' '}
|
show={
|
||||||
<span data-loading>{type}</span>
|
<TableCell className={styles.tableCell} align="left">
|
||||||
</div>
|
<div className={styles.tableCellType}>
|
||||||
</TableCell>
|
<IconComponent
|
||||||
|
data-loading
|
||||||
|
className={styles.icon}
|
||||||
|
/>{' '}
|
||||||
|
<span data-loading>{type}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{environments.map((env: IEnvironments) => {
|
{environments.map((env: IEnvironments) => {
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -93,12 +104,7 @@ const FeatureToggleListNewItem = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<Toast
|
{toast}
|
||||||
show={snackbarData.show}
|
|
||||||
onClose={hideSnackbar}
|
|
||||||
text={snackbarData.text}
|
|
||||||
type={snackbarData.type}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -222,7 +222,11 @@ const FeatureView = ({
|
|||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
className={commonStyles.fullwidth}
|
className={commonStyles.fullwidth}
|
||||||
style={{ overflow: 'visible' }}
|
style={{
|
||||||
|
overflow: 'visible',
|
||||||
|
borderRadius: '10px',
|
||||||
|
boxShadow: 'none',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
} from '../../../../testIds';
|
} from '../../../../testIds';
|
||||||
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||||
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
|
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import useQueryParams from '../../../../hooks/useQueryParams';
|
||||||
|
|
||||||
const CreateFeature = ({
|
const CreateFeature = ({
|
||||||
input,
|
input,
|
||||||
@ -25,9 +27,19 @@ const CreateFeature = ({
|
|||||||
setValue,
|
setValue,
|
||||||
validateName,
|
validateName,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
|
||||||
user,
|
user,
|
||||||
}) => {
|
}) => {
|
||||||
|
const params = useQueryParams();
|
||||||
|
const project = params.get('project');
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (project) {
|
||||||
|
setValue('project', project);
|
||||||
|
}
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.onbeforeunload = () =>
|
window.onbeforeunload = () =>
|
||||||
'Data will be lost if you leave the page, are you sure?';
|
'Data will be lost if you leave the page, are you sure?';
|
||||||
@ -37,6 +49,8 @@ const CreateFeature = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onCancel = () => history.goBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent="Create new feature toggle">
|
<PageContent headerContent="Create new feature toggle">
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
@ -73,7 +87,7 @@ const CreateFeature = ({
|
|||||||
</div>
|
</div>
|
||||||
<section className={styles.formContainer}>
|
<section className={styles.formContainer}>
|
||||||
<ProjectSelect
|
<ProjectSelect
|
||||||
value={input.project}
|
value={project || input.project}
|
||||||
onChange={v => setValue('project', v.target.value)}
|
onChange={v => setValue('project', v.target.value)}
|
||||||
filter={projectFilterGenerator(user, CREATE_FEATURE)}
|
filter={projectFilterGenerator(user, CREATE_FEATURE)}
|
||||||
/>
|
/>
|
||||||
|
@ -4,9 +4,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
strategyCard: {
|
strategyCard: {
|
||||||
width: '337px',
|
width: '337px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
[theme.breakpoints.down('xs')]: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
[theme.breakpoints.down('1250')]: {
|
[theme.breakpoints.down('1250')]: {
|
||||||
width: '300px',
|
width: '300px',
|
||||||
},
|
},
|
||||||
@ -16,5 +14,8 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
[theme.breakpoints.down('860')]: {
|
[theme.breakpoints.down('860')]: {
|
||||||
width: '380px',
|
width: '380px',
|
||||||
},
|
},
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -9,7 +9,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
strategyCardHeader: {
|
strategyCardHeader: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
background: `linear-gradient(${theme.palette.cards.gradient.top}, ${theme.palette.cards.gradient.bottom})`,
|
background: theme.palette.primary.dark,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import arrayMove from 'array-move';
|
import arrayMove from 'array-move';
|
||||||
import { Button } from '@material-ui/core';
|
|
||||||
|
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
@ -14,6 +13,8 @@ import EditStrategyModal from './EditStrategyModal/EditStrategyModal';
|
|||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import CreateStrategy from './AddStrategy/AddStrategy';
|
import CreateStrategy from './AddStrategy/AddStrategy';
|
||||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||||
|
import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import { Add } from '@material-ui/icons';
|
||||||
|
|
||||||
const cleanStrategy = strategy => ({
|
const cleanStrategy = strategy => ({
|
||||||
name: strategy.name,
|
name: strategy.name,
|
||||||
@ -110,12 +111,8 @@ const StrategiesList = props => {
|
|||||||
setEditStrategyIndex(undefined);
|
setEditStrategyIndex(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { strategies, configuredStrategies, featureToggleName, editable } =
|
||||||
strategies,
|
props;
|
||||||
configuredStrategies,
|
|
||||||
featureToggleName,
|
|
||||||
editable,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const resolveStrategyDefinition = strategyName => {
|
const resolveStrategyDefinition = strategyName => {
|
||||||
if (!strategies || strategies.length === 0) {
|
if (!strategies || strategies.length === 0) {
|
||||||
@ -170,14 +167,14 @@ const StrategiesList = props => {
|
|||||||
title="Activation strategies"
|
title="Activation strategies"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button
|
<ResponsiveButton
|
||||||
variant="contained"
|
|
||||||
disabled={!featureToggleName}
|
|
||||||
color="primary"
|
|
||||||
onClick={() => setShowCreateStrategy(true)}
|
onClick={() => setShowCreateStrategy(true)}
|
||||||
|
maxWidth="700px"
|
||||||
|
tooltip="Add strategy"
|
||||||
|
Icon={Add}
|
||||||
>
|
>
|
||||||
Add strategy
|
Add strategy
|
||||||
</Button>
|
</ResponsiveButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -497,10 +497,10 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-22 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-23 PrivateNotchedOutline-legendNotched-24"
|
className="PrivateNotchedOutline-legendLabelled-24 PrivateNotchedOutline-legendNotched-25"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Stickiness
|
Stickiness
|
||||||
|
@ -5,6 +5,8 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
"overflow": "visible",
|
"overflow": "visible",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,10 +142,10 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="PrivateNotchedOutline-root-19 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-20 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-21"
|
className="PrivateNotchedOutline-legendLabelled-22"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Project
|
Project
|
||||||
@ -175,7 +177,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-23 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-24 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
@ -194,7 +196,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={false}
|
checked={false}
|
||||||
className="PrivateSwitchBase-input-26 MuiSwitch-input"
|
className="PrivateSwitchBase-input-27 MuiSwitch-input"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -327,7 +329,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root makeStyles-tabNav-27 MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root makeStyles-tabNav-28 MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiTabs-root"
|
className="MuiTabs-root"
|
||||||
@ -375,7 +377,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
Activation
|
Activation
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="PrivateTabIndicator-root-28 PrivateTabIndicator-colorPrimary-29 MuiTabs-indicator"
|
className="PrivateTabIndicator-root-29 PrivateTabIndicator-colorPrimary-30 MuiTabs-indicator"
|
||||||
style={Object {}}
|
style={Object {}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,15 +6,24 @@ import { Grid } from '@material-ui/core';
|
|||||||
|
|
||||||
import styles from '../../styles.module.scss';
|
import styles from '../../styles.module.scss';
|
||||||
import ErrorContainer from '../../error/error-container';
|
import ErrorContainer from '../../error/error-container';
|
||||||
import Header from '../../menu/Header';
|
import Header from '../../menu/Header/Header';
|
||||||
import Footer from '../../menu/Footer/Footer';
|
import Footer from '../../menu/Footer/Footer';
|
||||||
import Proclamation from '../../common/Proclamation/Proclamation';
|
import Proclamation from '../../common/Proclamation/Proclamation';
|
||||||
|
import BreadcrumbNav from '../../common/BreadcrumbNav/BreadcrumbNav';
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
container: {
|
container: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
|
contentContainer: {
|
||||||
|
height: '100%',
|
||||||
|
padding: '3.25rem 0',
|
||||||
|
position: 'relative',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
padding: '3.25rem 0.75rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const MainLayout = ({ children, location, uiConfig }) => {
|
const MainLayout = ({ children, location, uiConfig }) => {
|
||||||
@ -26,7 +35,8 @@ const MainLayout = ({ children, location, uiConfig }) => {
|
|||||||
<Grid container className={muiStyles.container}>
|
<Grid container className={muiStyles.container}>
|
||||||
<div className={classnames(styles.contentWrapper)}>
|
<div className={classnames(styles.contentWrapper)}>
|
||||||
<Grid item className={styles.content} xs={12} sm={12}>
|
<Grid item className={styles.content} xs={12} sm={12}>
|
||||||
<div className={styles.contentContainer}>
|
<div className={muiStyles.contentContainer}>
|
||||||
|
<BreadcrumbNav />
|
||||||
<Proclamation toast={uiConfig.toast} />
|
<Proclamation toast={uiConfig.toast} />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
|
||||||
import { useTheme } from '@material-ui/core/styles';
|
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
AppBar,
|
|
||||||
Container,
|
|
||||||
Typography,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import { DrawerMenu } from '../drawer';
|
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
|
||||||
import Breadcrumb from '../breadcrumb';
|
|
||||||
import UserProfile from '../../user/UserProfile';
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
|
||||||
import { useStyles } from './styles';
|
|
||||||
|
|
||||||
const Header = ({ uiConfig }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
|
||||||
const styles = useStyles();
|
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
|
|
||||||
const toggleDrawer = () => setOpenDrawer(prev => !prev);
|
|
||||||
|
|
||||||
const { links, name, flags } = uiConfig;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<AppBar className={styles.header} position="static">
|
|
||||||
<Container className={styles.container}>
|
|
||||||
<IconButton
|
|
||||||
className={styles.drawerButton}
|
|
||||||
onClick={toggleDrawer}
|
|
||||||
>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!smallScreen}
|
|
||||||
show={
|
|
||||||
<Typography
|
|
||||||
variant="h1"
|
|
||||||
className={styles.headerTitle}
|
|
||||||
>
|
|
||||||
<Route path="/:path" component={Breadcrumb} />
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.userContainer}>
|
|
||||||
<Tooltip title="Go to the documentation">
|
|
||||||
<a
|
|
||||||
href="https://docs.getunleash.io/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={styles.docsLink}
|
|
||||||
>
|
|
||||||
<MenuBookIcon className={styles.docsIcon} />
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
<UserProfile />
|
|
||||||
</div>
|
|
||||||
<DrawerMenu
|
|
||||||
links={links}
|
|
||||||
title={name}
|
|
||||||
flags={flags}
|
|
||||||
open={openDrawer}
|
|
||||||
toggleDrawer={toggleDrawer}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
</AppBar>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Header.propTypes = {
|
|
||||||
uiConfig: PropTypes.object.isRequired,
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
88
frontend/src/component/menu/Header/Header.styles.ts
Normal file
88
frontend/src/component/menu/Header/Header.styles.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
header: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: '#000',
|
||||||
|
padding: '0.5rem',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginLeft: '1.5rem',
|
||||||
|
['& a']: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
marginRight: '1.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drawerButton: {
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
advancedNavButton: {
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'inherit',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: '1.4rem',
|
||||||
|
},
|
||||||
|
userContainer: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
logoOnly: {
|
||||||
|
width: '60px',
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
width: '150px',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
top: '25px',
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
minWidth: '150px',
|
||||||
|
},
|
||||||
|
menuItemBox: {
|
||||||
|
width: '12.5px',
|
||||||
|
height: '12.5px',
|
||||||
|
display: 'block',
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
marginRight: '1rem',
|
||||||
|
borderRadius: '2px',
|
||||||
|
},
|
||||||
|
navMenuLink: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
docsLink: {
|
||||||
|
color: '#000',
|
||||||
|
textDecoration: 'none',
|
||||||
|
padding: '0.25rem 0.8rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
docsIcon: {
|
||||||
|
color: '#6C6C6C',
|
||||||
|
height: '25px',
|
||||||
|
width: '25px',
|
||||||
|
},
|
||||||
|
}));
|
172
frontend/src/component/menu/Header/Header.tsx
Normal file
172
frontend/src/component/menu/Header/Header.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
|
import { useTheme } from '@material-ui/core/styles';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { AppBar, Container, IconButton, Tooltip } from '@material-ui/core';
|
||||||
|
import { DrawerMenu } from '../drawer';
|
||||||
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
|
import UserProfile from '../../user/UserProfile';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||||
|
import { ReactComponent as UnleashLogo } from '../../../assets/img/logo-dark-with-text.svg';
|
||||||
|
|
||||||
|
import { useStyles } from './Header.styles';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import { ADMIN } from '../../AccessProvider/permissions';
|
||||||
|
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||||
|
import { IPermission } from '../../../interfaces/user';
|
||||||
|
import NavigationMenu from './NavigationMenu/NavigationMenu';
|
||||||
|
import { getRoutes } from '../routes';
|
||||||
|
import { KeyboardArrowDown } from '@material-ui/icons';
|
||||||
|
import { filterByFlags } from '../../common/util';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [anchorEl, setAnchorEl] = useState();
|
||||||
|
const [anchorElAdvanced, setAnchorElAdvanced] = useState();
|
||||||
|
const [admin, setAdmin] = useState(false);
|
||||||
|
const { permissions } = useUser();
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
const styles = useStyles();
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
|
||||||
|
const toggleDrawer = () => setOpenDrawer(prev => !prev);
|
||||||
|
const handleClose = () => setAnchorEl(null);
|
||||||
|
const handleCloseAdvanced = () => setAnchorElAdvanced(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const admin = permissions.find(
|
||||||
|
(element: IPermission) => element.permission === ADMIN
|
||||||
|
);
|
||||||
|
|
||||||
|
if (admin) {
|
||||||
|
setAdmin(true);
|
||||||
|
}
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
const { links, name, flags } = uiConfig;
|
||||||
|
const routes = getRoutes();
|
||||||
|
|
||||||
|
const filteredMainRoutes = {
|
||||||
|
mainNavRoutes: routes.mainNavRoutes.filter(filterByFlags(flags)),
|
||||||
|
adminRoutes: routes.adminRoutes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppBar className={styles.header} position="static">
|
||||||
|
<Container className={styles.container}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={smallScreen}
|
||||||
|
show={
|
||||||
|
<IconButton
|
||||||
|
className={styles.drawerButton}
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Link to="/" className={commonStyles.flexRow}>
|
||||||
|
<UnleashLogo className={styles.logo} />{' '}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerMenu
|
||||||
|
title={name}
|
||||||
|
flags={flags}
|
||||||
|
links={links}
|
||||||
|
open={openDrawer}
|
||||||
|
toggleDrawer={toggleDrawer}
|
||||||
|
admin={admin}
|
||||||
|
routes={filteredMainRoutes}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!smallScreen}
|
||||||
|
show={
|
||||||
|
<div className={styles.links}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={flags?.P}
|
||||||
|
show={<Link to="/projects">Projects</Link>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={styles.advancedNavButton}
|
||||||
|
onClick={e =>
|
||||||
|
setAnchorElAdvanced(e.currentTarget)
|
||||||
|
}
|
||||||
|
onMouseEnter={e =>
|
||||||
|
setAnchorElAdvanced(e.currentTarget)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Navigate
|
||||||
|
<KeyboardArrowDown />
|
||||||
|
</button>
|
||||||
|
<NavigationMenu
|
||||||
|
id="settings-navigation"
|
||||||
|
options={filteredMainRoutes.mainNavRoutes}
|
||||||
|
anchorEl={anchorElAdvanced}
|
||||||
|
handleClose={handleCloseAdvanced}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={styles.userContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!smallScreen}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Tooltip title="Go to the documentation">
|
||||||
|
<a
|
||||||
|
href="https://docs.getunleash.io/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={styles.docsLink}
|
||||||
|
>
|
||||||
|
<MenuBookIcon
|
||||||
|
className={styles.docsIcon}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={admin}
|
||||||
|
show={
|
||||||
|
<IconButton
|
||||||
|
onClick={e =>
|
||||||
|
setAnchorEl(e.currentTarget)
|
||||||
|
}
|
||||||
|
onMouseEnter={e =>
|
||||||
|
setAnchorEl(e.currentTarget)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsIcon
|
||||||
|
className={styles.docsIcon}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NavigationMenu
|
||||||
|
id="admin-navigation"
|
||||||
|
options={routes.adminRoutes}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
handleClose={handleClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UserProfile />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</AppBar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
menuItem: {
|
||||||
|
minWidth: '150px',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
margin: '0',
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
menuItemBox: {
|
||||||
|
width: '12.5px',
|
||||||
|
height: '12.5px',
|
||||||
|
display: 'block',
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
marginRight: '1rem',
|
||||||
|
borderRadius: '2px',
|
||||||
|
},
|
||||||
|
navMenuLink: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
color: '#000',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,35 @@
|
|||||||
|
import { ListItem, Link } from '@material-ui/core';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useStyles } from './NavigationLink.styles';
|
||||||
|
|
||||||
|
interface INavigationLinkProps {
|
||||||
|
path: string;
|
||||||
|
text: string;
|
||||||
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigationLink = ({ path, text, handleClose }: INavigationLinkProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={() => {
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
component={RouterLink}
|
||||||
|
className={styles.navMenuLink}
|
||||||
|
to={path}
|
||||||
|
>
|
||||||
|
<span className={styles.menuItemBox} />
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigationLink;
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Popover, List } from '@material-ui/core';
|
||||||
|
import NavigationLink from '../NavigationLink/NavigationLink';
|
||||||
|
|
||||||
|
interface INavigationMenuProps {
|
||||||
|
options: any[];
|
||||||
|
id: string;
|
||||||
|
anchorEl: any;
|
||||||
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigationMenu = ({
|
||||||
|
options,
|
||||||
|
id,
|
||||||
|
handleClose,
|
||||||
|
anchorEl,
|
||||||
|
}: INavigationMenuProps) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
id={id}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onMouseLeave={handleClose}
|
||||||
|
style={{ top: '30px', left: '-90px' }}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{options.map(option => {
|
||||||
|
return (
|
||||||
|
<NavigationLink
|
||||||
|
key={option.path}
|
||||||
|
handleClose={handleClose}
|
||||||
|
path={option.path}
|
||||||
|
text={option.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigationMenu;
|
@ -1,7 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Header from './Header';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({ uiConfig: state.uiConfig.toJS() });
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Header);
|
|
@ -1,40 +0,0 @@
|
|||||||
import { makeStyles } from '@material-ui/styles';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles(theme => ({
|
|
||||||
header: {
|
|
||||||
padding: '1rem',
|
|
||||||
boxShadow: 'none',
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
padding: '1rem 0.5rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
padding: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
drawerButton: {
|
|
||||||
color: theme.palette.iconButtons.main,
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: '1.4rem',
|
|
||||||
},
|
|
||||||
userContainer: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
docsLink: {
|
|
||||||
color: '#fff',
|
|
||||||
textDecoration: 'none',
|
|
||||||
padding: '0.25rem 0.8rem',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
docsIcon: {
|
|
||||||
height: '25px',
|
|
||||||
width: '25px',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,5 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`should render DrawerMenu 1`] = `null`;
|
|
||||||
|
|
||||||
exports[`should render DrawerMenu with "features" selected 1`] = `null`;
|
|
@ -4,14 +4,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/features",
|
"path": "/features",
|
||||||
"title": "Feature Toggles",
|
"title": "Feature Toggles",
|
||||||
@ -19,14 +11,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/strategies",
|
"path": "/strategies",
|
||||||
"title": "Strategies",
|
"title": "Strategies",
|
||||||
@ -34,14 +18,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/history",
|
"path": "/history",
|
||||||
"title": "Event History",
|
"title": "Event History",
|
||||||
@ -49,14 +25,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/archive",
|
"path": "/archive",
|
||||||
"title": "Archived Toggles",
|
"title": "Archived Toggles",
|
||||||
@ -64,14 +32,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/applications",
|
"path": "/applications",
|
||||||
"title": "Applications",
|
"title": "Applications",
|
||||||
@ -80,14 +40,6 @@ Array [
|
|||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"flag": "C",
|
"flag": "C",
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/context",
|
"path": "/context",
|
||||||
"title": "Context Fields",
|
"title": "Context Fields",
|
||||||
@ -96,14 +48,6 @@ Array [
|
|||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"flag": "P",
|
"flag": "P",
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/projects",
|
"path": "/projects",
|
||||||
"title": "Projects",
|
"title": "Projects",
|
||||||
@ -111,14 +55,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/tag-types",
|
"path": "/tag-types",
|
||||||
"title": "Tag types",
|
"title": "Tag types",
|
||||||
@ -127,14 +63,6 @@ Array [
|
|||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/addons",
|
"path": "/addons",
|
||||||
"title": "Addons",
|
"title": "Addons",
|
||||||
@ -142,14 +70,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/reporting",
|
"path": "/reporting",
|
||||||
"title": "Reporting",
|
"title": "Reporting",
|
||||||
@ -158,14 +78,6 @@ Array [
|
|||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/admin",
|
"path": "/admin",
|
||||||
"title": "Admin",
|
"title": "Admin",
|
||||||
@ -173,14 +85,6 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"compare": null,
|
|
||||||
"type": Object {
|
|
||||||
"$$typeof": Symbol(react.forward_ref),
|
|
||||||
"render": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
"path": "/logout",
|
"path": "/logout",
|
||||||
"title": "Sign out",
|
"title": "Sign out",
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { DrawerMenu } from '../drawer';
|
|
||||||
|
|
||||||
jest.mock('@material-ui/core');
|
|
||||||
|
|
||||||
test('should render DrawerMenu', () => {
|
|
||||||
const tree = renderer.create(
|
|
||||||
<MemoryRouter>
|
|
||||||
<DrawerMenu />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render DrawerMenu with "features" selected', () => {
|
|
||||||
const tree = renderer.create(
|
|
||||||
<MemoryRouter initialEntries={['/features']}>
|
|
||||||
<DrawerMenu />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
@ -1,61 +1,13 @@
|
|||||||
import { Divider, Drawer, List } from '@material-ui/core';
|
import { Divider, Drawer, List } from '@material-ui/core';
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||||
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
|
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
|
||||||
|
|
||||||
import styles from './drawer.module.scss';
|
import styles from './drawer.module.scss';
|
||||||
|
|
||||||
import { baseRoutes as routes } from './routes';
|
|
||||||
|
|
||||||
import { ReactComponent as LogoIcon } from '../../assets/icons/logo_wbg.svg';
|
import { ReactComponent as LogoIcon } from '../../assets/icons/logo_wbg.svg';
|
||||||
|
import NavigationLink from './Header/NavigationLink/NavigationLink';
|
||||||
const filterByFlags = flags => r => {
|
import ConditionallyRender from '../common/ConditionallyRender';
|
||||||
if (r.flag && !flags[r.flag]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getIcon(IconComponent) {
|
|
||||||
if (IconComponent === 'c_github') {
|
|
||||||
return <GitHubIcon className={classnames(styles.navigationIcon)} />;
|
|
||||||
} else if (IconComponent === 'library_books') {
|
|
||||||
return <LibraryBooksIcon className={styles.navigationIcon} />;
|
|
||||||
} else {
|
|
||||||
return <IconComponent className={styles.navigationIcon} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLink(link, toggleDrawer) {
|
|
||||||
if (link.path) {
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
onClick={() => toggleDrawer()}
|
|
||||||
key={link.path}
|
|
||||||
to={link.path}
|
|
||||||
className={classnames(styles.navigationLink)}
|
|
||||||
activeClassName={classnames(styles.navigationLinkActive)}
|
|
||||||
>
|
|
||||||
{getIcon(link.icon)} {link.value}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
key={link.href}
|
|
||||||
target="_blank"
|
|
||||||
className={[styles.navigationLink].join(' ')}
|
|
||||||
title={link.title}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{getIcon(link.icon)} {link.value}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DrawerMenu = ({
|
export const DrawerMenu = ({
|
||||||
links = [],
|
links = [],
|
||||||
@ -63,45 +15,84 @@ export const DrawerMenu = ({
|
|||||||
flags = {},
|
flags = {},
|
||||||
open = false,
|
open = false,
|
||||||
toggleDrawer,
|
toggleDrawer,
|
||||||
}) => (
|
admin,
|
||||||
<Drawer
|
routes,
|
||||||
className={styles.drawer}
|
}) => {
|
||||||
open={open}
|
const renderLinks = () => {
|
||||||
anchor={'left'}
|
return links.map(link => {
|
||||||
onClose={() => toggleDrawer()}
|
let icon = null;
|
||||||
>
|
if (link.value === 'GitHub') {
|
||||||
<div className={styles.drawerContainer}>
|
icon = <GitHubIcon className={styles.navigationIcon} />;
|
||||||
<div>
|
} else if (link.value === 'Documentation') {
|
||||||
<span className={[styles.drawerTitle].join(' ')}>
|
icon = <LibraryBooksIcon className={styles.navigationIcon} />;
|
||||||
<LogoIcon className={styles.drawerTitleLogo} />
|
}
|
||||||
|
|
||||||
<span className={styles.drawerTitleText}>{title}</span>
|
return (
|
||||||
</span>
|
<a
|
||||||
|
href={link.href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className={styles.iconLink}
|
||||||
|
key={link.value}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{link.value}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className={styles.drawer}
|
||||||
|
open={open}
|
||||||
|
anchor={'left'}
|
||||||
|
onClose={() => toggleDrawer()}
|
||||||
|
>
|
||||||
|
<div className={styles.drawerContainer}>
|
||||||
|
<div>
|
||||||
|
<span className={[styles.drawerTitle].join(' ')}>
|
||||||
|
<LogoIcon className={styles.drawerTitleLogo} />
|
||||||
|
|
||||||
|
<span className={styles.drawerTitleText}>{title}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<List className={styles.drawerList}>
|
||||||
|
{routes.mainNavRoutes.map(item => (
|
||||||
|
<NavigationLink
|
||||||
|
handleClose={() => toggleDrawer()}
|
||||||
|
path={item.path}
|
||||||
|
text={item.title}
|
||||||
|
key={item.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={admin}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<List className={styles.drawerList}>
|
||||||
|
{routes.adminRoutes.map(item => (
|
||||||
|
<NavigationLink
|
||||||
|
handleClose={() => toggleDrawer()}
|
||||||
|
path={item.path}
|
||||||
|
text={item.title}
|
||||||
|
key={item.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<div className={styles.iconLinkList}>{renderLinks()}</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
</Drawer>
|
||||||
<List className={styles.drawerList}>
|
);
|
||||||
{routes.filter(filterByFlags(flags)).map(item => (
|
};
|
||||||
<NavLink
|
|
||||||
onClick={() => toggleDrawer()}
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
className={classnames(styles.navigationLink)}
|
|
||||||
activeClassName={classnames(
|
|
||||||
styles.navigationLinkActive
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{getIcon(item.icon)}
|
|
||||||
{item.title}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
<Divider />
|
|
||||||
<List className={styles.navigation}>
|
|
||||||
{links.map(l => renderLink(l, toggleDrawer))}
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
|
|
||||||
DrawerMenu.propTypes = {
|
DrawerMenu.propTypes = {
|
||||||
links: PropTypes.array,
|
links: PropTypes.array,
|
||||||
|
@ -22,10 +22,19 @@
|
|||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawerList {
|
.drawerList,
|
||||||
|
.iconLinkList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: centre;
|
align-items: centre;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconLink {
|
||||||
|
display: flex;
|
||||||
|
padding: 0.8rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigationLink {
|
.navigationLink {
|
||||||
@ -44,6 +53,7 @@
|
|||||||
|
|
||||||
.navigationIcon {
|
.navigationIcon {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
|
fill: #635dc5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconGitHub {
|
.iconGitHub {
|
||||||
|
@ -15,7 +15,6 @@ import ContextFields from '../../page/context';
|
|||||||
import CreateContextField from '../../page/context/create';
|
import CreateContextField from '../../page/context/create';
|
||||||
import EditContextField from '../../page/context/edit';
|
import EditContextField from '../../page/context/edit';
|
||||||
import LogoutFeatures from '../../page/user/logout';
|
import LogoutFeatures from '../../page/user/logout';
|
||||||
import ListProjects from '../../page/project';
|
|
||||||
import CreateProject from '../../page/project/create';
|
import CreateProject from '../../page/project/create';
|
||||||
import EditProject from '../../page/project/edit';
|
import EditProject from '../../page/project/edit';
|
||||||
import ViewProject from '../../page/project/view';
|
import ViewProject from '../../page/project/view';
|
||||||
@ -39,25 +38,9 @@ import { P, C } from '../common/flags';
|
|||||||
import NewUser from '../user/NewUser';
|
import NewUser from '../user/NewUser';
|
||||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||||
import ProjectListNew from '../project/ProjectListNew/ProjectListNew';
|
import ProjectListNew from '../project/ProjectList/ProjectList';
|
||||||
import Project from '../project/Project/Project';
|
import Project from '../project/Project/Project';
|
||||||
|
|
||||||
import {
|
|
||||||
List,
|
|
||||||
Extension,
|
|
||||||
History,
|
|
||||||
Archive as ArchiveIcon,
|
|
||||||
Apps,
|
|
||||||
Label,
|
|
||||||
DeviceHub,
|
|
||||||
Album,
|
|
||||||
ExitToApp,
|
|
||||||
FolderOpen,
|
|
||||||
Report,
|
|
||||||
Money,
|
|
||||||
Person,
|
|
||||||
} from '@material-ui/icons';
|
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Features
|
// Features
|
||||||
{
|
{
|
||||||
@ -87,7 +70,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/features',
|
path: '/features',
|
||||||
title: 'Feature Toggles',
|
title: 'Feature Toggles',
|
||||||
icon: List,
|
|
||||||
component: Features,
|
component: Features,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -113,7 +95,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/strategies',
|
path: '/strategies',
|
||||||
title: 'Strategies',
|
title: 'Strategies',
|
||||||
icon: Extension,
|
|
||||||
component: Strategies,
|
component: Strategies,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -131,7 +112,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/history',
|
path: '/history',
|
||||||
title: 'Event History',
|
title: 'Event History',
|
||||||
icon: History,
|
|
||||||
component: HistoryPage,
|
component: HistoryPage,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -149,7 +129,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/archive',
|
path: '/archive',
|
||||||
title: 'Archived Toggles',
|
title: 'Archived Toggles',
|
||||||
icon: ArchiveIcon,
|
|
||||||
component: Archive,
|
component: Archive,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -167,7 +146,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/applications',
|
path: '/applications',
|
||||||
title: 'Applications',
|
title: 'Applications',
|
||||||
icon: Apps,
|
|
||||||
component: Applications,
|
component: Applications,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -193,7 +171,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/context',
|
path: '/context',
|
||||||
title: 'Context Fields',
|
title: 'Context Fields',
|
||||||
icon: Album,
|
|
||||||
component: ContextFields,
|
component: ContextFields,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
flag: C,
|
flag: C,
|
||||||
@ -245,20 +222,9 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/projects',
|
path: '/projects',
|
||||||
title: 'Projects',
|
title: 'Projects',
|
||||||
icon: FolderOpen,
|
|
||||||
component: ListProjects,
|
|
||||||
flag: P,
|
|
||||||
type: 'protected',
|
|
||||||
layout: 'main',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/projects-new',
|
|
||||||
title: 'Projects new',
|
|
||||||
icon: 'folder_open',
|
|
||||||
component: ProjectListNew,
|
component: ProjectListNew,
|
||||||
flag: P,
|
flag: P,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
hidden: true,
|
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -281,7 +247,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/tag-types',
|
path: '/tag-types',
|
||||||
title: 'Tag types',
|
title: 'Tag types',
|
||||||
icon: Label,
|
|
||||||
component: ListTagTypes,
|
component: ListTagTypes,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -298,7 +263,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/tags',
|
path: '/tags',
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
icon: Label,
|
|
||||||
component: ListTags,
|
component: ListTags,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
@ -325,7 +289,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/addons',
|
path: '/addons',
|
||||||
title: 'Addons',
|
title: 'Addons',
|
||||||
icon: DeviceHub,
|
|
||||||
component: Addons,
|
component: Addons,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
@ -334,7 +297,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/reporting',
|
path: '/reporting',
|
||||||
title: 'Reporting',
|
title: 'Reporting',
|
||||||
icon: Report,
|
|
||||||
component: Reporting,
|
component: Reporting,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -367,7 +329,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/admin-invoices',
|
path: '/admin-invoices',
|
||||||
title: 'Invoices',
|
title: 'Invoices',
|
||||||
icon: Money,
|
|
||||||
component: AdminInvoice,
|
component: AdminInvoice,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
@ -376,7 +337,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
title: 'Admin',
|
title: 'Admin',
|
||||||
icon: Album,
|
|
||||||
component: Admin,
|
component: Admin,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
@ -385,7 +345,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/logout',
|
path: '/logout',
|
||||||
title: 'Sign out',
|
title: 'Sign out',
|
||||||
icon: ExitToApp,
|
|
||||||
component: LogoutFeatures,
|
component: LogoutFeatures,
|
||||||
type: 'unprotected',
|
type: 'unprotected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
@ -393,7 +352,6 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
title: 'Log in',
|
title: 'Log in',
|
||||||
icon: Person,
|
|
||||||
component: Login,
|
component: Login,
|
||||||
type: 'unprotected',
|
type: 'unprotected',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@ -430,3 +388,28 @@ export const getRoute = path => routes.find(route => route.path === path);
|
|||||||
export const baseRoutes = routes
|
export const baseRoutes = routes
|
||||||
.filter(route => !route.hidden)
|
.filter(route => !route.hidden)
|
||||||
.filter(route => !route.parent);
|
.filter(route => !route.parent);
|
||||||
|
|
||||||
|
const computeRoutes = () => {
|
||||||
|
const computedRoutes = {
|
||||||
|
mainNavRoutes:
|
||||||
|
baseRoutes.filter(
|
||||||
|
route =>
|
||||||
|
route.path !== '/admin' &&
|
||||||
|
route.path !== '/logout' &&
|
||||||
|
route.path !== '/history'
|
||||||
|
) || [],
|
||||||
|
adminRoutes:
|
||||||
|
routes.filter(
|
||||||
|
route =>
|
||||||
|
(route.path.startsWith('/admin') &&
|
||||||
|
route.path !== '/admin-invoices' &&
|
||||||
|
route.path !== '/admin') ||
|
||||||
|
route.path === '/history'
|
||||||
|
) || [],
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
return computedRoutes;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRoutes = computeRoutes();
|
||||||
|
11
frontend/src/component/project/Project/Project.styles.ts
Normal file
11
frontend/src/component/project/Project/Project.styles.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
containerStyles: {
|
||||||
|
marginTop: '1.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
@ -6,20 +6,46 @@ import ApiError from '../../common/ApiError/ApiError';
|
|||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import ProjectFeatureToggles from './ProjectFeatureToggles/ProjectFeatureToggles';
|
import ProjectFeatureToggles from './ProjectFeatureToggles/ProjectFeatureToggles';
|
||||||
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
||||||
|
import { useStyles } from './Project.styles';
|
||||||
|
import { IconButton } from '@material-ui/core';
|
||||||
|
import { Edit } from '@material-ui/icons';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const Project = () => {
|
const Project = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const params = useQueryParams();
|
||||||
const { project, error, loading, refetch } = useProject(id);
|
const { project, error, loading, refetch } = useProject(id);
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
|
const { toast, setToastData } = useToast();
|
||||||
const { members, features, health } = project;
|
const { members, features, health } = project;
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
const containerStyles = { marginTop: '1.5rem', display: 'flex' };
|
useEffect(() => {
|
||||||
|
const created = params.get('created');
|
||||||
|
const edited = params.get('edited');
|
||||||
|
|
||||||
|
if (created || edited) {
|
||||||
|
const text = created ? 'Project created' : 'Project updated';
|
||||||
|
setToastData({
|
||||||
|
show: true,
|
||||||
|
type: 'success',
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<h1 data-loading className={commonStyles.title}>
|
<h1 data-loading className={commonStyles.title}>
|
||||||
{project?.name}
|
{project?.name}{' '}
|
||||||
|
<IconButton component={Link} to={`/projects/edit/${id}`}>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
</h1>
|
</h1>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={error}
|
condition={error}
|
||||||
@ -32,7 +58,7 @@ const Project = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div style={containerStyles}>
|
<div className={styles.containerStyles}>
|
||||||
<ProjectInfo
|
<ProjectInfo
|
||||||
id={id}
|
id={id}
|
||||||
memberCount={members}
|
memberCount={members}
|
||||||
@ -41,6 +67,7 @@ const Project = () => {
|
|||||||
/>
|
/>
|
||||||
<ProjectFeatureToggles features={features} loading={loading} />
|
<ProjectFeatureToggles features={features} loading={loading} />
|
||||||
</div>
|
</div>
|
||||||
|
{toast}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,10 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
marginLeft: '2rem',
|
marginLeft: '2rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
marginLeft: '0',
|
||||||
|
paddingBottom: '4rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
@ -22,4 +26,10 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
height: '30px',
|
height: '30px',
|
||||||
width: '30px',
|
width: '30px',
|
||||||
},
|
},
|
||||||
|
noTogglesFound: {
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { Button, IconButton } from '@material-ui/core';
|
import { IconButton } from '@material-ui/core';
|
||||||
|
import { Add } from '@material-ui/icons';
|
||||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { IFeatureToggleListItem } from '../../../../interfaces/featureToggle';
|
import { IFeatureToggleListItem } from '../../../../interfaces/featureToggle';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
import { PROJECTFILTERING } from '../../../common/flags';
|
import { PROJECTFILTERING } from '../../../common/flags';
|
||||||
import HeaderTitle from '../../../common/HeaderTitle';
|
import HeaderTitle from '../../../common/HeaderTitle';
|
||||||
import PageContent from '../../../common/PageContent';
|
import PageContent from '../../../common/PageContent';
|
||||||
|
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
|
||||||
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ const ProjectFeatureToggles = ({
|
|||||||
}: IProjectFeatureToggles) => {
|
}: IProjectFeatureToggles) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
@ -44,24 +47,46 @@ const ProjectFeatureToggles = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<ResponsiveButton
|
||||||
variant="contained"
|
onClick={() =>
|
||||||
color="primary"
|
history.push(
|
||||||
component={Link}
|
`/features/create?project=${id}`
|
||||||
to="/features/create"
|
)
|
||||||
data-loading
|
}
|
||||||
|
maxWidth="700px"
|
||||||
|
tooltip="New feature toggle"
|
||||||
|
Icon={Add}
|
||||||
>
|
>
|
||||||
New feature toggle
|
New feature toggle
|
||||||
</Button>
|
</ResponsiveButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FeatureToggleListNew
|
<ConditionallyRender
|
||||||
features={features}
|
condition={features?.length > 0}
|
||||||
loading={loading}
|
show={
|
||||||
projectId={id}
|
<FeatureToggleListNew
|
||||||
|
features={features}
|
||||||
|
loading={loading}
|
||||||
|
projectId={id}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
@ -3,11 +3,23 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
projectInfo: {
|
projectInfo: {
|
||||||
width: '275px',
|
width: '275px',
|
||||||
padding: '1rem',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
projectIcon: {
|
||||||
|
margin: '2rem 0',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
margin: '0 0 0.25rem 0',
|
||||||
|
width: '53px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
marginBottom: '1.25rem',
|
marginBottom: '1.25rem',
|
||||||
@ -15,10 +27,35 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
emphazisedText: {
|
emphazisedText: {
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
marginBottom: '2rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
infoSection: {
|
infoSection: {
|
||||||
margin: '1.8rem 0',
|
margin: '0',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '10px',
|
||||||
|
width: '100%',
|
||||||
|
padding: '1.5rem 1rem 1.5rem 1rem',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
margin: '0 0.25rem',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0.8rem',
|
||||||
|
['&:first-child']: {
|
||||||
|
marginLeft: '0',
|
||||||
|
},
|
||||||
|
['&:last-child']: {
|
||||||
|
marginRight: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
arrowIcon: {
|
arrowIcon: {
|
||||||
color: '#635dc5',
|
color: '#635dc5',
|
||||||
@ -27,5 +64,14 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
infoLink: {
|
infoLink: {
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: '#635dc5',
|
color: '#635dc5',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '5px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
linkText: {
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Paper } from '@material-ui/core';
|
|
||||||
import { useStyles } from './ProjectInfo.styles';
|
import { useStyles } from './ProjectInfo.styles';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
|
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
|
||||||
@ -33,15 +32,19 @@ const ProjectInfo = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside>
|
<aside>
|
||||||
<Paper className={styles.projectInfo}>
|
<div className={styles.projectInfo}>
|
||||||
<div className={styles.infoSection} data-loading>
|
<div className={styles.infoSection}>
|
||||||
<ProjectIcon />
|
<div data-loading>
|
||||||
</div>
|
<ProjectIcon className={styles.projectIcon} />
|
||||||
|
</div>
|
||||||
<div className={styles.infoSection} data-loading>
|
<p className={styles.subtitle} data-loading>
|
||||||
<p className={styles.subtitle}>Overall health rating</p>
|
Overall health rating
|
||||||
<p className={styles.emphazisedText}>{health}%</p>
|
</p>
|
||||||
|
<p className={styles.emphazisedText} data-loading>
|
||||||
|
{health}%
|
||||||
|
</p>
|
||||||
<Link
|
<Link
|
||||||
|
data-loading
|
||||||
className={classnames(
|
className={classnames(
|
||||||
commonStyles.flexRow,
|
commonStyles.flexRow,
|
||||||
commonStyles.justifyCenter,
|
commonStyles.justifyCenter,
|
||||||
@ -49,15 +52,37 @@ const ProjectInfo = ({
|
|||||||
)}
|
)}
|
||||||
to="/reporting"
|
to="/reporting"
|
||||||
>
|
>
|
||||||
view more{' '}
|
<span className={styles.linkText} data-loading>
|
||||||
<ArrowForwardIcon className={styles.arrowIcon} />
|
view more{' '}
|
||||||
|
</span>
|
||||||
|
<ArrowForwardIcon
|
||||||
|
data-loading
|
||||||
|
className={styles.arrowIcon}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.infoSection} data-loading>
|
<div className={styles.infoSection}>
|
||||||
<p className={styles.subtitle}>Project members</p>
|
<p className={styles.subtitle} data-loading>
|
||||||
<p className={styles.emphazisedText}>{memberCount}</p>
|
Feature toggles
|
||||||
|
</p>
|
||||||
|
<p className={styles.emphazisedText} data-loading>
|
||||||
|
{featureCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.infoSection}
|
||||||
|
style={{ marginBottom: '0' }}
|
||||||
|
>
|
||||||
|
<p className={styles.subtitle} data-loading>
|
||||||
|
Project members
|
||||||
|
</p>
|
||||||
|
<p data-loading className={styles.emphazisedText}>
|
||||||
|
{memberCount}
|
||||||
|
</p>
|
||||||
<Link
|
<Link
|
||||||
|
data-loading
|
||||||
className={classnames(
|
className={classnames(
|
||||||
commonStyles.flexRow,
|
commonStyles.flexRow,
|
||||||
commonStyles.justifyCenter,
|
commonStyles.justifyCenter,
|
||||||
@ -65,16 +90,16 @@ const ProjectInfo = ({
|
|||||||
)}
|
)}
|
||||||
to={link}
|
to={link}
|
||||||
>
|
>
|
||||||
view more{' '}
|
<span className={styles.linkText} data-loading>
|
||||||
<ArrowForwardIcon className={styles.arrowIcon} />
|
view more{' '}
|
||||||
|
</span>
|
||||||
|
<ArrowForwardIcon
|
||||||
|
data-loading
|
||||||
|
className={styles.arrowIcon}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className={styles.infoSection} data-loading>
|
|
||||||
<p className={styles.subtitle}>Feature toggles</p>
|
|
||||||
<p className={styles.emphazisedText}>{featureCount}</p>
|
|
||||||
</div>
|
|
||||||
</Paper>
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,9 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
margin: '0.5rem',
|
margin: '0.5rem',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
border: '1px solid #efefef',
|
border: '1px solid #efefef',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -38,4 +41,11 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
color: '#4A4599',
|
color: '#4A4599',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
actionsBtn: {
|
||||||
|
transform: 'translateX(15px)',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: theme.palette.grey[700],
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
import { Card, IconButton } from '@material-ui/core';
|
import { Card, IconButton, Menu, MenuItem } from '@material-ui/core';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { useStyles } from './ProjectCard.styles';
|
import { useStyles } from './ProjectCard.styles';
|
||||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||||
|
|
||||||
import { ReactComponent as ProjectIcon } from '../../../assets/icons/projectIcon.svg';
|
import { ReactComponent as ProjectIcon } from '../../../assets/icons/projectIcon.svg';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { PROJECTCARDACTIONS } from '../../common/flags';
|
import { useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
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';
|
||||||
interface IProjectCardProps {
|
interface IProjectCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
featureCount: number;
|
featureCount: number;
|
||||||
health: number;
|
health: number;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
|
id: string;
|
||||||
onHover: () => void;
|
onHover: () => void;
|
||||||
|
setToastData: Dispatch<
|
||||||
|
SetStateAction<{
|
||||||
|
show: boolean;
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectCard = ({
|
const ProjectCard = ({
|
||||||
@ -20,20 +33,66 @@ const ProjectCard = ({
|
|||||||
health,
|
health,
|
||||||
memberCount,
|
memberCount,
|
||||||
onHover,
|
onHover,
|
||||||
|
id,
|
||||||
|
setToastData,
|
||||||
}: IProjectCardProps) => {
|
}: IProjectCardProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const { refetch: refetchProjectOverview } = useProjects();
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||||
|
const { deleteProject } = useProjectApi();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.projectCard} onMouseEnter={onHover}>
|
<Card className={styles.projectCard} onMouseEnter={onHover}>
|
||||||
<div className={styles.header} data-loading>
|
<div className={styles.header} data-loading>
|
||||||
<h2 className={styles.title}>{name}</h2>
|
<h2 className={styles.title}>{name}</h2>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={PROJECTCARDACTIONS}
|
condition={true}
|
||||||
show={
|
show={
|
||||||
<IconButton data-loading>
|
<IconButton
|
||||||
|
className={styles.actionsBtn}
|
||||||
|
data-loading
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
<MoreVertIcon />
|
<MoreVertIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Menu
|
||||||
|
id="project-card-menu"
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
style={{ top: '40px', left: '-60px' }}
|
||||||
|
onClose={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAnchorEl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
history.push(`/projects/edit/${id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className={styles.icon} />
|
||||||
|
Edit project
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowDelDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete className={styles.icon} />
|
||||||
|
Delete project
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
<div data-loading>
|
<div data-loading>
|
||||||
<ProjectIcon className={styles.projectIcon} />
|
<ProjectIcon className={styles.projectIcon} />
|
||||||
@ -59,6 +118,38 @@ const ProjectCard = ({
|
|||||||
<p data-loading>members</p>
|
<p data-loading>members</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialogue
|
||||||
|
open={showDelDialog}
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteProject(id)
|
||||||
|
.then(() => {
|
||||||
|
setToastData({
|
||||||
|
show: true,
|
||||||
|
type: 'success',
|
||||||
|
text: 'Successfully deleted project',
|
||||||
|
});
|
||||||
|
refetchProjectOverview();
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
setToastData({
|
||||||
|
show: true,
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setShowDelDialog(false);
|
||||||
|
setAnchorEl(null);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClose={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAnchorEl(null);
|
||||||
|
setShowDelDialog(false);
|
||||||
|
}}
|
||||||
|
title="Really delete project"
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useContext, useEffect, useState } from 'react';
|
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import {
|
|
||||||
CREATE_PROJECT,
|
|
||||||
DELETE_PROJECT,
|
|
||||||
UPDATE_PROJECT,
|
|
||||||
} from '../../AccessProvider/permissions';
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItemText,
|
|
||||||
Tooltip,
|
|
||||||
} from '@material-ui/core';
|
|
||||||
import {
|
|
||||||
Add,
|
|
||||||
SupervisedUserCircle,
|
|
||||||
Delete,
|
|
||||||
FolderOpen,
|
|
||||||
} from '@material-ui/icons';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import ConfirmDialogue from '../../common/Dialogue';
|
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
|
||||||
import { useStyles } from './styles';
|
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
|
||||||
import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton';
|
|
||||||
|
|
||||||
const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
|
||||||
const [project, setProject] = useState(undefined);
|
|
||||||
const styles = useStyles();
|
|
||||||
useEffect(() => {
|
|
||||||
fetchProjects();
|
|
||||||
}, [fetchProjects]);
|
|
||||||
|
|
||||||
const addProjectButton = () => (
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(CREATE_PROJECT)}
|
|
||||||
show={
|
|
||||||
<ResponsiveButton
|
|
||||||
Icon={Add}
|
|
||||||
onClick={() => history.push('/projects/create')}
|
|
||||||
maxWidth="700px"
|
|
||||||
tooltip="Add new project"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectLink = ({ id, name }) => (
|
|
||||||
<Link to={`/projects/view/${id}`}>
|
|
||||||
<strong>{name}</strong>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
const mgmAccessButton = project => (
|
|
||||||
<Tooltip title="Manage access">
|
|
||||||
<Link
|
|
||||||
to={`/projects/${project.id}/access`}
|
|
||||||
style={{ color: 'black' }}
|
|
||||||
>
|
|
||||||
<IconButton aria-label="manage_access">
|
|
||||||
<SupervisedUserCircle />
|
|
||||||
</IconButton>
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteProjectButton = project => (
|
|
||||||
<Tooltip title="Remove project">
|
|
||||||
<IconButton
|
|
||||||
aria-label="delete"
|
|
||||||
onClick={() => {
|
|
||||||
setProject(project);
|
|
||||||
setShowDelDialogue(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderProjectList = () =>
|
|
||||||
projects.map(project => (
|
|
||||||
<ListItem key={project.id} classes={{ root: styles.listItem }}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<FolderOpen />
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={projectLink(project)}
|
|
||||||
secondary={project.description}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(UPDATE_PROJECT, project.id)}
|
|
||||||
show={mgmAccessButton(project)}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(DELETE_PROJECT, project.id)}
|
|
||||||
show={deleteProjectButton(project)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
headerContent={
|
|
||||||
<HeaderTitle title="Projects" actions={addProjectButton()} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<List>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={projects.length > 0}
|
|
||||||
show={renderProjectList()}
|
|
||||||
elseShow={<ListItem>No projects defined</ListItem>}
|
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
<ConfirmDialogue
|
|
||||||
open={showDelDialogue}
|
|
||||||
onClick={() => {
|
|
||||||
removeProject(project);
|
|
||||||
setProject(undefined);
|
|
||||||
setShowDelDialogue(false);
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setProject(undefined);
|
|
||||||
setShowDelDialogue(false);
|
|
||||||
}}
|
|
||||||
title="Really delete project"
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ProjectList.propTypes = {
|
|
||||||
projects: PropTypes.array.isRequired,
|
|
||||||
fetchProjects: PropTypes.func.isRequired,
|
|
||||||
removeProject: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectList;
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apiError: {
|
||||||
|
maxWidth: '400px',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
cardLink: {
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: '0',
|
||||||
|
background: 'transparent',
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
pointer: 'cursor',
|
||||||
|
},
|
||||||
|
}));
|
@ -5,7 +5,7 @@ import { getProjectFetcher } from '../../../hooks/api/getters/useProject/getProj
|
|||||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import ProjectCard from '../ProjectCard/ProjectCard';
|
import ProjectCard from '../ProjectCard/ProjectCard';
|
||||||
import { useStyles } from './ProjectListNew.styles';
|
import { useStyles } from './ProjectList.styles';
|
||||||
import { IProjectCard } from '../../../interfaces/project';
|
import { IProjectCard } from '../../../interfaces/project';
|
||||||
|
|
||||||
import loadingData from './loadingData';
|
import loadingData from './loadingData';
|
||||||
@ -18,6 +18,7 @@ import { CREATE_PROJECT } from '../../AccessProvider/permissions';
|
|||||||
|
|
||||||
import { Add } from '@material-ui/icons';
|
import { Add } from '@material-ui/icons';
|
||||||
import ApiError from '../../common/ApiError/ApiError';
|
import ApiError from '../../common/ApiError/ApiError';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
|
||||||
type projectMap = {
|
type projectMap = {
|
||||||
[index: string]: boolean;
|
[index: string]: boolean;
|
||||||
@ -26,6 +27,7 @@ type projectMap = {
|
|||||||
const ProjectListNew = () => {
|
const ProjectListNew = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { toast, setToastData } = useToast();
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { projects, loading, error, refetch } = useProjects();
|
const { projects, loading, error, refetch } = useProjects();
|
||||||
@ -74,7 +76,9 @@ const ProjectListNew = () => {
|
|||||||
name={project?.name}
|
name={project?.name}
|
||||||
memberCount={project?.memberCount}
|
memberCount={project?.memberCount}
|
||||||
health={project?.health}
|
health={project?.health}
|
||||||
|
id={project?.id}
|
||||||
featureCount={project?.featureCount}
|
featureCount={project?.featureCount}
|
||||||
|
setToastData={setToastData}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@ -88,10 +92,12 @@ const ProjectListNew = () => {
|
|||||||
data-loading
|
data-loading
|
||||||
onHover={() => {}}
|
onHover={() => {}}
|
||||||
key={project.id}
|
key={project.id}
|
||||||
projectName={project.name}
|
name={project.name}
|
||||||
members={2}
|
id={project.id}
|
||||||
|
memberCount={2}
|
||||||
health={95}
|
health={95}
|
||||||
toggles={4}
|
featureCount={4}
|
||||||
|
setToastData={setToastData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -114,7 +120,9 @@ const ProjectListNew = () => {
|
|||||||
}
|
}
|
||||||
maxWidth="700px"
|
maxWidth="700px"
|
||||||
tooltip="Add new project"
|
tooltip="Add new project"
|
||||||
/>
|
>
|
||||||
|
Add new project
|
||||||
|
</ResponsiveButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -129,6 +137,7 @@ const ProjectListNew = () => {
|
|||||||
elseShow={renderProjects()}
|
elseShow={renderProjects()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{toast}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -1,22 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { fetchProjects, removeProject } from '../../../store/project/actions';
|
|
||||||
import ProjectList from './ProjectList';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const projects = state.projects.toJS();
|
|
||||||
|
|
||||||
return {
|
|
||||||
projects,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
removeProject: project => {
|
|
||||||
removeProject(project)(dispatch);
|
|
||||||
},
|
|
||||||
fetchProjects: () => fetchProjects()(dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ProjectListContainer = connect(mapStateToProps, mapDispatchToProps)(ProjectList);
|
|
||||||
|
|
||||||
export default ProjectListContainer;
|
|
40
frontend/src/component/project/ProjectList/loadingData.ts
Normal file
40
frontend/src/component/project/ProjectList/loadingData.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const loadingData = [
|
||||||
|
{
|
||||||
|
id: 'loading1',
|
||||||
|
name: 'loading1',
|
||||||
|
memberCount: 1,
|
||||||
|
health: 95,
|
||||||
|
featureCount: 4,
|
||||||
|
createdAt: '',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loading2',
|
||||||
|
name: 'loading2',
|
||||||
|
memberCount: 1,
|
||||||
|
health: 95,
|
||||||
|
featureCount: 4,
|
||||||
|
createdAt: '',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loading3',
|
||||||
|
name: 'loading3',
|
||||||
|
memberCount: 1,
|
||||||
|
health: 95,
|
||||||
|
featureCount: 4,
|
||||||
|
createdAt: '',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loading4',
|
||||||
|
name: 'loading4',
|
||||||
|
memberCount: 1,
|
||||||
|
health: 95,
|
||||||
|
featureCount: 4,
|
||||||
|
createdAt: '',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default loadingData;
|
@ -1,11 +0,0 @@
|
|||||||
import { makeStyles } from '@material-ui/styles';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
listItem: {
|
|
||||||
padding: 0,
|
|
||||||
['& a']: {
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,13 +0,0 @@
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles(theme => ({
|
|
||||||
container: {
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
},
|
|
||||||
apiError: {
|
|
||||||
maxWidth: '400px',
|
|
||||||
marginBottom: '1rem',
|
|
||||||
},
|
|
||||||
cardLink: { color: 'inherit', textDecoration: 'none' },
|
|
||||||
}));
|
|
@ -1,32 +0,0 @@
|
|||||||
const loadingData = [
|
|
||||||
{
|
|
||||||
id: 'loading1',
|
|
||||||
name: 'loading1',
|
|
||||||
members: 1,
|
|
||||||
health: 95,
|
|
||||||
toggles: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'loading2',
|
|
||||||
name: 'loading2',
|
|
||||||
members: 1,
|
|
||||||
health: 95,
|
|
||||||
toggles: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'loading3',
|
|
||||||
name: 'loading3',
|
|
||||||
members: 1,
|
|
||||||
health: 95,
|
|
||||||
toggles: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'loading4',
|
|
||||||
name: 'loading4',
|
|
||||||
members: 1,
|
|
||||||
health: 95,
|
|
||||||
toggles: 4,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default loadingData;
|
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TextField, Typography } from '@material-ui/core';
|
import { TextField, Typography, Button } from '@material-ui/core';
|
||||||
|
|
||||||
import styles from './Project.module.scss';
|
import styles from './Project.module.scss';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@ -10,7 +10,7 @@ import PageContent from '../common/PageContent/PageContent';
|
|||||||
import AccessContext from '../../contexts/AccessContext';
|
import AccessContext from '../../contexts/AccessContext';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender';
|
||||||
import { CREATE_PROJECT } from '../AccessProvider/permissions';
|
import { CREATE_PROJECT } from '../AccessProvider/permissions';
|
||||||
import { Link } from 'react-router-dom';
|
import HeaderTitle from '../common/HeaderTitle';
|
||||||
|
|
||||||
class ProjectFormComponent extends Component {
|
class ProjectFormComponent extends Component {
|
||||||
static contextType = AccessContext;
|
static contextType = AccessContext;
|
||||||
@ -75,15 +75,10 @@ class ProjectFormComponent extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onCancel = evt => {
|
onCancel = evt => {
|
||||||
const { editMode } = this.props;
|
|
||||||
const { project } = this.state;
|
const { project } = this.state;
|
||||||
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
if (editMode) {
|
|
||||||
this.props.history.push(`/projects/view/${project.id}`);
|
this.props.history.push(`/projects/${project.id}`);
|
||||||
} else {
|
|
||||||
this.props.history.push('/projects');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = async evt => {
|
onSubmit = async evt => {
|
||||||
@ -93,8 +88,10 @@ class ProjectFormComponent extends Component {
|
|||||||
const valid = await this.validate(project.id);
|
const valid = await this.validate(project.id);
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
const { editMode } = this.props;
|
||||||
|
const query = editMode ? 'edited=true' : 'created=true';
|
||||||
await this.props.submit(project);
|
await this.props.submit(project);
|
||||||
this.props.history.push(`/projects/view/${project.id}`);
|
this.props.history.push(`/projects/${project.id}?${query}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,20 +104,28 @@ class ProjectFormComponent extends Component {
|
|||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
headerContent={
|
headerContent={
|
||||||
<div>
|
<HeaderTitle
|
||||||
<span>{submitText} Project</span>
|
title={`${submitText} Project`}
|
||||||
<ConditionallyRender
|
actions={
|
||||||
condition={hasAccess(CREATE_PROJECT) && editMode}
|
<ConditionallyRender
|
||||||
show={
|
condition={
|
||||||
<Link
|
hasAccess(CREATE_PROJECT) && editMode
|
||||||
to={`/projects/${project.id}/access`}
|
}
|
||||||
style={{ float: 'right' }}
|
show={
|
||||||
>
|
<Button
|
||||||
Manage access
|
color="primary"
|
||||||
</Link>
|
onClick={() =>
|
||||||
}
|
this.props.history.push(
|
||||||
/>
|
`/projects/${project.id}/access`
|
||||||
</div>
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Manage access
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
|
@ -3,6 +3,12 @@
|
|||||||
exports[`renders correctly with one strategy 1`] = `
|
exports[`renders correctly with one strategy 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-3"
|
className="makeStyles-headerContainer-3"
|
||||||
@ -132,6 +138,12 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
exports[`renders correctly with one strategy without permissions 1`] = `
|
exports[`renders correctly with one strategy without permissions 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-3"
|
className="makeStyles-headerContainer-3"
|
||||||
|
@ -3,6 +3,12 @@
|
|||||||
exports[`renders correctly with one strategy 1`] = `
|
exports[`renders correctly with one strategy 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #ecebeb;
|
background-color: #ecebeb;
|
||||||
|
background-image: url('../assets/img/texture.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-x: right;
|
||||||
|
background-position-y: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@ -3,6 +3,12 @@
|
|||||||
exports[`it supports editMode 1`] = `
|
exports[`it supports editMode 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
@ -100,6 +106,12 @@ exports[`it supports editMode 1`] = `
|
|||||||
exports[`renders correctly for creating 1`] = `
|
exports[`renders correctly for creating 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
@ -197,6 +209,12 @@ exports[`renders correctly for creating 1`] = `
|
|||||||
exports[`renders correctly for creating without permissions 1`] = `
|
exports[`renders correctly for creating without permissions 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
|
@ -3,6 +3,12 @@
|
|||||||
exports[`renders a list with elements correctly 1`] = `
|
exports[`renders a list with elements correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
@ -146,6 +152,12 @@ exports[`renders a list with elements correctly 1`] = `
|
|||||||
exports[`renders an empty list correctly 1`] = `
|
exports[`renders an empty list correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"borderRadius": "10px",
|
||||||
|
"boxShadow": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerContainer-1"
|
className="makeStyles-headerContainer-1"
|
||||||
|
@ -62,7 +62,6 @@ const UserProfile = ({
|
|||||||
styles.button
|
styles.button
|
||||||
)}
|
)}
|
||||||
onClick={() => setShowProfile(prev => !prev)}
|
onClick={() => setShowProfile(prev => !prev)}
|
||||||
tabIndex="1"
|
|
||||||
role="button"
|
role="button"
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
|
@ -8,7 +8,7 @@ export const useStyles = makeStyles({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
color: '#fff',
|
color: 'inherit',
|
||||||
padding: '0.2rem 1rem',
|
padding: '0.2rem 1rem',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -9,12 +9,14 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useStyles } from './UserProfileContent.styles';
|
import { useStyles } from './UserProfileContent.styles';
|
||||||
import { useCommonStyles } from '../../../../common.styles';
|
import { useCommonStyles } from '../../../../common.styles';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import EditProfile from '../EditProfile/EditProfile';
|
import EditProfile from '../EditProfile/EditProfile';
|
||||||
import legacyStyles from '../../user.module.scss';
|
import legacyStyles from '../../user.module.scss';
|
||||||
|
import usePermissions from '../../../../hooks/usePermissions';
|
||||||
|
|
||||||
const UserProfileContent = ({
|
const UserProfileContent = ({
|
||||||
showProfile,
|
showProfile,
|
||||||
@ -30,6 +32,7 @@ const UserProfileContent = ({
|
|||||||
const [updatedPassword, setUpdatedPassword] = useState(false);
|
const [updatedPassword, setUpdatedPassword] = useState(false);
|
||||||
const [edititingProfile, setEditingProfile] = useState(false);
|
const [edititingProfile, setEditingProfile] = useState(false);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const { isAdmin } = usePermissions();
|
||||||
|
|
||||||
const setLocale = value => {
|
const setLocale = value => {
|
||||||
updateSettingLocation('locale', value);
|
updateSettingLocation('locale', value);
|
||||||
@ -128,6 +131,27 @@ const UserProfileContent = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
<div className={commonStyles.divider} />
|
<div className={commonStyles.divider} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isAdmin()}
|
||||||
|
show={
|
||||||
|
<Link
|
||||||
|
to="/admin-invoices"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
Account and billing
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className={styles.link}
|
||||||
|
href="https://www.getunleash.io/privacy-policy"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Privacy policy
|
||||||
|
</a>
|
||||||
|
<div className={commonStyles.divider} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -26,4 +26,10 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
editingEmail: {
|
editingEmail: {
|
||||||
transform: 'translateX(10px) translateY(-60px)',
|
transform: 'translateX(10px) translateY(-60px)',
|
||||||
},
|
},
|
||||||
|
link: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
textDecoration: 'none',
|
||||||
|
textAlign: 'left',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
container: {
|
container: {
|
||||||
margin: 'auto auto 0 auto',
|
margin: 'auto auto 0 auto',
|
||||||
width: '200px',
|
width: '230px',
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
marginTop: '1rem',
|
marginTop: '1rem',
|
||||||
},
|
},
|
||||||
|
@ -153,6 +153,20 @@ const useAPI = ({
|
|||||||
throw new ForbiddenError(res.status);
|
throw new ForbiddenError(res.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (res.status > 399) {
|
||||||
|
const response = await res.json();
|
||||||
|
if (response?.details?.length > 0) {
|
||||||
|
const error = response.details[0];
|
||||||
|
if (propagateErrors) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (propagateErrors) {
|
||||||
|
throw new Error('Action could not be performed');
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
const useProjectApi = () => {
|
||||||
|
const { makeRequest, createRequest, errors } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteProject = async (projectId: string) => {
|
||||||
|
const path = `api/admin/projects/${projectId}`;
|
||||||
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { deleteProject, errors };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProjectApi;
|
@ -5,7 +5,7 @@ export const defaultValue = {
|
|||||||
version: '3.x',
|
version: '3.x',
|
||||||
environment: '',
|
environment: '',
|
||||||
slogan: 'The enterprise ready feature toggle service.',
|
slogan: 'The enterprise ready feature toggle service.',
|
||||||
flags: {},
|
flags: { P: false, C: false },
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
value: 'Documentation',
|
value: 'Documentation',
|
||||||
|
@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr';
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { formatApiPath } from '../../../../utils/format-path';
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
import { defaultValue } from './defaultValue';
|
import { defaultValue } from './defaultValue';
|
||||||
|
import { IUiConfig } from '../../../../interfaces/uiConfig';
|
||||||
|
|
||||||
const REQUEST_KEY = 'api/admin/ui-config';
|
const REQUEST_KEY = 'api/admin/ui-config';
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ const useUiConfig = () => {
|
|||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error } = useSWR(REQUEST_KEY, fetcher);
|
const { data, error } = useSWR<IUiConfig>(REQUEST_KEY, fetcher);
|
||||||
const [loading, setLoading] = useState(!error && !data);
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
const refetch = () => {
|
const refetch = () => {
|
||||||
@ -27,7 +28,7 @@ const useUiConfig = () => {
|
|||||||
}, [data, error]);
|
}, [data, error]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uiConfig: data || defaultValue,
|
uiConfig: { ...defaultValue, ...data },
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
refetch,
|
refetch,
|
||||||
|
36
frontend/src/hooks/api/getters/useUser/useUser.ts
Normal file
36
frontend/src/hooks/api/getters/useUser/useUser.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import { IPermission } from '../../../../interfaces/user';
|
||||||
|
|
||||||
|
const useUser = () => {
|
||||||
|
const KEY = `api/admin/user`;
|
||||||
|
const fetcher = () => {
|
||||||
|
const path = formatApiPath(`api/admin/user`);
|
||||||
|
return fetch(path, {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = useSWR(KEY, fetcher);
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const refetch = () => {
|
||||||
|
mutate(KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: data?.user || {},
|
||||||
|
permissions: (data?.permissions || []) as IPermission[],
|
||||||
|
feedback: data?.feedback || [],
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUser;
|
8
frontend/src/hooks/usePermissions.ts
Normal file
8
frontend/src/hooks/usePermissions.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import AccessContext from '../contexts/AccessContext';
|
||||||
|
|
||||||
|
const usePermissions = () => {
|
||||||
|
return useContext(AccessContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePermissions;
|
26
frontend/src/hooks/useToast.tsx
Normal file
26
frontend/src/hooks/useToast.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import Toast from '../component/common/Toast/Toast';
|
||||||
|
|
||||||
|
const useToast = () => {
|
||||||
|
const [toastData, setToastData] = useState({
|
||||||
|
show: false,
|
||||||
|
type: 'success',
|
||||||
|
text: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const hideToast = () => {
|
||||||
|
setToastData(prev => ({ ...prev, show: false }));
|
||||||
|
};
|
||||||
|
const toast = (
|
||||||
|
<Toast
|
||||||
|
show={toastData.show}
|
||||||
|
onClose={hideToast}
|
||||||
|
text={toastData.text}
|
||||||
|
type={toastData.type}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return { toast, setToastData, hideToast };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useToast;
|
35
frontend/src/interfaces/uiConfig.ts
Normal file
35
frontend/src/interfaces/uiConfig.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export interface IUiConfig {
|
||||||
|
authenticationType: string;
|
||||||
|
baseUriPath: string;
|
||||||
|
flags: IFlags;
|
||||||
|
name: string;
|
||||||
|
slogan: string;
|
||||||
|
unleashUrl: string;
|
||||||
|
version: string;
|
||||||
|
versionInfo: IVersionInfo;
|
||||||
|
links: ILinks[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFlags {
|
||||||
|
C: boolean;
|
||||||
|
P: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IVersionInfo {
|
||||||
|
instanceId: string;
|
||||||
|
isLatest: boolean;
|
||||||
|
latest: Object;
|
||||||
|
current: ICurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICurrent {
|
||||||
|
oss: string;
|
||||||
|
enterprise: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILinks {
|
||||||
|
value: string;
|
||||||
|
icon: string;
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
}
|
@ -1,16 +1,22 @@
|
|||||||
import { createMuiTheme } from '@material-ui/core/styles';
|
import { createMuiTheme } from '@material-ui/core/styles';
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme = createMuiTheme({
|
||||||
|
typography: {
|
||||||
|
fontFamily: ['Sen', 'Roboto, sans-serif'],
|
||||||
|
},
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#1A4049',
|
main: '#635DC5',
|
||||||
light: '#B3DAED',
|
light: '#817AFE',
|
||||||
dark: '#0A1A1d',
|
dark: '#635DC5',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#122D33',
|
main: '#635DC5',
|
||||||
light: '#40836f',
|
light: '#817AFE',
|
||||||
dark: '#002c1d',
|
dark: '#635DC5',
|
||||||
|
},
|
||||||
|
grey: {
|
||||||
|
main: '#6C6C6C',
|
||||||
},
|
},
|
||||||
neutral: {
|
neutral: {
|
||||||
main: '#18243e',
|
main: '#18243e',
|
||||||
|
Loading…
Reference in New Issue
Block a user