mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +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');
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
@ -34,10 +52,12 @@ html {
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
/* font-family: 'Sen'; */
|
||||
}
|
||||
|
||||
.MuiButton-root {
|
||||
border-radius: 3px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.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',
|
||||
transform: 'translateY(400px)',
|
||||
},
|
||||
fadeInBottomStartWithoutFixed: {
|
||||
opacity: '0',
|
||||
right: '40px',
|
||||
bottom: '40px',
|
||||
transform: 'translateY(400px)',
|
||||
},
|
||||
fadeInBottomEnter: {
|
||||
transform: 'translateY(0)',
|
||||
opacity: '1',
|
||||
|
@ -14,6 +14,25 @@ interface IPermission {
|
||||
}
|
||||
|
||||
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 permissions = store.getState().user.get('permissions') || [];
|
||||
|
||||
@ -36,7 +55,7 @@ const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const context = { hasAccess };
|
||||
const context = { hasAccess, isAdmin };
|
||||
|
||||
return (
|
||||
<AccessContext.Provider value={context}>
|
||||
|
@ -1,4 +1,5 @@
|
||||
export const ADMIN = 'ADMIN';
|
||||
export const EDITOR = 'EDITOR';
|
||||
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
||||
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
|
||||
export const DELETE_FEATURE = 'DELETE_FEATURE';
|
||||
|
@ -32,7 +32,6 @@ const AddonFormComponent = ({
|
||||
}, [fetch, provider]); // empty array => fetch only first time
|
||||
|
||||
useEffect(() => {
|
||||
console.log(addon);
|
||||
setConfig({ ...addon });
|
||||
/* eslint-disable-next-line */
|
||||
}, [addon.description, addon.provider]);
|
||||
|
@ -24,6 +24,12 @@ exports[`renders correctly if no application 1`] = `
|
||||
exports[`renders correctly with permissions 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
@ -543,6 +549,12 @@ exports[`renders correctly with permissions 1`] = `
|
||||
exports[`renders correctly without permission 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
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 } : {};
|
||||
|
||||
return (
|
||||
<Paper {...rest} {...paperProps}>
|
||||
<Paper
|
||||
{...rest}
|
||||
{...paperProps}
|
||||
style={{ borderRadius: '10px', boxShadow: 'none' }}
|
||||
>
|
||||
{header}
|
||||
<div className={bodyClasses}>{children}</div>
|
||||
</Paper>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
import classnames from 'classnames';
|
||||
import { useStyles } from './PaginationUI.styles';
|
||||
@ -7,6 +7,7 @@ import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
|
||||
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
|
||||
|
||||
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
|
||||
import { useMediaQuery, useTheme } from '@material-ui/core';
|
||||
|
||||
interface IPaginateUIProps {
|
||||
pages: any[];
|
||||
@ -24,9 +25,17 @@ const PaginateUI = ({
|
||||
nextPage,
|
||||
}: IPaginateUIProps) => {
|
||||
const STARTLIMIT = 6;
|
||||
const theme = useTheme();
|
||||
const styles = useStyles();
|
||||
const [limit, setLimit] = useState(STARTLIMIT);
|
||||
const [start, setStart] = useState(0);
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
useEffect(() => {
|
||||
if (matches) {
|
||||
setLimit(4);
|
||||
}
|
||||
}, [matches]);
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
|
@ -23,7 +23,6 @@ const renderProclamation = (id: string) => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
console.log('RETURNING TRUE');
|
||||
return true;
|
||||
};
|
||||
|
||||
|
@ -8,12 +8,13 @@ interface IResponsiveButtonProps {
|
||||
maxWidth: string;
|
||||
}
|
||||
|
||||
const ResponsiveButton = ({
|
||||
const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
Icon,
|
||||
onClick,
|
||||
maxWidth,
|
||||
tooltip,
|
||||
}: IResponsiveButtonProps) => {
|
||||
children,
|
||||
}) => {
|
||||
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
|
||||
|
||||
return (
|
||||
@ -21,14 +22,19 @@ const ResponsiveButton = ({
|
||||
condition={smallScreen}
|
||||
show={
|
||||
<Tooltip title={tooltip ? tooltip : ''}>
|
||||
<IconButton onClick={onClick}>
|
||||
<IconButton onClick={onClick} data-loading>
|
||||
<Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
elseShow={
|
||||
<Button onClick={onClick} color="primary" variant="contained">
|
||||
Add new project
|
||||
<Button
|
||||
onClick={onClick}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
data-loading
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
@ -5,7 +5,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.searchField.main,
|
||||
borderRadius: theme.borders.radius.main,
|
||||
borderRadius: '25px',
|
||||
padding: '0.25rem 0.5rem',
|
||||
maxWidth: '450px',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
|
@ -24,7 +24,7 @@ const Toast = ({
|
||||
<Portal>
|
||||
<AnimateOnMount
|
||||
mounted={show}
|
||||
start={styles.fadeInBottomStart}
|
||||
start={styles.fadeInBottomStartWithoutFixed}
|
||||
enter={styles.fadeInBottomEnter}
|
||||
leave={styles.fadeInBottomLeave}
|
||||
container={styles.fullWidth}
|
||||
@ -33,6 +33,7 @@ const Toast = ({
|
||||
open={show}
|
||||
onClose={onClose}
|
||||
autoHideDuration={autoHideDuration}
|
||||
style={{ bottom: '40px' }}
|
||||
>
|
||||
<Alert variant="filled" severity={type} onClose={onClose}>
|
||||
{text}
|
||||
|
@ -3,5 +3,4 @@ export const C = 'C';
|
||||
export const RBAC = 'RBAC';
|
||||
export const OIDC = 'OIDC';
|
||||
|
||||
export const PROJECTCARDACTIONS = false;
|
||||
export const PROJECTFILTERING = false;
|
||||
|
@ -16,6 +16,13 @@ const dateOptions = {
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
export const filterByFlags = flags => r => {
|
||||
if (r.flag && !flags[r.flag]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const scrollToTop = () => {
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
@ -3,7 +3,8 @@ import { makeStyles } from '@material-ui/styles';
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
typeChip: {
|
||||
margin: '0 8px',
|
||||
boxShadow: theme.boxShadows.chip.main,
|
||||
backgroundColor: theme.palette.chips.main,
|
||||
background: 'transparent',
|
||||
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
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-8"
|
||||
@ -278,6 +284,12 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
</div>
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-8"
|
||||
|
@ -11,6 +11,11 @@ export const useStyles = makeStyles(theme => ({
|
||||
tableCellHeader: {
|
||||
paddingBottom: '0.5rem',
|
||||
},
|
||||
typeHeader: {
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
tableCellName: {
|
||||
width: '250px',
|
||||
},
|
||||
@ -20,6 +25,9 @@ export const useStyles = makeStyles(theme => ({
|
||||
tableCellType: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
marginRight: '0.3rem',
|
||||
|
@ -89,7 +89,8 @@ const FeatureToggleListNew = ({
|
||||
<TableCell
|
||||
className={classnames(
|
||||
styles.tableCell,
|
||||
styles.tableCellHeader
|
||||
styles.tableCellHeader,
|
||||
styles.typeHeader
|
||||
)}
|
||||
align="left"
|
||||
>
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { Switch, TableCell, TableRow } from '@material-ui/core';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
Switch,
|
||||
TableCell,
|
||||
TableRow,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getFeatureTypeIcons } from '../../../../utils/get-feature-type-icons';
|
||||
import { useStyles } from '../FeatureToggleListNew.styles';
|
||||
import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv';
|
||||
import { IEnvironments } from '../../../../interfaces/featureToggle';
|
||||
import Toast from '../../../common/Toast/Toast';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
|
||||
interface IFeatureToggleListNewItemProps {
|
||||
name: string;
|
||||
@ -20,15 +27,14 @@ const FeatureToggleListNewItem = ({
|
||||
environments,
|
||||
projectId,
|
||||
}: IFeatureToggleListNewItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { toast, setToastData } = useToast();
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const { toggleFeatureByEnvironment } = useToggleFeatureByEnv(
|
||||
projectId,
|
||||
name
|
||||
);
|
||||
const [snackbarData, setSnackbardata] = useState({
|
||||
show: false,
|
||||
type: 'success',
|
||||
text: '',
|
||||
});
|
||||
|
||||
const styles = useStyles();
|
||||
const history = useHistory();
|
||||
const ref = useRef(null);
|
||||
@ -42,14 +48,14 @@ const FeatureToggleListNewItem = ({
|
||||
const handleToggle = (env: IEnvironments) => {
|
||||
toggleFeatureByEnvironment(env.name, env.enabled)
|
||||
.then(() => {
|
||||
setSnackbardata({
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully updated toggle status.',
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
setSnackbardata({
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
@ -57,10 +63,6 @@ const FeatureToggleListNewItem = ({
|
||||
});
|
||||
};
|
||||
|
||||
const hideSnackbar = () => {
|
||||
setSnackbardata(prev => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
const IconComponent = getFeatureTypeIcons(type);
|
||||
|
||||
return (
|
||||
@ -69,12 +71,21 @@ const FeatureToggleListNewItem = ({
|
||||
<TableCell className={styles.tableCell} align="left">
|
||||
<span data-loading>{name}</span>
|
||||
</TableCell>
|
||||
<TableCell className={styles.tableCell} align="left">
|
||||
<div className={styles.tableCellType}>
|
||||
<IconComponent data-loading className={styles.icon} />{' '}
|
||||
<span data-loading>{type}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<ConditionallyRender
|
||||
condition={!smallScreen}
|
||||
show={
|
||||
<TableCell className={styles.tableCell} align="left">
|
||||
<div className={styles.tableCellType}>
|
||||
<IconComponent
|
||||
data-loading
|
||||
className={styles.icon}
|
||||
/>{' '}
|
||||
<span data-loading>{type}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
}
|
||||
/>
|
||||
|
||||
{environments.map((env: IEnvironments) => {
|
||||
return (
|
||||
<TableCell
|
||||
@ -93,12 +104,7 @@ const FeatureToggleListNewItem = ({
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
<Toast
|
||||
show={snackbarData.show}
|
||||
onClose={hideSnackbar}
|
||||
text={snackbarData.text}
|
||||
type={snackbarData.type}
|
||||
/>
|
||||
{toast}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -222,7 +222,11 @@ const FeatureView = ({
|
||||
return (
|
||||
<Paper
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
borderRadius: '10px',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
|
@ -18,6 +18,8 @@ import {
|
||||
} from '../../../../testIds';
|
||||
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import useQueryParams from '../../../../hooks/useQueryParams';
|
||||
|
||||
const CreateFeature = ({
|
||||
input,
|
||||
@ -25,9 +27,19 @@ const CreateFeature = ({
|
||||
setValue,
|
||||
validateName,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
user,
|
||||
}) => {
|
||||
const params = useQueryParams();
|
||||
const project = params.get('project');
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (project) {
|
||||
setValue('project', project);
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.onbeforeunload = () =>
|
||||
'Data will be lost if you leave the page, are you sure?';
|
||||
@ -37,6 +49,8 @@ const CreateFeature = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onCancel = () => history.goBack();
|
||||
|
||||
return (
|
||||
<PageContent headerContent="Create new feature toggle">
|
||||
<form onSubmit={onSubmit}>
|
||||
@ -73,7 +87,7 @@ const CreateFeature = ({
|
||||
</div>
|
||||
<section className={styles.formContainer}>
|
||||
<ProjectSelect
|
||||
value={input.project}
|
||||
value={project || input.project}
|
||||
onChange={v => setValue('project', v.target.value)}
|
||||
filter={projectFilterGenerator(user, CREATE_FEATURE)}
|
||||
/>
|
||||
|
@ -4,9 +4,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
strategyCard: {
|
||||
width: '337px',
|
||||
height: '100%',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
[theme.breakpoints.down('1250')]: {
|
||||
width: '300px',
|
||||
},
|
||||
@ -16,5 +14,8 @@ export const useStyles = makeStyles(theme => ({
|
||||
[theme.breakpoints.down('860')]: {
|
||||
width: '380px',
|
||||
},
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
@ -9,7 +9,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
strategyCardHeader: {
|
||||
display: 'flex',
|
||||
background: `linear-gradient(${theme.palette.cards.gradient.top}, ${theme.palette.cards.gradient.bottom})`,
|
||||
background: theme.palette.primary.dark,
|
||||
color: '#fff',
|
||||
textAlign: 'left',
|
||||
},
|
||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import arrayMove from 'array-move';
|
||||
import { Button } from '@material-ui/core';
|
||||
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
@ -14,6 +13,8 @@ import EditStrategyModal from './EditStrategyModal/EditStrategyModal';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import CreateStrategy from './AddStrategy/AddStrategy';
|
||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||
import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton';
|
||||
import { Add } from '@material-ui/icons';
|
||||
|
||||
const cleanStrategy = strategy => ({
|
||||
name: strategy.name,
|
||||
@ -110,12 +111,8 @@ const StrategiesList = props => {
|
||||
setEditStrategyIndex(undefined);
|
||||
};
|
||||
|
||||
const {
|
||||
strategies,
|
||||
configuredStrategies,
|
||||
featureToggleName,
|
||||
editable,
|
||||
} = props;
|
||||
const { strategies, configuredStrategies, featureToggleName, editable } =
|
||||
props;
|
||||
|
||||
const resolveStrategyDefinition = strategyName => {
|
||||
if (!strategies || strategies.length === 0) {
|
||||
@ -170,14 +167,14 @@ const StrategiesList = props => {
|
||||
title="Activation strategies"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!featureToggleName}
|
||||
color="primary"
|
||||
<ResponsiveButton
|
||||
onClick={() => setShowCreateStrategy(true)}
|
||||
maxWidth="700px"
|
||||
tooltip="Add strategy"
|
||||
Icon={Add}
|
||||
>
|
||||
Add strategy
|
||||
</Button>
|
||||
</ResponsiveButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -497,10 +497,10 @@ exports[`renders correctly with with variants 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-22 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-23 PrivateNotchedOutline-legendNotched-24"
|
||||
className="PrivateNotchedOutline-legendLabelled-24 PrivateNotchedOutline-legendNotched-25"
|
||||
>
|
||||
<span>
|
||||
Stickiness
|
||||
|
@ -5,6 +5,8 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
"overflow": "visible",
|
||||
}
|
||||
}
|
||||
@ -140,10 +142,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-19 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-20 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-21"
|
||||
className="PrivateNotchedOutline-legendLabelled-22"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -175,7 +177,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<span
|
||||
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]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -194,7 +196,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="PrivateSwitchBase-input-26 MuiSwitch-input"
|
||||
className="PrivateSwitchBase-input-27 MuiSwitch-input"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
@ -327,7 +329,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-27 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-28 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -375,7 +377,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-28 PrivateTabIndicator-colorPrimary-29 MuiTabs-indicator"
|
||||
className="PrivateTabIndicator-root-29 PrivateTabIndicator-colorPrimary-30 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
|
@ -6,15 +6,24 @@ import { Grid } from '@material-ui/core';
|
||||
|
||||
import styles from '../../styles.module.scss';
|
||||
import ErrorContainer from '../../error/error-container';
|
||||
import Header from '../../menu/Header';
|
||||
import Header from '../../menu/Header/Header';
|
||||
import Footer from '../../menu/Footer/Footer';
|
||||
import Proclamation from '../../common/Proclamation/Proclamation';
|
||||
import BreadcrumbNav from '../../common/BreadcrumbNav/BreadcrumbNav';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
height: '100%',
|
||||
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 }) => {
|
||||
@ -26,7 +35,8 @@ const MainLayout = ({ children, location, uiConfig }) => {
|
||||
<Grid container className={muiStyles.container}>
|
||||
<div className={classnames(styles.contentWrapper)}>
|
||||
<Grid item className={styles.content} xs={12} sm={12}>
|
||||
<div className={styles.contentContainer}>
|
||||
<div className={muiStyles.contentContainer}>
|
||||
<BreadcrumbNav />
|
||||
<Proclamation toast={uiConfig.toast} />
|
||||
{children}
|
||||
</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 [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/features",
|
||||
"title": "Feature Toggles",
|
||||
@ -19,14 +11,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/strategies",
|
||||
"title": "Strategies",
|
||||
@ -34,14 +18,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/history",
|
||||
"title": "Event History",
|
||||
@ -49,14 +25,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/archive",
|
||||
"title": "Archived Toggles",
|
||||
@ -64,14 +32,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/applications",
|
||||
"title": "Applications",
|
||||
@ -80,14 +40,6 @@ Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "C",
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/context",
|
||||
"title": "Context Fields",
|
||||
@ -96,14 +48,6 @@ Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "P",
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/projects",
|
||||
"title": "Projects",
|
||||
@ -111,14 +55,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/tag-types",
|
||||
"title": "Tag types",
|
||||
@ -127,14 +63,6 @@ Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"hidden": false,
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/addons",
|
||||
"title": "Addons",
|
||||
@ -142,14 +70,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/reporting",
|
||||
"title": "Reporting",
|
||||
@ -158,14 +78,6 @@ Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"hidden": false,
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/admin",
|
||||
"title": "Admin",
|
||||
@ -173,14 +85,6 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"icon": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"layout": "main",
|
||||
"path": "/logout",
|
||||
"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 { NavLink } from 'react-router-dom';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
|
||||
|
||||
import styles from './drawer.module.scss';
|
||||
|
||||
import { baseRoutes as routes } from './routes';
|
||||
|
||||
import { ReactComponent as LogoIcon } from '../../assets/icons/logo_wbg.svg';
|
||||
|
||||
const filterByFlags = flags => r => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
import NavigationLink from './Header/NavigationLink/NavigationLink';
|
||||
import ConditionallyRender from '../common/ConditionallyRender';
|
||||
|
||||
export const DrawerMenu = ({
|
||||
links = [],
|
||||
@ -63,45 +15,84 @@ export const DrawerMenu = ({
|
||||
flags = {},
|
||||
open = false,
|
||||
toggleDrawer,
|
||||
}) => (
|
||||
<Drawer
|
||||
className={styles.drawer}
|
||||
open={open}
|
||||
anchor={'left'}
|
||||
onClose={() => toggleDrawer()}
|
||||
>
|
||||
<div className={styles.drawerContainer}>
|
||||
<div>
|
||||
<span className={[styles.drawerTitle].join(' ')}>
|
||||
<LogoIcon className={styles.drawerTitleLogo} />
|
||||
admin,
|
||||
routes,
|
||||
}) => {
|
||||
const renderLinks = () => {
|
||||
return links.map(link => {
|
||||
let icon = null;
|
||||
if (link.value === 'GitHub') {
|
||||
icon = <GitHubIcon className={styles.navigationIcon} />;
|
||||
} else if (link.value === 'Documentation') {
|
||||
icon = <LibraryBooksIcon className={styles.navigationIcon} />;
|
||||
}
|
||||
|
||||
<span className={styles.drawerTitleText}>{title}</span>
|
||||
</span>
|
||||
return (
|
||||
<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>
|
||||
<Divider />
|
||||
<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>
|
||||
);
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
DrawerMenu.propTypes = {
|
||||
links: PropTypes.array,
|
||||
|
@ -22,10 +22,19 @@
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.drawerList {
|
||||
.drawerList,
|
||||
.iconLinkList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: centre;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.iconLink {
|
||||
display: flex;
|
||||
padding: 0.8rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.navigationLink {
|
||||
@ -44,6 +53,7 @@
|
||||
|
||||
.navigationIcon {
|
||||
margin-right: 16px;
|
||||
fill: #635dc5;
|
||||
}
|
||||
|
||||
.iconGitHub {
|
||||
|
@ -15,7 +15,6 @@ import ContextFields from '../../page/context';
|
||||
import CreateContextField from '../../page/context/create';
|
||||
import EditContextField from '../../page/context/edit';
|
||||
import LogoutFeatures from '../../page/user/logout';
|
||||
import ListProjects from '../../page/project';
|
||||
import CreateProject from '../../page/project/create';
|
||||
import EditProject from '../../page/project/edit';
|
||||
import ViewProject from '../../page/project/view';
|
||||
@ -39,25 +38,9 @@ import { P, C } from '../common/flags';
|
||||
import NewUser from '../user/NewUser';
|
||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||
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 {
|
||||
List,
|
||||
Extension,
|
||||
History,
|
||||
Archive as ArchiveIcon,
|
||||
Apps,
|
||||
Label,
|
||||
DeviceHub,
|
||||
Album,
|
||||
ExitToApp,
|
||||
FolderOpen,
|
||||
Report,
|
||||
Money,
|
||||
Person,
|
||||
} from '@material-ui/icons';
|
||||
|
||||
export const routes = [
|
||||
// Features
|
||||
{
|
||||
@ -87,7 +70,6 @@ export const routes = [
|
||||
{
|
||||
path: '/features',
|
||||
title: 'Feature Toggles',
|
||||
icon: List,
|
||||
component: Features,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -113,7 +95,6 @@ export const routes = [
|
||||
{
|
||||
path: '/strategies',
|
||||
title: 'Strategies',
|
||||
icon: Extension,
|
||||
component: Strategies,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -131,7 +112,6 @@ export const routes = [
|
||||
{
|
||||
path: '/history',
|
||||
title: 'Event History',
|
||||
icon: History,
|
||||
component: HistoryPage,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -149,7 +129,6 @@ export const routes = [
|
||||
{
|
||||
path: '/archive',
|
||||
title: 'Archived Toggles',
|
||||
icon: ArchiveIcon,
|
||||
component: Archive,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -167,7 +146,6 @@ export const routes = [
|
||||
{
|
||||
path: '/applications',
|
||||
title: 'Applications',
|
||||
icon: Apps,
|
||||
component: Applications,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -193,7 +171,6 @@ export const routes = [
|
||||
{
|
||||
path: '/context',
|
||||
title: 'Context Fields',
|
||||
icon: Album,
|
||||
component: ContextFields,
|
||||
type: 'protected',
|
||||
flag: C,
|
||||
@ -245,20 +222,9 @@ export const routes = [
|
||||
{
|
||||
path: '/projects',
|
||||
title: 'Projects',
|
||||
icon: FolderOpen,
|
||||
component: ListProjects,
|
||||
flag: P,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/projects-new',
|
||||
title: 'Projects new',
|
||||
icon: 'folder_open',
|
||||
component: ProjectListNew,
|
||||
flag: P,
|
||||
type: 'protected',
|
||||
hidden: true,
|
||||
layout: 'main',
|
||||
},
|
||||
|
||||
@ -281,7 +247,6 @@ export const routes = [
|
||||
{
|
||||
path: '/tag-types',
|
||||
title: 'Tag types',
|
||||
icon: Label,
|
||||
component: ListTagTypes,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -298,7 +263,6 @@ export const routes = [
|
||||
{
|
||||
path: '/tags',
|
||||
title: 'Tags',
|
||||
icon: Label,
|
||||
component: ListTags,
|
||||
hidden: true,
|
||||
type: 'protected',
|
||||
@ -325,7 +289,6 @@ export const routes = [
|
||||
{
|
||||
path: '/addons',
|
||||
title: 'Addons',
|
||||
icon: DeviceHub,
|
||||
component: Addons,
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
@ -334,7 +297,6 @@ export const routes = [
|
||||
{
|
||||
path: '/reporting',
|
||||
title: 'Reporting',
|
||||
icon: Report,
|
||||
component: Reporting,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
@ -367,7 +329,6 @@ export const routes = [
|
||||
{
|
||||
path: '/admin-invoices',
|
||||
title: 'Invoices',
|
||||
icon: Money,
|
||||
component: AdminInvoice,
|
||||
hidden: true,
|
||||
type: 'protected',
|
||||
@ -376,7 +337,6 @@ export const routes = [
|
||||
{
|
||||
path: '/admin',
|
||||
title: 'Admin',
|
||||
icon: Album,
|
||||
component: Admin,
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
@ -385,7 +345,6 @@ export const routes = [
|
||||
{
|
||||
path: '/logout',
|
||||
title: 'Sign out',
|
||||
icon: ExitToApp,
|
||||
component: LogoutFeatures,
|
||||
type: 'unprotected',
|
||||
layout: 'main',
|
||||
@ -393,7 +352,6 @@ export const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
title: 'Log in',
|
||||
icon: Person,
|
||||
component: Login,
|
||||
type: 'unprotected',
|
||||
hidden: true,
|
||||
@ -430,3 +388,28 @@ export const getRoute = path => routes.find(route => route.path === path);
|
||||
export const baseRoutes = routes
|
||||
.filter(route => !route.hidden)
|
||||
.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 ProjectFeatureToggles from './ProjectFeatureToggles/ProjectFeatureToggles';
|
||||
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 { id } = useParams<{ id: string }>();
|
||||
const params = useQueryParams();
|
||||
const { project, error, loading, refetch } = useProject(id);
|
||||
const ref = useLoading(loading);
|
||||
const { toast, setToastData } = useToast();
|
||||
const { members, features, health } = project;
|
||||
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 (
|
||||
<div ref={ref}>
|
||||
<h1 data-loading className={commonStyles.title}>
|
||||
{project?.name}
|
||||
{project?.name}{' '}
|
||||
<IconButton component={Link} to={`/projects/edit/${id}`}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</h1>
|
||||
<ConditionallyRender
|
||||
condition={error}
|
||||
@ -32,7 +58,7 @@ const Project = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div style={containerStyles}>
|
||||
<div className={styles.containerStyles}>
|
||||
<ProjectInfo
|
||||
id={id}
|
||||
memberCount={members}
|
||||
@ -41,6 +67,7 @@ const Project = () => {
|
||||
/>
|
||||
<ProjectFeatureToggles features={features} loading={loading} />
|
||||
</div>
|
||||
{toast}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,6 +6,10 @@ export const useStyles = makeStyles(theme => ({
|
||||
marginLeft: '2rem',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginLeft: '0',
|
||||
paddingBottom: '4rem',
|
||||
},
|
||||
},
|
||||
header: {
|
||||
padding: '1rem',
|
||||
@ -22,4 +26,10 @@ export const useStyles = makeStyles(theme => ({
|
||||
height: '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 { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { IFeatureToggleListItem } from '../../../../interfaces/featureToggle';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import { PROJECTFILTERING } from '../../../common/flags';
|
||||
import HeaderTitle from '../../../common/HeaderTitle';
|
||||
import PageContent from '../../../common/PageContent';
|
||||
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
|
||||
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
|
||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||
|
||||
@ -21,6 +23,7 @@ const ProjectFeatureToggles = ({
|
||||
}: IProjectFeatureToggles) => {
|
||||
const styles = useStyles();
|
||||
const { id } = useParams();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
@ -44,24 +47,46 @@ const ProjectFeatureToggles = ({
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to="/features/create"
|
||||
data-loading
|
||||
<ResponsiveButton
|
||||
onClick={() =>
|
||||
history.push(
|
||||
`/features/create?project=${id}`
|
||||
)
|
||||
}
|
||||
maxWidth="700px"
|
||||
tooltip="New feature toggle"
|
||||
Icon={Add}
|
||||
>
|
||||
New feature toggle
|
||||
</Button>
|
||||
</ResponsiveButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FeatureToggleListNew
|
||||
features={features}
|
||||
loading={loading}
|
||||
projectId={id}
|
||||
<ConditionallyRender
|
||||
condition={features?.length > 0}
|
||||
show={
|
||||
<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>
|
||||
);
|
||||
|
@ -3,11 +3,23 @@ import { makeStyles } from '@material-ui/core/styles';
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
projectInfo: {
|
||||
width: '275px',
|
||||
padding: '1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
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: {
|
||||
marginBottom: '1.25rem',
|
||||
@ -15,10 +27,35 @@ export const useStyles = makeStyles(theme => ({
|
||||
emphazisedText: {
|
||||
fontSize: '1.5rem',
|
||||
marginBottom: '1rem',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
fontSize: '1rem',
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
},
|
||||
infoSection: {
|
||||
margin: '1.8rem 0',
|
||||
margin: '0',
|
||||
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: {
|
||||
color: '#635dc5',
|
||||
@ -27,5 +64,14 @@ export const useStyles = makeStyles(theme => ({
|
||||
infoLink: {
|
||||
textDecoration: 'none',
|
||||
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 { Link } from 'react-router-dom';
|
||||
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
|
||||
@ -33,15 +32,19 @@ const ProjectInfo = ({
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<Paper className={styles.projectInfo}>
|
||||
<div className={styles.infoSection} data-loading>
|
||||
<ProjectIcon />
|
||||
</div>
|
||||
|
||||
<div className={styles.infoSection} data-loading>
|
||||
<p className={styles.subtitle}>Overall health rating</p>
|
||||
<p className={styles.emphazisedText}>{health}%</p>
|
||||
<div className={styles.projectInfo}>
|
||||
<div className={styles.infoSection}>
|
||||
<div data-loading>
|
||||
<ProjectIcon className={styles.projectIcon} />
|
||||
</div>
|
||||
<p className={styles.subtitle} data-loading>
|
||||
Overall health rating
|
||||
</p>
|
||||
<p className={styles.emphazisedText} data-loading>
|
||||
{health}%
|
||||
</p>
|
||||
<Link
|
||||
data-loading
|
||||
className={classnames(
|
||||
commonStyles.flexRow,
|
||||
commonStyles.justifyCenter,
|
||||
@ -49,15 +52,37 @@ const ProjectInfo = ({
|
||||
)}
|
||||
to="/reporting"
|
||||
>
|
||||
view more{' '}
|
||||
<ArrowForwardIcon className={styles.arrowIcon} />
|
||||
<span className={styles.linkText} data-loading>
|
||||
view more{' '}
|
||||
</span>
|
||||
<ArrowForwardIcon
|
||||
data-loading
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.infoSection} data-loading>
|
||||
<p className={styles.subtitle}>Project members</p>
|
||||
<p className={styles.emphazisedText}>{memberCount}</p>
|
||||
<div className={styles.infoSection}>
|
||||
<p className={styles.subtitle} data-loading>
|
||||
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
|
||||
data-loading
|
||||
className={classnames(
|
||||
commonStyles.flexRow,
|
||||
commonStyles.justifyCenter,
|
||||
@ -65,16 +90,16 @@ const ProjectInfo = ({
|
||||
)}
|
||||
to={link}
|
||||
>
|
||||
view more{' '}
|
||||
<ArrowForwardIcon className={styles.arrowIcon} />
|
||||
<span className={styles.linkText} data-loading>
|
||||
view more{' '}
|
||||
</span>
|
||||
<ArrowForwardIcon
|
||||
data-loading
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.infoSection} data-loading>
|
||||
<p className={styles.subtitle}>Feature toggles</p>
|
||||
<p className={styles.emphazisedText}>{featureCount}</p>
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
@ -11,6 +11,9 @@ export const useStyles = makeStyles(theme => ({
|
||||
margin: '0.5rem',
|
||||
boxShadow: 'none',
|
||||
border: '1px solid #efefef',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
@ -38,4 +41,11 @@ export const useStyles = makeStyles(theme => ({
|
||||
color: '#4A4599',
|
||||
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 MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
|
||||
import { ReactComponent as ProjectIcon } from '../../../assets/icons/projectIcon.svg';
|
||||
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 {
|
||||
name: string;
|
||||
featureCount: number;
|
||||
health: number;
|
||||
memberCount: number;
|
||||
id: string;
|
||||
onHover: () => void;
|
||||
setToastData: Dispatch<
|
||||
SetStateAction<{
|
||||
show: boolean;
|
||||
type: string;
|
||||
text: string;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
const ProjectCard = ({
|
||||
@ -20,20 +33,66 @@ const ProjectCard = ({
|
||||
health,
|
||||
memberCount,
|
||||
onHover,
|
||||
id,
|
||||
setToastData,
|
||||
}: IProjectCardProps) => {
|
||||
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 (
|
||||
<Card className={styles.projectCard} onMouseEnter={onHover}>
|
||||
<div className={styles.header} data-loading>
|
||||
<h2 className={styles.title}>{name}</h2>
|
||||
<ConditionallyRender
|
||||
condition={PROJECTCARDACTIONS}
|
||||
condition={true}
|
||||
show={
|
||||
<IconButton data-loading>
|
||||
<IconButton
|
||||
className={styles.actionsBtn}
|
||||
data-loading
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</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 data-loading>
|
||||
<ProjectIcon className={styles.projectIcon} />
|
||||
@ -59,6 +118,38 @@ const ProjectCard = ({
|
||||
<p data-loading>members</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -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 ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import ProjectCard from '../ProjectCard/ProjectCard';
|
||||
import { useStyles } from './ProjectListNew.styles';
|
||||
import { useStyles } from './ProjectList.styles';
|
||||
import { IProjectCard } from '../../../interfaces/project';
|
||||
|
||||
import loadingData from './loadingData';
|
||||
@ -18,6 +18,7 @@ import { CREATE_PROJECT } from '../../AccessProvider/permissions';
|
||||
|
||||
import { Add } from '@material-ui/icons';
|
||||
import ApiError from '../../common/ApiError/ApiError';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
|
||||
type projectMap = {
|
||||
[index: string]: boolean;
|
||||
@ -26,6 +27,7 @@ type projectMap = {
|
||||
const ProjectListNew = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const history = useHistory();
|
||||
const { toast, setToastData } = useToast();
|
||||
|
||||
const styles = useStyles();
|
||||
const { projects, loading, error, refetch } = useProjects();
|
||||
@ -74,7 +76,9 @@ const ProjectListNew = () => {
|
||||
name={project?.name}
|
||||
memberCount={project?.memberCount}
|
||||
health={project?.health}
|
||||
id={project?.id}
|
||||
featureCount={project?.featureCount}
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
@ -88,10 +92,12 @@ const ProjectListNew = () => {
|
||||
data-loading
|
||||
onHover={() => {}}
|
||||
key={project.id}
|
||||
projectName={project.name}
|
||||
members={2}
|
||||
name={project.name}
|
||||
id={project.id}
|
||||
memberCount={2}
|
||||
health={95}
|
||||
toggles={4}
|
||||
featureCount={4}
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -114,7 +120,9 @@ const ProjectListNew = () => {
|
||||
}
|
||||
maxWidth="700px"
|
||||
tooltip="Add new project"
|
||||
/>
|
||||
>
|
||||
Add new project
|
||||
</ResponsiveButton>
|
||||
}
|
||||
/>
|
||||
}
|
||||
@ -129,6 +137,7 @@ const ProjectListNew = () => {
|
||||
elseShow={renderProjects()}
|
||||
/>
|
||||
</div>
|
||||
{toast}
|
||||
</PageContent>
|
||||
</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 { TextField, Typography } from '@material-ui/core';
|
||||
import { TextField, Typography, Button } from '@material-ui/core';
|
||||
|
||||
import styles from './Project.module.scss';
|
||||
import classnames from 'classnames';
|
||||
@ -10,7 +10,7 @@ import PageContent from '../common/PageContent/PageContent';
|
||||
import AccessContext from '../../contexts/AccessContext';
|
||||
import ConditionallyRender from '../common/ConditionallyRender';
|
||||
import { CREATE_PROJECT } from '../AccessProvider/permissions';
|
||||
import { Link } from 'react-router-dom';
|
||||
import HeaderTitle from '../common/HeaderTitle';
|
||||
|
||||
class ProjectFormComponent extends Component {
|
||||
static contextType = AccessContext;
|
||||
@ -75,15 +75,10 @@ class ProjectFormComponent extends Component {
|
||||
};
|
||||
|
||||
onCancel = evt => {
|
||||
const { editMode } = this.props;
|
||||
const { project } = this.state;
|
||||
|
||||
evt.preventDefault();
|
||||
if (editMode) {
|
||||
this.props.history.push(`/projects/view/${project.id}`);
|
||||
} else {
|
||||
this.props.history.push('/projects');
|
||||
}
|
||||
|
||||
this.props.history.push(`/projects/${project.id}`);
|
||||
};
|
||||
|
||||
onSubmit = async evt => {
|
||||
@ -93,8 +88,10 @@ class ProjectFormComponent extends Component {
|
||||
const valid = await this.validate(project.id);
|
||||
|
||||
if (valid) {
|
||||
const { editMode } = this.props;
|
||||
const query = editMode ? 'edited=true' : 'created=true';
|
||||
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 (
|
||||
<PageContent
|
||||
headerContent={
|
||||
<div>
|
||||
<span>{submitText} Project</span>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_PROJECT) && editMode}
|
||||
show={
|
||||
<Link
|
||||
to={`/projects/${project.id}/access`}
|
||||
style={{ float: 'right' }}
|
||||
>
|
||||
Manage access
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<HeaderTitle
|
||||
title={`${submitText} Project`}
|
||||
actions={
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasAccess(CREATE_PROJECT) && editMode
|
||||
}
|
||||
show={
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
this.props.history.push(
|
||||
`/projects/${project.id}/access`
|
||||
)
|
||||
}
|
||||
>
|
||||
Manage access
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
|
@ -3,6 +3,12 @@
|
||||
exports[`renders correctly with one strategy 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-3"
|
||||
@ -132,6 +138,12 @@ exports[`renders correctly with one strategy 1`] = `
|
||||
exports[`renders correctly with one strategy without permissions 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-3"
|
||||
|
@ -3,6 +3,12 @@
|
||||
exports[`renders correctly with one strategy 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
|
@ -24,6 +24,10 @@
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
background-color: #ecebeb;
|
||||
background-image: url('../assets/img/texture.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: right;
|
||||
background-position-y: bottom;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@ -3,6 +3,12 @@
|
||||
exports[`it supports editMode 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
@ -100,6 +106,12 @@ exports[`it supports editMode 1`] = `
|
||||
exports[`renders correctly for creating 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
@ -197,6 +209,12 @@ exports[`renders correctly for creating 1`] = `
|
||||
exports[`renders correctly for creating without permissions 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
|
@ -3,6 +3,12 @@
|
||||
exports[`renders a list with elements correctly 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
@ -146,6 +152,12 @@ exports[`renders a list with elements correctly 1`] = `
|
||||
exports[`renders an empty list correctly 1`] = `
|
||||
<div
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "10px",
|
||||
"boxShadow": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-1"
|
||||
|
@ -62,7 +62,6 @@ const UserProfile = ({
|
||||
styles.button
|
||||
)}
|
||||
onClick={() => setShowProfile(prev => !prev)}
|
||||
tabIndex="1"
|
||||
role="button"
|
||||
disableRipple
|
||||
>
|
||||
|
@ -8,7 +8,7 @@ export const useStyles = makeStyles({
|
||||
position: 'relative',
|
||||
},
|
||||
button: {
|
||||
color: '#fff',
|
||||
color: 'inherit',
|
||||
padding: '0.2rem 1rem',
|
||||
},
|
||||
});
|
||||
|
@ -9,12 +9,14 @@ import {
|
||||
Select,
|
||||
InputLabel,
|
||||
} from '@material-ui/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classnames from 'classnames';
|
||||
import { useStyles } from './UserProfileContent.styles';
|
||||
import { useCommonStyles } from '../../../../common.styles';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import EditProfile from '../EditProfile/EditProfile';
|
||||
import legacyStyles from '../../user.module.scss';
|
||||
import usePermissions from '../../../../hooks/usePermissions';
|
||||
|
||||
const UserProfileContent = ({
|
||||
showProfile,
|
||||
@ -30,6 +32,7 @@ const UserProfileContent = ({
|
||||
const [updatedPassword, setUpdatedPassword] = useState(false);
|
||||
const [edititingProfile, setEditingProfile] = useState(false);
|
||||
const styles = useStyles();
|
||||
const { isAdmin } = usePermissions();
|
||||
|
||||
const setLocale = value => {
|
||||
updateSettingLocation('locale', value);
|
||||
@ -128,6 +131,27 @@ const UserProfileContent = ({
|
||||
</FormControl>
|
||||
</div>
|
||||
<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
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
@ -26,4 +26,10 @@ export const useStyles = makeStyles(theme => ({
|
||||
editingEmail: {
|
||||
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 => ({
|
||||
container: {
|
||||
margin: 'auto auto 0 auto',
|
||||
width: '200px',
|
||||
width: '230px',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginTop: '1rem',
|
||||
},
|
||||
|
@ -153,6 +153,20 @@ const useAPI = ({
|
||||
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 {
|
||||
|
@ -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',
|
||||
environment: '',
|
||||
slogan: 'The enterprise ready feature toggle service.',
|
||||
flags: {},
|
||||
flags: { P: false, C: false },
|
||||
links: [
|
||||
{
|
||||
value: 'Documentation',
|
||||
|
@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
import { defaultValue } from './defaultValue';
|
||||
import { IUiConfig } from '../../../../interfaces/uiConfig';
|
||||
|
||||
const REQUEST_KEY = 'api/admin/ui-config';
|
||||
|
||||
@ -15,7 +16,7 @@ const useUiConfig = () => {
|
||||
}).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 refetch = () => {
|
||||
@ -27,7 +28,7 @@ const useUiConfig = () => {
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
uiConfig: data || defaultValue,
|
||||
uiConfig: { ...defaultValue, ...data },
|
||||
error,
|
||||
loading,
|
||||
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';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
typography: {
|
||||
fontFamily: ['Sen', 'Roboto, sans-serif'],
|
||||
},
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#1A4049',
|
||||
light: '#B3DAED',
|
||||
dark: '#0A1A1d',
|
||||
main: '#635DC5',
|
||||
light: '#817AFE',
|
||||
dark: '#635DC5',
|
||||
},
|
||||
secondary: {
|
||||
main: '#122D33',
|
||||
light: '#40836f',
|
||||
dark: '#002c1d',
|
||||
main: '#635DC5',
|
||||
light: '#817AFE',
|
||||
dark: '#635DC5',
|
||||
},
|
||||
grey: {
|
||||
main: '#6C6C6C',
|
||||
},
|
||||
neutral: {
|
||||
main: '#18243e',
|
||||
|
Loading…
Reference in New Issue
Block a user