1
0
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:
Fredrik Strand Oseberg 2021-07-16 15:41:54 +02:00 committed by GitHub
parent 16e0a7b4de
commit 1a63d91f95
86 changed files with 1422 additions and 772 deletions

View File

@ -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 {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -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',

View File

@ -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}>

View File

@ -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';

View File

@ -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]);

View File

@ -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"

View File

@ -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',
},
}));

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -23,7 +23,6 @@ const renderProclamation = (id: string) => {
return false;
}
}
console.log('RETURNING TRUE');
return true;
};

View File

@ -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>
}
/>

View File

@ -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')]: {

View File

@ -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}

View File

@ -3,5 +3,4 @@ export const C = 'C';
export const RBAC = 'RBAC';
export const OIDC = 'OIDC';
export const PROJECTCARDACTIONS = false;
export const PROJECTFILTERING = false;

View File

@ -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);
};

View File

@ -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,
},
}));

View File

@ -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"

View File

@ -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',

View File

@ -89,7 +89,8 @@ const FeatureToggleListNew = ({
<TableCell
className={classnames(
styles.tableCell,
styles.tableCellHeader
styles.tableCellHeader,
styles.typeHeader
)}
align="left"
>

View File

@ -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}
</>
);
};

View File

@ -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}>

View File

@ -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)}
/>

View File

@ -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%',
},
},
}));

View File

@ -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',
},

View File

@ -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>
</>
}
/>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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;

View 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',
},
}));

View 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;

View File

@ -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',
},
}));

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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',
},
}));

View File

@ -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`;

View File

@ -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",

View File

@ -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();
});

View File

@ -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,

View File

@ -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 {

View File

@ -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();

View 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',
},
},
}));

View File

@ -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>
);
};

View File

@ -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',
},
}));

View File

@ -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>
);

View File

@ -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',
},
},
}));

View File

@ -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>
);
};

View File

@ -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',
},
}));

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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',
},
}));

View File

@ -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>
);

View File

@ -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;

View 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;

View File

@ -1,11 +0,0 @@
import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles({
listItem: {
padding: 0,
['& a']: {
textDecoration: 'none',
color: 'inherit',
},
},
});

View File

@ -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' },
}));

View File

@ -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;

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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 {

View File

@ -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"

View File

@ -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"

View File

@ -62,7 +62,6 @@ const UserProfile = ({
styles.button
)}
onClick={() => setShowProfile(prev => !prev)}
tabIndex="1"
role="button"
disableRipple
>

View File

@ -8,7 +8,7 @@ export const useStyles = makeStyles({
position: 'relative',
},
button: {
color: '#fff',
color: 'inherit',
padding: '0.2rem 1rem',
},
});

View File

@ -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"

View File

@ -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%',
},
}));

View File

@ -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',
},

View File

@ -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 {

View File

@ -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;

View File

@ -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',

View File

@ -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,

View 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;

View File

@ -0,0 +1,8 @@
import { useContext } from 'react';
import AccessContext from '../contexts/AccessContext';
const usePermissions = () => {
return useContext(AccessContext);
};
export default usePermissions;

View 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;

View 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;
}

View File

@ -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',