mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
Fix/customer journey (#297)
* fix: add onClose to archive toggle dialoge * fix: add link to ConfirmUserLink component * fix: remove icons from admin menu * fix: move button on user list to top right * refactor: move add new api key to header * refactor: button order * fix: lowercase dropdown buttons on feature toggle list * refactor: reorganize reporting dashboard * refactor: consistent buttons * feat: enhance gradual rollout strategy creation * feat: ui tweaks on project access * fix: adjust divider * fix: remove unused imports * fix: update snapshots * fix: add auth options to new user page * fix: add divider * fix: uncontrolled input * fix: add data-loading to sorted by * fix: update snapshots * fix: navigate to project view on create and edit * fix: rename project * fix: add placeholder for feature toggle list component * fix: conditonally render link
This commit is contained in:
parent
e1034a458b
commit
cbd4773cf6
@ -13,7 +13,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.MuiButton-root {
|
.MuiButton-root {
|
||||||
border-radius: 25px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton {
|
.skeleton {
|
||||||
|
99
frontend/src/assets/icons/google.svg
Normal file
99
frontend/src/assets/icons/google.svg
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="46px"
|
||||||
|
height="46px"
|
||||||
|
viewBox="0 0 46 46"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
x="-50%"
|
||||||
|
y="-50%"
|
||||||
|
width="200%"
|
||||||
|
height="200%"
|
||||||
|
filterUnits="objectBoundingBox"
|
||||||
|
id="filter-1"
|
||||||
|
>
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="0.5"
|
||||||
|
in="shadowOffsetOuter1"
|
||||||
|
result="shadowBlurOuter1"
|
||||||
|
/>
|
||||||
|
<feColorMatrix
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0"
|
||||||
|
in="shadowBlurOuter1"
|
||||||
|
type="matrix"
|
||||||
|
result="shadowMatrixOuter1"
|
||||||
|
/>
|
||||||
|
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="0.5"
|
||||||
|
in="shadowOffsetOuter2"
|
||||||
|
result="shadowBlurOuter2"
|
||||||
|
/>
|
||||||
|
<feColorMatrix
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0"
|
||||||
|
in="shadowBlurOuter2"
|
||||||
|
type="matrix"
|
||||||
|
result="shadowMatrixOuter2"
|
||||||
|
/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1" />
|
||||||
|
<feMergeNode in="shadowMatrixOuter2" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="Google-Button"
|
||||||
|
stroke="none"
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
fillRule="evenodd"
|
||||||
|
>
|
||||||
|
<g id="9-PATCH" transform="translate(-608.000000, -160.000000)" />
|
||||||
|
<g
|
||||||
|
id="btn_google_light_normal"
|
||||||
|
transform="translate(-1.000000, -1.000000)"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="button"
|
||||||
|
transform="translate(4.000000, 4.000000)"
|
||||||
|
filter="url(#filter-1)"
|
||||||
|
>
|
||||||
|
<g id="button-bg">
|
||||||
|
<use fill="#FFFFFF" fillRule="evenodd" />
|
||||||
|
<use fill="none" />
|
||||||
|
<use fill="none" />
|
||||||
|
<use fill="none" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="logo_googleg_48dp" transform="translate(15.000000, 15.000000)">
|
||||||
|
<path
|
||||||
|
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#4285F4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#34A853"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#FBBC05"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#EA4335"
|
||||||
|
/>
|
||||||
|
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" />
|
||||||
|
</g>
|
||||||
|
<g id="handles_square" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
@ -47,9 +47,8 @@ const ReportCard = ({ features }) => {
|
|||||||
|
|
||||||
const total = features.length;
|
const total = features.length;
|
||||||
const activeTogglesArray = getActiveToggles();
|
const activeTogglesArray = getActiveToggles();
|
||||||
const potentiallyStaleToggles = getPotentiallyStaleToggles(
|
const potentiallyStaleToggles =
|
||||||
activeTogglesArray
|
getPotentiallyStaleToggles(activeTogglesArray);
|
||||||
);
|
|
||||||
|
|
||||||
const activeTogglesCount = activeTogglesArray.length;
|
const activeTogglesCount = activeTogglesArray.length;
|
||||||
const staleTogglesCount = features.length - activeTogglesCount;
|
const staleTogglesCount = features.length - activeTogglesCount;
|
||||||
@ -95,6 +94,17 @@ const ReportCard = ({ features }) => {
|
|||||||
return (
|
return (
|
||||||
<Paper className={styles.card}>
|
<Paper className={styles.card}>
|
||||||
<div className={styles.reportCardContainer}>
|
<div className={styles.reportCardContainer}>
|
||||||
|
<div className={styles.reportCardHealth}>
|
||||||
|
<h2 className={styles.header}>Health rating</h2>
|
||||||
|
<div className={styles.reportCardHealthInnerContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={healthRating > -1}
|
||||||
|
show={
|
||||||
|
<p className={healthClasses}>{healthRating}%</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className={styles.reportCardListContainer}>
|
<div className={styles.reportCardListContainer}>
|
||||||
<h2 className={styles.header}>Toggle report</h2>
|
<h2 className={styles.header}>Toggle report</h2>
|
||||||
<ul className={styles.reportCardList}>
|
<ul className={styles.reportCardList}>
|
||||||
@ -118,17 +128,7 @@ const ReportCard = ({ features }) => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.reportCardHealth}>
|
|
||||||
<h2 className={styles.header}>Health rating</h2>
|
|
||||||
<div className={styles.reportCardHealthInnerContainer}>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={healthRating > -1}
|
|
||||||
show={
|
|
||||||
<p className={healthClasses}>{healthRating}%</p>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.reportCardAction}>
|
<div className={styles.reportCardAction}>
|
||||||
<h2 className={styles.header}>Potential actions</h2>
|
<h2 className={styles.header}>Potential actions</h2>
|
||||||
<div className={styles.reportCardActionContainer}>
|
<div className={styles.reportCardActionContainer}>
|
||||||
|
@ -8,12 +8,27 @@ import CheckIcon from '@material-ui/icons/Check';
|
|||||||
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { pluralize, getDates, expired, toggleExpiryByTypeMap, getDiffInDays } from '../../utils';
|
import {
|
||||||
|
pluralize,
|
||||||
|
getDates,
|
||||||
|
expired,
|
||||||
|
toggleExpiryByTypeMap,
|
||||||
|
getDiffInDays,
|
||||||
|
} from '../../utils';
|
||||||
import { KILLSWITCH, PERMISSION } from '../../constants';
|
import { KILLSWITCH, PERMISSION } from '../../constants';
|
||||||
|
|
||||||
import styles from '../ReportToggleList.module.scss';
|
import styles from '../ReportToggleList.module.scss';
|
||||||
|
|
||||||
const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checked, bulkActionsOn, setFeatures }) => {
|
const ReportToggleListItem = ({
|
||||||
|
name,
|
||||||
|
stale,
|
||||||
|
lastSeenAt,
|
||||||
|
createdAt,
|
||||||
|
type,
|
||||||
|
checked,
|
||||||
|
bulkActionsOn,
|
||||||
|
setFeatures,
|
||||||
|
}) => {
|
||||||
const nameMatches = feature => feature.name === name;
|
const nameMatches = feature => feature.name === name;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@ -80,17 +95,26 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke
|
|||||||
|
|
||||||
const formatReportStatus = () => {
|
const formatReportStatus = () => {
|
||||||
if (type === KILLSWITCH || type === PERMISSION) {
|
if (type === KILLSWITCH || type === PERMISSION) {
|
||||||
return renderStatus(<CheckIcon className={styles.reportIcon} />, 'Active');
|
return renderStatus(
|
||||||
|
<CheckIcon className={styles.reportIcon} />,
|
||||||
|
'Healthy'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [date, now] = getDates(createdAt);
|
const [date, now] = getDates(createdAt);
|
||||||
const diff = getDiffInDays(date, now);
|
const diff = getDiffInDays(date, now);
|
||||||
|
|
||||||
if (expired(diff, type)) {
|
if (expired(diff, type)) {
|
||||||
return renderStatus(<ReportProblemOutlinedIcon className={styles.reportIcon} />, 'Potentially stale');
|
return renderStatus(
|
||||||
|
<ReportProblemOutlinedIcon className={styles.reportIcon} />,
|
||||||
|
'Potentially stale'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderStatus(<CheckIcon className={styles.reportIcon} />, 'Active');
|
return renderStatus(
|
||||||
|
<CheckIcon className={styles.reportIcon} />,
|
||||||
|
'Healthy'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToFeature = () => {
|
const navigateToFeature = () => {
|
||||||
@ -102,7 +126,12 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr role="button" tabIndex={0} onClick={navigateToFeature} className={styles.tableRow}>
|
<tr
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={navigateToFeature}
|
||||||
|
className={styles.tableRow}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={bulkActionsOn}
|
condition={bulkActionsOn}
|
||||||
show={
|
show={
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
emptyStateListItem: {
|
||||||
|
border: `2px dashed ${theme.palette.borders.main}`,
|
||||||
|
padding: '0.8rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,26 @@
|
|||||||
|
import { ListItem } from '@material-ui/core';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
|
import { useStyles } from './ListPlaceholder.styles';
|
||||||
|
|
||||||
|
interface IListPlaceholderProps {
|
||||||
|
text: string;
|
||||||
|
link?: string;
|
||||||
|
linkText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListPlaceholder = ({ text, link, linkText }: IListPlaceholderProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem className={styles.emptyStateListItem}>
|
||||||
|
{text}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(link && linkText)}
|
||||||
|
show={<Link to="/features/create">Add your first toggle</Link>}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListPlaceholder;
|
@ -78,7 +78,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdownButton {
|
.dropdownButton {
|
||||||
text-transform: none;
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@ export const FormButtons = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<Icon>add</Icon>}
|
|
||||||
>
|
>
|
||||||
{submitText}
|
{submitText}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -14,6 +14,8 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
useMediaQuery,
|
||||||
|
Button,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -24,6 +26,7 @@ import AccessContext from '../../../contexts/AccessContext';
|
|||||||
const ContextList = ({ removeContextField, history, contextFields }) => {
|
const ContextList = ({ removeContextField, history, contextFields }) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
const [name, setName] = useState();
|
const [name, setName] = useState();
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
@ -63,11 +66,27 @@ const ContextList = ({ removeContextField, history, contextFields }) => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(CREATE_CONTEXT_FIELD)}
|
condition={hasAccess(CREATE_CONTEXT_FIELD)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Add context type">
|
<ConditionallyRender
|
||||||
<IconButton onClick={() => history.push('/context/create')}>
|
condition={smallScreen}
|
||||||
<Icon>add</Icon>
|
show={
|
||||||
</IconButton>
|
<Tooltip title="Add context type">
|
||||||
</Tooltip>
|
<IconButton
|
||||||
|
onClick={() => history.push('/context/create')}
|
||||||
|
>
|
||||||
|
<Icon>add</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Button
|
||||||
|
onClick={() => history.push('/context/create')}
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Add new context field
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -26,6 +26,7 @@ import { CREATE_FEATURE } from '../../AccessProvider/permissions';
|
|||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||||
|
|
||||||
const FeatureToggleList = ({
|
const FeatureToggleList = ({
|
||||||
fetcher,
|
fetcher,
|
||||||
@ -41,7 +42,7 @@ const FeatureToggleList = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:800px)');
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
fetcher();
|
fetcher();
|
||||||
@ -102,13 +103,12 @@ const FeatureToggleList = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<ListItem className={styles.emptyStateListItem}>
|
<ListPlaceholder
|
||||||
No features available. Get started by adding a
|
text="No features available. Get started by adding a
|
||||||
new feature toggle.
|
new feature toggle."
|
||||||
<Link to="/features/create">
|
link="/features/create"
|
||||||
Add your first toggle
|
linkText="Add your first toggle"
|
||||||
</Link>
|
/>
|
||||||
</ListItem>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { MenuItem } from '@material-ui/core';
|
import { MenuItem, Typography } from '@material-ui/core';
|
||||||
import { MenuItemWithIcon } from '../../../common';
|
import { MenuItemWithIcon } from '../../../common';
|
||||||
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
|
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
|
||||||
import ProjectSelect from '../../../common/ProjectSelect';
|
import ProjectSelect from '../../../common/ProjectSelect';
|
||||||
@ -66,6 +66,9 @@ const FeatureToggleListActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.actions} ref={ref}>
|
<div className={styles.actions} ref={ref}>
|
||||||
|
<Typography variant="body2" data-loading>
|
||||||
|
Sorted by:
|
||||||
|
</Typography>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
id={'metric'}
|
id={'metric'}
|
||||||
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
||||||
@ -73,6 +76,7 @@ const FeatureToggleListActions = ({
|
|||||||
callback={toggleMetrics}
|
callback={toggleMetrics}
|
||||||
renderOptions={renderMetricsOptions}
|
renderOptions={renderMetricsOptions}
|
||||||
className=""
|
className=""
|
||||||
|
style={{ textTransform: 'lowercase' }}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
@ -82,11 +86,13 @@ const FeatureToggleListActions = ({
|
|||||||
renderOptions={renderSortingOptions}
|
renderOptions={renderSortingOptions}
|
||||||
title="Sort by"
|
title="Sort by"
|
||||||
className=""
|
className=""
|
||||||
|
style={{ textTransform: 'lowercase' }}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<ProjectSelect
|
<ProjectSelect
|
||||||
settings={settings}
|
settings={settings}
|
||||||
updateSetting={updateSetting}
|
updateSetting={updateSetting}
|
||||||
|
style={{ textTransform: 'lowercase' }}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,5 +6,7 @@ export const useStyles = makeStyles({
|
|||||||
margin: '0 0.25rem',
|
margin: '0 0.25rem',
|
||||||
},
|
},
|
||||||
marginRight: '0.25rem',
|
marginRight: '0.25rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -66,6 +66,12 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="makeStyles-actions-15"
|
className="makeStyles-actions-15"
|
||||||
>
|
>
|
||||||
|
<p
|
||||||
|
className="MuiTypography-root MuiTypography-body2"
|
||||||
|
data-loading={true}
|
||||||
|
>
|
||||||
|
Sorted by:
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
aria-controls="metric"
|
aria-controls="metric"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
@ -85,6 +91,11 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onTouchEnd={[Function]}
|
onTouchEnd={[Function]}
|
||||||
onTouchMove={[Function]}
|
onTouchMove={[Function]}
|
||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textTransform": "lowercase",
|
||||||
|
}
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
title="Metric interval"
|
title="Metric interval"
|
||||||
type="button"
|
type="button"
|
||||||
@ -124,6 +135,11 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onTouchEnd={[Function]}
|
onTouchEnd={[Function]}
|
||||||
onTouchMove={[Function]}
|
onTouchMove={[Function]}
|
||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textTransform": "lowercase",
|
||||||
|
}
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
title="Sort by"
|
title="Sort by"
|
||||||
type="button"
|
type="button"
|
||||||
@ -268,6 +284,12 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="makeStyles-actions-15"
|
className="makeStyles-actions-15"
|
||||||
>
|
>
|
||||||
|
<p
|
||||||
|
className="MuiTypography-root MuiTypography-body2"
|
||||||
|
data-loading={true}
|
||||||
|
>
|
||||||
|
Sorted by:
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
aria-controls="metric"
|
aria-controls="metric"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
@ -287,6 +309,11 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
onTouchEnd={[Function]}
|
onTouchEnd={[Function]}
|
||||||
onTouchMove={[Function]}
|
onTouchMove={[Function]}
|
||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textTransform": "lowercase",
|
||||||
|
}
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
title="Metric interval"
|
title="Metric interval"
|
||||||
type="button"
|
type="button"
|
||||||
@ -329,6 +356,11 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
onTouchEnd={[Function]}
|
onTouchEnd={[Function]}
|
||||||
onTouchMove={[Function]}
|
onTouchMove={[Function]}
|
||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textTransform": "lowercase",
|
||||||
|
}
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
title="Sort by"
|
title="Sort by"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -318,10 +318,6 @@ const FeatureView = ({
|
|||||||
<AddTagDialog
|
<AddTagDialog
|
||||||
featureToggleName={featureToggle.name}
|
featureToggleName={featureToggle.name}
|
||||||
/>
|
/>
|
||||||
<StatusUpdateComponent
|
|
||||||
stale={featureToggle.stale}
|
|
||||||
updateStale={updateStale}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
title="Create new feature toggle by cloning configuration"
|
title="Create new feature toggle by cloning configuration"
|
||||||
component={Link}
|
component={Link}
|
||||||
@ -329,6 +325,10 @@ const FeatureView = ({
|
|||||||
>
|
>
|
||||||
Clone
|
Clone
|
||||||
</Button>
|
</Button>
|
||||||
|
<StatusUpdateComponent
|
||||||
|
stale={featureToggle.stale}
|
||||||
|
updateStale={updateStale}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!hasAccess(DELETE_FEATURE, project)}
|
disabled={!hasAccess(DELETE_FEATURE, project)}
|
||||||
@ -368,6 +368,7 @@ const FeatureView = ({
|
|||||||
setDelDialog(false);
|
setDelDialog(false);
|
||||||
removeToggle();
|
removeToggle();
|
||||||
}}
|
}}
|
||||||
|
onClose={() => setDelDialog(false)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import InputPercentage from './input-percentage';
|
import InputPercentage from './input-percentage';
|
||||||
import Select from '../../../common/select';
|
import Select from '../../../common/select';
|
||||||
import { TextField, Typography } from '@material-ui/core';
|
import { Icon, TextField, Tooltip, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
const builtInStickinessOptions = [
|
const builtInStickinessOptions = [
|
||||||
{ key: 'default', label: 'default' },
|
{ key: 'default', label: 'default' },
|
||||||
@ -21,7 +21,9 @@ const FlexibleStrategy = ({ updateParameter, parameters, context }) => {
|
|||||||
builtInStickinessOptions.concat(
|
builtInStickinessOptions.concat(
|
||||||
context
|
context
|
||||||
.filter(c => c.stickiness)
|
.filter(c => c.stickiness)
|
||||||
.filter(c => !builtInStickinessOptions.find(s => s.key === c.name))
|
.filter(
|
||||||
|
c => !builtInStickinessOptions.find(s => s.key === c.name)
|
||||||
|
)
|
||||||
.map(c => ({ key: c.name, label: c.name }))
|
.map(c => ({ key: c.name, label: c.name }))
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -33,13 +35,37 @@ const FlexibleStrategy = ({ updateParameter, parameters, context }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<InputPercentage name="Rollout" value={1 * rollout} onChange={onUpdate('rollout')} />
|
<InputPercentage
|
||||||
|
name="Rollout"
|
||||||
|
value={1 * rollout}
|
||||||
|
onChange={onUpdate('rollout')}
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Tooltip
|
||||||
Stickiness
|
placement="right-start"
|
||||||
</Typography>
|
title="Stickiness defines what parameter should be used to ensure that your users get consistency in features. By default unleash will use the first value present in the context in the order of userId, sessionId and random."
|
||||||
<br />
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
style={{
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Stickiness
|
||||||
|
<Icon
|
||||||
|
style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'gray',
|
||||||
|
marginLeft: '0.2rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</Icon>
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
<Select
|
<Select
|
||||||
name="stickiness"
|
name="stickiness"
|
||||||
label="Stickiness"
|
label="Stickiness"
|
||||||
@ -48,6 +74,32 @@ const FlexibleStrategy = ({ updateParameter, parameters, context }) => {
|
|||||||
onChange={e => onUpdate('stickiness')(e, e.target.value)}
|
onChange={e => onUpdate('stickiness')(e, e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<Tooltip
|
||||||
|
placement="right-start"
|
||||||
|
title="GroupId is used to ensure that different toggles will hash differently for the same user. The groupId defaults to feature toggle name, but you can override it to correlate rollout of multiple feature toggles."
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
style={{
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
GroupId
|
||||||
|
<Icon
|
||||||
|
style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'gray',
|
||||||
|
marginLeft: '0.2rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</Icon>
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
<TextField
|
<TextField
|
||||||
label="groupId"
|
label="groupId"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -4,12 +4,22 @@ import { Tooltip, Icon, Typography } from '@material-ui/core';
|
|||||||
import StrategyConstraintInputField from '../StrategyConstraintInputField';
|
import StrategyConstraintInputField from '../StrategyConstraintInputField';
|
||||||
import { useCommonStyles } from '../../../../../common.styles';
|
import { useCommonStyles } from '../../../../../common.styles';
|
||||||
|
|
||||||
const StrategyConstraintInput = ({ constraints, updateConstraints, contextNames, contextFields, enabled }) => {
|
const StrategyConstraintInput = ({
|
||||||
|
constraints,
|
||||||
|
updateConstraints,
|
||||||
|
contextNames,
|
||||||
|
contextFields,
|
||||||
|
enabled,
|
||||||
|
}) => {
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const addConstraint = evt => {
|
const addConstraint = evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const updatedConstraints = [...constraints];
|
const updatedConstraints = [...constraints];
|
||||||
updatedConstraints.push({ contextName: contextNames[0], operator: 'IN', values: [] });
|
updatedConstraints.push({
|
||||||
|
contextName: contextNames[0],
|
||||||
|
operator: 'IN',
|
||||||
|
values: [],
|
||||||
|
});
|
||||||
|
|
||||||
updateConstraints(updatedConstraints);
|
updateConstraints(updatedConstraints);
|
||||||
};
|
};
|
||||||
@ -35,12 +45,22 @@ const StrategyConstraintInput = ({ constraints, updateConstraints, contextNames,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={commonStyles.contentSpacingY}>
|
<div className={commonStyles.contentSpacingY}>
|
||||||
<Typography variant="subtitle2">
|
<Tooltip
|
||||||
{'Constraints '}
|
placement="right-start"
|
||||||
<Tooltip title={<span>Use context fields to constrain the activation strategy.</span>}>
|
title={
|
||||||
<Icon style={{ fontSize: '0.9em', color: 'gray' }}>info</Icon>
|
<span>
|
||||||
</Tooltip>
|
Use context fields to constrain the activation strategy.
|
||||||
</Typography>
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2">
|
||||||
|
{'Constraints '}
|
||||||
|
|
||||||
|
<Icon style={{ fontSize: '0.9rem', color: 'gray' }}>
|
||||||
|
info
|
||||||
|
</Icon>
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
<table style={{ margin: 0 }}>
|
<table style={{ margin: 0 }}>
|
||||||
<tbody>
|
<tbody>
|
||||||
{constraints.map((c, index) => (
|
{constraints.map((c, index) => (
|
||||||
@ -56,7 +76,11 @@ const StrategyConstraintInput = ({ constraints, updateConstraints, contextNames,
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<small>
|
<small>
|
||||||
<a href="#add-constraint" title="Add constraint" onClick={addConstraint}>
|
<a
|
||||||
|
href="#add-constraint"
|
||||||
|
title="Add constraint"
|
||||||
|
onClick={addConstraint}
|
||||||
|
>
|
||||||
Add constraint
|
Add constraint
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
|
@ -216,6 +216,32 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<AddTagDialog
|
<AddTagDialog
|
||||||
featureToggleName="Another"
|
featureToggleName="Another"
|
||||||
/>
|
/>
|
||||||
|
<a
|
||||||
|
aria-disabled={false}
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
href="/features/copy/Another"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
title="Create new feature toggle by cloning configuration"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Clone
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<button
|
<button
|
||||||
aria-controls="feature-stale-dropdown"
|
aria-controls="feature-stale-dropdown"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
@ -258,32 +284,6 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
aria-disabled={false}
|
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
|
||||||
href="/features/copy/Another"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onDragLeave={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
onMouseDown={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseUp={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchMove={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
title="Create new feature toggle by cloning configuration"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiButton-label"
|
|
||||||
>
|
|
||||||
Clone
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
@ -35,7 +35,7 @@ import AdminAuth from '../../page/admin/auth';
|
|||||||
import Reporting from '../../page/reporting';
|
import Reporting from '../../page/reporting';
|
||||||
import Login from '../user/Login';
|
import Login from '../user/Login';
|
||||||
import { P, C } from '../common/flags';
|
import { P, C } from '../common/flags';
|
||||||
import NewUser from '../user/NewUser/NewUser';
|
import NewUser from '../user/NewUser';
|
||||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +15,8 @@ import {
|
|||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Button,
|
||||||
|
useMediaQuery,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ConfirmDialogue from '../../common/Dialogue';
|
import ConfirmDialogue from '../../common/Dialogue';
|
||||||
@ -24,6 +26,7 @@ import AccessContext from '../../../contexts/AccessContext';
|
|||||||
|
|
||||||
const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
const [project, setProject] = useState(undefined);
|
const [project, setProject] = useState(undefined);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
@ -35,14 +38,27 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(CREATE_PROJECT)}
|
condition={hasAccess(CREATE_PROJECT)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Add new project">
|
<ConditionallyRender
|
||||||
<IconButton
|
condition={smallScreen}
|
||||||
aria-label="add-project"
|
show={
|
||||||
onClick={() => history.push('/projects/create')}
|
<Tooltip title="Add new project">
|
||||||
>
|
<IconButton
|
||||||
<Icon>add</Icon>
|
onClick={() => history.push('/projects/create')}
|
||||||
</IconButton>
|
>
|
||||||
</Tooltip>
|
<Icon>add</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Button
|
||||||
|
onClick={() => history.push('/projects/create')}
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Add new project
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { Typography, Button, List } from '@material-ui/core';
|
import { Typography, Button, List, ListItem } from '@material-ui/core';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
@ -7,8 +7,9 @@ import PageContent from '../../common/PageContent';
|
|||||||
|
|
||||||
import FeatureToggleListItem from '../../feature/FeatureToggleList/FeatureToggleListItem';
|
import FeatureToggleListItem from '../../feature/FeatureToggleList/FeatureToggleListItem';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||||
|
|
||||||
const ViewProject = ({
|
const ProjectView = ({
|
||||||
project,
|
project,
|
||||||
features,
|
features,
|
||||||
settings,
|
settings,
|
||||||
@ -81,10 +82,21 @@ const ViewProject = ({
|
|||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2">
|
||||||
Feature toggles in this project
|
Feature toggles in this project
|
||||||
</Typography>
|
</Typography>
|
||||||
<List>{renderProjectFeatures()}</List>
|
<List>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={features.length > 0}
|
||||||
|
show={renderProjectFeatures()}
|
||||||
|
elseShow={
|
||||||
|
<ListPlaceholder
|
||||||
|
text="No features available. Get started by adding a
|
||||||
|
new feature toggle."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewProject;
|
export default ProjectView;
|
@ -3,7 +3,7 @@ import {
|
|||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
} from '../../../store/feature-toggle/actions';
|
} from '../../../store/feature-toggle/actions';
|
||||||
import ViewProject from './ViewProject';
|
import ViewProject from './ProjectView';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const projectBase = { id: '', name: '', description: '' };
|
const projectBase = { id: '', name: '', description: '' };
|
@ -68,7 +68,7 @@ function AddUserComponent({ roles, addUserToRole }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container justify="center" spacing={3} alignItems="flex-end">
|
<Grid container justify="left" spacing={3} alignItems="flex-end">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id="add-user-component"
|
id="add-user-component"
|
||||||
@ -93,6 +93,8 @@ function AddUserComponent({ roles, addUserToRole }) {
|
|||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="User"
|
label="User"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
name="search"
|
name="search"
|
||||||
onChange={handleQueryUpdate}
|
onChange={handleQueryUpdate}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -119,8 +121,15 @@ function AddUserComponent({ roles, addUserToRole }) {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FormControl>
|
<FormControl
|
||||||
<InputLabel id="add-user-select-role-label">
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: '125px' }}
|
||||||
|
>
|
||||||
|
<InputLabel
|
||||||
|
style={{ backgroundColor: '#fff' }}
|
||||||
|
id="add-user-select-role-label"
|
||||||
|
>
|
||||||
Role
|
Role
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
InputLabel,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
@ -19,21 +18,28 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
|
FormControl,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import AddUserComponent from './access-add-user';
|
import AddUserComponent from './access-add-user';
|
||||||
|
|
||||||
import projectApi from '../../store/project/api';
|
import projectApi from '../../store/project/api';
|
||||||
|
import PageContent from '../common/PageContent';
|
||||||
|
import HeaderTitle from '../common/HeaderTitle';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
function AccessComponent({ projectId, project }) {
|
function AccessComponent({ projectId, project }) {
|
||||||
const [roles, setRoles] = useState([]);
|
const [roles, setRoles] = useState([]);
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const fetchAccess = async () => {
|
const fetchAccess = async () => {
|
||||||
const access = await projectApi.fetchAccess(projectId);
|
const access = await projectApi.fetchAccess(projectId);
|
||||||
setRoles(access.roles);
|
setRoles(access.roles);
|
||||||
setUsers(access.users.map(u => ({ ...u, name: u.name || '(No name)' })));
|
setUsers(
|
||||||
|
access.users.map(u => ({ ...u, name: u.name || '(No name)' }))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -85,8 +91,23 @@ function AccessComponent({ projectId, project }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{ minHeight: '400px' }}>
|
<PageContent
|
||||||
<CardHeader title={`Managed Access for project "${project.name}"`} />
|
style={{ minHeight: '400px' }}
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
title={`Manage Access for project "${project.name}"`}
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => history.goBack()}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<AddUserComponent roles={roles} addUserToRole={addUser} />
|
<AddUserComponent roles={roles} addUserToRole={addUser} />
|
||||||
<Dialog
|
<Dialog
|
||||||
open={!!error}
|
open={!!error}
|
||||||
@ -96,14 +117,29 @@ function AccessComponent({ projectId, project }) {
|
|||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
|
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id="alert-dialog-description">{error}</DialogContentText>
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
{error}
|
||||||
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCloseError} color="secondary" autoFocus>
|
<Button
|
||||||
|
onClick={handleCloseError}
|
||||||
|
color="secondary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '1px',
|
||||||
|
width: '106.65%',
|
||||||
|
marginLeft: '-2rem',
|
||||||
|
backgroundColor: '#efefef',
|
||||||
|
marginTop: '2rem',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
<List>
|
<List>
|
||||||
{users.map(user => {
|
{users.map(user => {
|
||||||
const labelId = `checkbox-list-secondary-label-${user.id}`;
|
const labelId = `checkbox-list-secondary-label-${user.id}`;
|
||||||
@ -112,25 +148,50 @@ function AccessComponent({ projectId, project }) {
|
|||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar alt={user.name} src={user.imageUrl} />
|
<Avatar alt={user.name} src={user.imageUrl} />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText id={labelId} primary={user.name} secondary={user.email || user.username} />
|
<ListItemText
|
||||||
<ListItemSecondaryAction>
|
id={labelId}
|
||||||
<Select
|
primary={user.name}
|
||||||
labelId={`role-${user.id}-select-label`}
|
secondary={user.email || user.username}
|
||||||
id={`role-${user.id}-select`}
|
/>
|
||||||
placeholder="Choose role"
|
<ListItemSecondaryAction
|
||||||
value={user.roleId}
|
style={{
|
||||||
onChange={handleRoleChange(user.id, user.roleId)}
|
display: 'flex',
|
||||||
>
|
alignItems: 'center',
|
||||||
<MenuItem value="" disabled>
|
}}
|
||||||
Choose role
|
>
|
||||||
</MenuItem>
|
<FormControl variant="outlined" size="small">
|
||||||
{roles.map(role => (
|
<InputLabel
|
||||||
<MenuItem key={`${user.id}:${role.id}`} value={role.id}>
|
style={{ backgroundColor: '#fff' }}
|
||||||
{role.name}
|
for="add-user-select-role-label"
|
||||||
|
>
|
||||||
|
Role
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`role-${user.id}-select-label`}
|
||||||
|
id={`role-${user.id}-select`}
|
||||||
|
key={user.id}
|
||||||
|
placeholder="Choose role"
|
||||||
|
value={user.roleId || ''}
|
||||||
|
onChange={handleRoleChange(
|
||||||
|
user.id,
|
||||||
|
user.roleId
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled>
|
||||||
|
Choose role
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
{roles.map(role => (
|
||||||
</Select>
|
<MenuItem
|
||||||
|
key={`${user.id}:${role.id}`}
|
||||||
|
value={role.id}
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
style={{ marginLeft: '0.5rem' }}
|
||||||
edge="end"
|
edge="end"
|
||||||
aria-label="delete"
|
aria-label="delete"
|
||||||
title="Remove access"
|
title="Remove access"
|
||||||
@ -143,7 +204,7 @@ function AccessComponent({ projectId, project }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
</Card>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,8 +75,15 @@ class ProjectFormComponent extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onCancel = evt => {
|
onCancel = evt => {
|
||||||
|
const { editMode } = this.props;
|
||||||
|
const { project } = this.state;
|
||||||
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.props.history.push('/projects');
|
if (editMode) {
|
||||||
|
this.props.history.push(`/projects/view/${project.id}`);
|
||||||
|
} else {
|
||||||
|
this.props.history.push('/projects');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = async evt => {
|
onSubmit = async evt => {
|
||||||
@ -87,7 +94,7 @@ class ProjectFormComponent extends Component {
|
|||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
await this.props.submit(project);
|
await this.props.submit(project);
|
||||||
this.props.history.push('/projects');
|
this.props.history.push(`/projects/view/${project.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,21 +105,24 @@ class ProjectFormComponent extends Component {
|
|||||||
const submitText = editMode ? 'Update' : 'Create';
|
const submitText = editMode ? 'Update' : 'Create';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
headerContent={<div>
|
headerContent={
|
||||||
<span>{submitText} Project</span>
|
<div>
|
||||||
<ConditionallyRender
|
<span>{submitText} Project</span>
|
||||||
condition={hasAccess(CREATE_PROJECT) && editMode}
|
<ConditionallyRender
|
||||||
show={
|
condition={hasAccess(CREATE_PROJECT) && editMode}
|
||||||
<Link
|
show={
|
||||||
to={`/projects/${project.id}/access`}
|
<Link
|
||||||
style={{float: 'right'}}
|
to={`/projects/${project.id}/access`}
|
||||||
>
|
style={{ float: 'right' }}
|
||||||
Manage access
|
>
|
||||||
</Link>
|
Manage access
|
||||||
}
|
</Link>
|
||||||
/>
|
}
|
||||||
</div>}>
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
style={{ marginBottom: '0.5rem' }}
|
style={{ marginBottom: '0.5rem' }}
|
||||||
@ -169,8 +179,6 @@ class ProjectFormComponent extends Component {
|
|||||||
this.setValue('description', v.target.value)
|
this.setValue('description', v.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(CREATE_PROJECT)}
|
condition={hasAccess(CREATE_PROJECT)}
|
||||||
|
@ -56,12 +56,8 @@ class WrapperComponent extends Component {
|
|||||||
|
|
||||||
onSubmit = async evt => {
|
onSubmit = async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const {
|
const { createStrategy, updateStrategy, history, editMode } =
|
||||||
createStrategy,
|
this.props;
|
||||||
updateStrategy,
|
|
||||||
history,
|
|
||||||
editMode,
|
|
||||||
} = this.props;
|
|
||||||
const { strategy } = this.state;
|
const { strategy } = this.state;
|
||||||
|
|
||||||
const parameters = (strategy.parameters || [])
|
const parameters = (strategy.parameters || [])
|
||||||
|
@ -60,16 +60,6 @@ exports[`it supports editMode 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="MuiButton-label"
|
className="MuiButton-label"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
className="MuiButton-startIcon MuiButton-iconSizeMedium"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
add
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
Update
|
Update
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -166,16 +156,6 @@ exports[`renders correctly for creating 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="MuiButton-label"
|
className="MuiButton-label"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
className="MuiButton-startIcon MuiButton-iconSizeMedium"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
add
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
Create
|
Create
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,13 +2,12 @@ import React, { useState } from 'react';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Grid, TextField, Typography } from '@material-ui/core';
|
import { Button, Grid, TextField, Typography } from '@material-ui/core';
|
||||||
import LockRounded from '@material-ui/icons/LockRounded';
|
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import { useStyles } from './HostedAuth.styles';
|
import { useStyles } from './HostedAuth.styles';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { GoogleSvg } from './Icons';
|
|
||||||
import useQueryParams from '../../../hooks/useQueryParams';
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
|
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||||
|
|
||||||
const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
@ -64,7 +63,6 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const { usernameError, passwordError, apiError } = errors;
|
const { usernameError, passwordError, apiError } = errors;
|
||||||
const { options = [] } = authDetails;
|
const { options = [] } = authDetails;
|
||||||
|
|
||||||
@ -72,19 +70,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
{options.map(o => (
|
<AuthOptions options={options} />
|
||||||
<div
|
|
||||||
key={o.type}
|
|
||||||
className={classnames(
|
|
||||||
styles.contentContainer,
|
|
||||||
commonStyles.contentSpacingY
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Button color="primary" variant="outlined" href={o.path} startIcon={o.type === 'google' ? <GoogleSvg /> : <LockRounded />}>
|
|
||||||
{o.message}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.fancyLine}>or</p>
|
<p className={styles.fancyLine}>or</p>
|
||||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||||
@ -140,9 +126,12 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<Typography variant="body2" align="center">Don't have an account? <br /> <a href="https://www.unleash-hosted.com/pricing">Sign up</a></Typography>
|
<Typography variant="body2" align="center">
|
||||||
|
Don't have an account? <br />{' '}
|
||||||
|
<a href="https://www.unleash-hosted.com/pricing">
|
||||||
|
Sign up
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,9 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
fontSize: '1.1rem',
|
fontSize: '1.1rem',
|
||||||
},
|
},
|
||||||
|
passwordHeader: {
|
||||||
|
marginTop: '2rem',
|
||||||
|
},
|
||||||
emailField: {
|
emailField: {
|
||||||
minWidth: '300px',
|
minWidth: '300px',
|
||||||
[theme.breakpoints.down('xs')]: {
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
@ -10,15 +10,16 @@ import useResetPassword from '../../../hooks/useResetPassword';
|
|||||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
||||||
|
import { IAuthStatus } from '../../../interfaces/user';
|
||||||
|
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||||
|
|
||||||
const NewUser = () => {
|
interface INewUserProps {
|
||||||
const {
|
user: IAuthStatus;
|
||||||
token,
|
}
|
||||||
data,
|
|
||||||
loading,
|
const NewUser = ({ user }: INewUserProps) => {
|
||||||
setLoading,
|
const { token, data, loading, setLoading, invalidToken } =
|
||||||
invalidToken,
|
useResetPassword();
|
||||||
} = useResetPassword();
|
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
@ -57,7 +58,7 @@ const NewUser = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
data-loading
|
data-loading
|
||||||
value={data?.email}
|
value={data?.email || ''}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
className={styles.emailField}
|
className={styles.emailField}
|
||||||
@ -79,9 +80,46 @@ const NewUser = () => {
|
|||||||
className={commonStyles.largeDivider}
|
className={commonStyles.largeDivider}
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<Typography variant="body1" data-loading>
|
<ConditionallyRender
|
||||||
Set a password for your account.
|
condition={
|
||||||
</Typography>
|
user?.authDetails?.options?.length > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Typography data-loading>
|
||||||
|
Login with 3rd party providers
|
||||||
|
</Typography>
|
||||||
|
<AuthOptions
|
||||||
|
options={
|
||||||
|
user?.authDetails?.options
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
commonStyles.largeDivider
|
||||||
|
}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
className={
|
||||||
|
styles.passwordHeader
|
||||||
|
}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
OR set a new password for your
|
||||||
|
account
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Set a password for your account.
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ResetPasswordDetails>
|
</ResetPasswordDetails>
|
||||||
}
|
}
|
||||||
|
8
frontend/src/component/user/NewUser/index.js
Normal file
8
frontend/src/component/user/NewUser/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import NewUser from './NewUser';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: any) => ({
|
||||||
|
user: state.user.toJS(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(NewUser);
|
@ -19,7 +19,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
switchesContainer: {
|
switchesContainer: {
|
||||||
position: 'fixed',
|
position: 'absolute',
|
||||||
bottom: '40px',
|
bottom: '40px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Button } from '@material-ui/core';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { useCommonStyles } from '../../../../common.styles';
|
||||||
|
import { IAuthOptions } from '../../../../interfaces/user';
|
||||||
|
import { ReactComponent as GoogleSvg } from '../../../../assets/icons/google.svg';
|
||||||
|
import LockRounded from '@material-ui/icons/LockRounded';
|
||||||
|
|
||||||
|
interface IAuthOptionProps {
|
||||||
|
options?: IAuthOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthOptions = ({ options }: IAuthOptionProps) => {
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options?.map(o => (
|
||||||
|
<div
|
||||||
|
key={o.type}
|
||||||
|
className={classnames(
|
||||||
|
commonStyles.flexColumn,
|
||||||
|
commonStyles.contentSpacingY
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
data-loading
|
||||||
|
variant="outlined"
|
||||||
|
href={o.path}
|
||||||
|
size="small"
|
||||||
|
style={{ maxWidth: '300px' }}
|
||||||
|
startIcon={
|
||||||
|
o.type === 'google' ? (
|
||||||
|
<GoogleSvg />
|
||||||
|
) : (
|
||||||
|
<LockRounded />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{o.message}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthOptions;
|
@ -111,17 +111,19 @@ const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password || ''}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
autoComplete="password"
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
type="password"
|
type="password"
|
||||||
value={confirmPassword}
|
value={confirmPassword || ''}
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
onChange={e => setConfirmPassword(e.target.value)}
|
onChange={e => setConfirmPassword(e.target.value)}
|
||||||
|
autoComplete="confirm-password"
|
||||||
data-loading
|
data-loading
|
||||||
/>
|
/>
|
||||||
<PasswordMatcher
|
<PasswordMatcher
|
||||||
|
@ -14,7 +14,13 @@ interface IAuthDetails {
|
|||||||
type: string;
|
type: string;
|
||||||
path: string;
|
path: string;
|
||||||
message: string;
|
message: string;
|
||||||
options: string[];
|
options: IAuthOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthOptions {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { Paper, Icon, Tabs, Tab } from '@material-ui/core';
|
import { Paper, Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
const navLinkStyle = {
|
const navLinkStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -18,38 +18,48 @@ const activeNavLinkStyle = {
|
|||||||
padding: '0.8rem 1.5rem',
|
padding: '0.8rem 1.5rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconStyle = {
|
function AdminMenu({ history }) {
|
||||||
marginRight: '5px',
|
|
||||||
};
|
|
||||||
|
|
||||||
function AdminMenu({history}) {
|
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
return (
|
return (
|
||||||
<Paper style={{ marginBottom: '1rem' }}>
|
<Paper style={{ marginBottom: '1rem' }}>
|
||||||
<Tabs centered value={pathname} >
|
<Tabs centered value={pathname}>
|
||||||
<Tab value="/admin/users" label={
|
<Tab
|
||||||
<NavLink to="/admin/users" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
|
value="/admin/users"
|
||||||
<Icon style={iconStyle}>supervised_user_circle</Icon>
|
label={
|
||||||
|
<NavLink
|
||||||
|
to="/admin/users"
|
||||||
|
activeStyle={activeNavLinkStyle}
|
||||||
|
style={navLinkStyle}
|
||||||
|
>
|
||||||
<span>Users</span>
|
<span>Users</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
}
|
}
|
||||||
>
|
></Tab>
|
||||||
</Tab>
|
<Tab
|
||||||
<Tab value="/admin/api" label={
|
value="/admin/api"
|
||||||
<NavLink to="/admin/api" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
|
label={
|
||||||
<Icon style={iconStyle}>apps</Icon>
|
<NavLink
|
||||||
API Access
|
to="/admin/api"
|
||||||
</NavLink>
|
activeStyle={activeNavLinkStyle}
|
||||||
}>
|
style={navLinkStyle}
|
||||||
</Tab>
|
>
|
||||||
<Tab value="/admin/auth" label={
|
API Access
|
||||||
<NavLink to="/admin/auth" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
|
</NavLink>
|
||||||
<Icon style={iconStyle}>lock</Icon>
|
}
|
||||||
Authentication
|
></Tab>
|
||||||
</NavLink>
|
<Tab
|
||||||
}>
|
value="/admin/auth"
|
||||||
</Tab>
|
label={
|
||||||
|
<NavLink
|
||||||
|
to="/admin/auth"
|
||||||
|
activeStyle={activeNavLinkStyle}
|
||||||
|
style={navLinkStyle}
|
||||||
|
>
|
||||||
|
Authentication
|
||||||
|
</NavLink>
|
||||||
|
}
|
||||||
|
></Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -13,10 +13,9 @@ import classnames from 'classnames';
|
|||||||
import { styles as commonStyles } from '../../../component/common';
|
import { styles as commonStyles } from '../../../component/common';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
function CreateApiKey({ addKey }) {
|
function CreateApiKey({ addKey, show, setShow }) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [type, setType] = useState('CLIENT');
|
const [type, setType] = useState('CLIENT');
|
||||||
const [show, setShow] = useState(false);
|
|
||||||
const [username, setUsername] = useState();
|
const [username, setUsername] = useState();
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
|
|
||||||
@ -107,6 +106,9 @@ function CreateApiKey({ addKey }) {
|
|||||||
|
|
||||||
CreateApiKey.propTypes = {
|
CreateApiKey.propTypes = {
|
||||||
addKey: PropTypes.func.isRequired,
|
addKey: PropTypes.func.isRequired,
|
||||||
|
setShow: PropTypes.func.isRequired,
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateApiKey;
|
export default CreateApiKey;
|
||||||
|
@ -21,6 +21,8 @@ import {
|
|||||||
DELETE_API_TOKEN,
|
DELETE_API_TOKEN,
|
||||||
CREATE_API_TOKEN,
|
CREATE_API_TOKEN,
|
||||||
} from '../../../component/AccessProvider/permissions';
|
} from '../../../component/AccessProvider/permissions';
|
||||||
|
import PageContent from '../../../component/common/PageContent';
|
||||||
|
import HeaderTitle from '../../../component/common/HeaderTitle';
|
||||||
|
|
||||||
function ApiKeyList({
|
function ApiKeyList({
|
||||||
location,
|
location,
|
||||||
@ -30,6 +32,8 @@ function ApiKeyList({
|
|||||||
keys,
|
keys,
|
||||||
unleashUrl,
|
unleashUrl,
|
||||||
}) {
|
}) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [delKey, setDelKey] = useState(undefined);
|
const [delKey, setDelKey] = useState(undefined);
|
||||||
@ -45,93 +49,111 @@ function ApiKeyList({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<PageContent
|
||||||
<Alert severity="info">
|
headerContent={
|
||||||
<p>
|
<HeaderTitle
|
||||||
Read the{' '}
|
title="API Access"
|
||||||
<a
|
actions={
|
||||||
href="https://docs.getunleash.io/docs"
|
<ConditionallyRender
|
||||||
target="_blank"
|
condition={hasAccess(CREATE_API_TOKEN)}
|
||||||
rel="noreferrer"
|
show={
|
||||||
>
|
<CreateApiKey
|
||||||
Getting started guide
|
addKey={addKey}
|
||||||
</a>{' '}
|
setShow={setShow}
|
||||||
to learn how to connect to the Unleash API from your
|
show={show}
|
||||||
application or programmatically. Please note it can take up
|
/>
|
||||||
to 1 minute before a new API key is activated.
|
}
|
||||||
</p>
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Alert severity="info">
|
||||||
|
<p>
|
||||||
|
Read the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.getunleash.io/docs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Getting started guide
|
||||||
|
</a>{' '}
|
||||||
|
to learn how to connect to the Unleash API from your
|
||||||
|
application or programmatically. Please note it can take
|
||||||
|
up to 1 minute before a new API key is activated.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<strong>API URL: </strong>{' '}
|
||||||
|
<pre style={{ display: 'inline' }}>{unleashUrl}/api/</pre>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<strong>API URL: </strong>{' '}
|
|
||||||
<pre style={{ display: 'inline' }}>{unleashUrl}/api/</pre>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<Table>
|
||||||
|
<TableHead>
|
||||||
<br />
|
<TableRow>
|
||||||
<Table>
|
<TableCell>Created</TableCell>
|
||||||
<TableHead>
|
<TableCell>Username</TableCell>
|
||||||
<TableRow>
|
<TableCell>Access Type</TableCell>
|
||||||
<TableCell>Created</TableCell>
|
<TableCell>Secret</TableCell>
|
||||||
<TableCell>Username</TableCell>
|
<TableCell>Action</TableCell>
|
||||||
<TableCell>Access Type</TableCell>
|
|
||||||
<TableCell>Secret</TableCell>
|
|
||||||
<TableCell>Action</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{keys.map(item => (
|
|
||||||
<TableRow key={item.secret}>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
{formatFullDateTimeWithLocale(
|
|
||||||
item.createdAt,
|
|
||||||
location.locale
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
{item.username}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
{item.type}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
|
||||||
<Secret value={item.secret} />
|
|
||||||
</TableCell>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(DELETE_API_TOKEN)}
|
|
||||||
show={
|
|
||||||
<TableCell style={{ textAlign: 'right' }}>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
setDelKey(item.secret);
|
|
||||||
setShowDelete(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon>delete</Icon>
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{keys.map(item => (
|
||||||
<Dialogue
|
<TableRow key={item.secret}>
|
||||||
open={showDelete}
|
<TableCell style={{ textAlign: 'left' }}>
|
||||||
onClick={deleteKey}
|
{formatFullDateTimeWithLocale(
|
||||||
onClose={() => {
|
item.createdAt,
|
||||||
setShowDelete(false);
|
location.locale
|
||||||
setDelKey(undefined);
|
)}
|
||||||
}}
|
</TableCell>
|
||||||
title="Really delete API key?"
|
<TableCell style={{ textAlign: 'left' }}>
|
||||||
>
|
{item.username}
|
||||||
<div>Are you sure you want to delete?</div>
|
</TableCell>
|
||||||
</Dialogue>
|
<TableCell style={{ textAlign: 'left' }}>
|
||||||
<ConditionallyRender
|
{item.type}
|
||||||
condition={hasAccess(CREATE_API_TOKEN)}
|
</TableCell>
|
||||||
show={<CreateApiKey addKey={addKey} />}
|
<TableCell style={{ textAlign: 'left' }}>
|
||||||
/>
|
<Secret value={item.secret} />
|
||||||
</div>
|
</TableCell>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(DELETE_API_TOKEN)}
|
||||||
|
show={
|
||||||
|
<TableCell
|
||||||
|
style={{ textAlign: 'right' }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setDelKey(item.secret);
|
||||||
|
setShowDelete(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon>delete</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<Dialogue
|
||||||
|
open={showDelete}
|
||||||
|
onClick={deleteKey}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDelete(false);
|
||||||
|
setDelKey(undefined);
|
||||||
|
}}
|
||||||
|
title="Really delete API key?"
|
||||||
|
>
|
||||||
|
<div>Are you sure you want to delete?</div>
|
||||||
|
</Dialogue>
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ApiKeyList from './api-key-list-container';
|
import ApiKeyList from './api-key-list-container';
|
||||||
|
|
||||||
import AdminMenu from '../admin-menu';
|
import AdminMenu from '../admin-menu';
|
||||||
import PageContent from '../../../component/common/PageContent/PageContent';
|
|
||||||
|
|
||||||
const render = ({history}) => (
|
const render = ({ history }) => {
|
||||||
<div>
|
return (
|
||||||
<AdminMenu history={history} />
|
<div>
|
||||||
<PageContent headerContent="API Access">
|
<AdminMenu history={history} />
|
||||||
<ApiKeyList />
|
<ApiKeyList />
|
||||||
</PageContent>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
render.propTypes = {
|
render.propTypes = {
|
||||||
match: PropTypes.object.isRequired,
|
match: PropTypes.object.isRequired,
|
||||||
|
@ -40,7 +40,7 @@ const ConfirmUserLink = ({
|
|||||||
Want to avoid this step in the future?{' '}
|
Want to avoid this step in the future?{' '}
|
||||||
{/* TODO - ADD LINK HERE ONCE IT EXISTS*/}
|
{/* TODO - ADD LINK HERE ONCE IT EXISTS*/}
|
||||||
<a
|
<a
|
||||||
href="https://docs.getunleash.ai/"
|
href="https://docs.getunleash.io/docs/deploy/email"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@ -23,7 +22,7 @@ import UserListItem from './UserListItem/UserListItem';
|
|||||||
import loadingData from './loadingData';
|
import loadingData from './loadingData';
|
||||||
import useLoading from '../../../../hooks/useLoading';
|
import useLoading from '../../../../hooks/useLoading';
|
||||||
|
|
||||||
function UsersList({ location }) {
|
function UsersList({ location, closeDialog, showDialog }) {
|
||||||
const { users, roles, refetch, loading } = useUsers();
|
const { users, roles, refetch, loading } = useUsers();
|
||||||
const {
|
const {
|
||||||
addUser,
|
addUser,
|
||||||
@ -35,7 +34,6 @@ function UsersList({ location }) {
|
|||||||
userApiErrors,
|
userApiErrors,
|
||||||
} = useAdminUsersApi();
|
} = useAdminUsersApi();
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDialog, setDialog] = useState(false);
|
|
||||||
const [pwDialog, setPwDialog] = useState({ open: false });
|
const [pwDialog, setPwDialog] = useState({ open: false });
|
||||||
const [delDialog, setDelDialog] = useState(false);
|
const [delDialog, setDelDialog] = useState(false);
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
@ -45,15 +43,6 @@ function UsersList({ location }) {
|
|||||||
const [updateDialog, setUpdateDialog] = useState({ open: false });
|
const [updateDialog, setUpdateDialog] = useState({ open: false });
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
|
|
||||||
const openDialog = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
setDialog(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDelDialog = () => {
|
const closeDelDialog = () => {
|
||||||
setDelDialog(false);
|
setDelDialog(false);
|
||||||
setDelUser(undefined);
|
setDelUser(undefined);
|
||||||
@ -177,19 +166,6 @@ function UsersList({ location }) {
|
|||||||
<TableBody>{renderUsers()}</TableBody>
|
<TableBody>{renderUsers()}</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<br />
|
<br />
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(ADMIN)}
|
|
||||||
show={
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={openDialog}
|
|
||||||
>
|
|
||||||
Add new user
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
elseShow={<small>PS! Only admins can add/remove users.</small>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmUserAdded
|
<ConfirmUserAdded
|
||||||
open={showConfirm}
|
open={showConfirm}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import UsersList from './UsersList';
|
import UsersList from './UsersList';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => {
|
||||||
location: state.settings.toJS().location || {},
|
return {
|
||||||
});
|
location: state.settings.toJS().location || {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const Container = connect(mapStateToProps)(UsersList);
|
const Container = connect(mapStateToProps)(UsersList);
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ const DelUserComponent = ({
|
|||||||
DelUserComponent.propTypes = {
|
DelUserComponent.propTypes = {
|
||||||
showDialog: propTypes.bool.isRequired,
|
showDialog: propTypes.bool.isRequired,
|
||||||
closeDialog: propTypes.func.isRequired,
|
closeDialog: propTypes.func.isRequired,
|
||||||
user: propTypes.object.isRequired,
|
user: propTypes.object,
|
||||||
removeUser: propTypes.func.isRequired,
|
removeUser: propTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import UsersList from './UsersList';
|
import UsersList from './UsersList';
|
||||||
import AdminMenu from '../admin-menu';
|
import AdminMenu from '../admin-menu';
|
||||||
@ -7,17 +7,60 @@ import AccessContext from '../../../contexts/AccessContext';
|
|||||||
import ConditionallyRender from '../../../component/common/ConditionallyRender';
|
import ConditionallyRender from '../../../component/common/ConditionallyRender';
|
||||||
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
|
import HeaderTitle from '../../../component/common/HeaderTitle';
|
||||||
|
import { Button } from '@material-ui/core';
|
||||||
|
|
||||||
const UsersAdmin = ({ history }) => {
|
const UsersAdmin = ({ history }) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const [showDialog, setDialog] = useState(false);
|
||||||
|
|
||||||
|
const openDialog = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AdminMenu history={history} />
|
<AdminMenu history={history} />
|
||||||
<PageContent headerContent="Users">
|
<PageContent
|
||||||
|
headerContent={
|
||||||
|
<HeaderTitle
|
||||||
|
title="Users"
|
||||||
|
actions={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(ADMIN)}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={openDialog}
|
||||||
|
>
|
||||||
|
Add new user
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<small>
|
||||||
|
PS! Only admins can add/remove users.
|
||||||
|
</small>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(ADMIN)}
|
condition={hasAccess(ADMIN)}
|
||||||
show={<UsersList />}
|
show={
|
||||||
|
<UsersList
|
||||||
|
openDialog={openDialog}
|
||||||
|
closeDialog={closeDialog}
|
||||||
|
showDialog={showDialog}
|
||||||
|
/>
|
||||||
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
You need instance admin to access this section.
|
You need instance admin to access this section.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ViewProject from '../../component/project/ViewProject';
|
import ViewProject from '../../component/project/ProjectView';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const render = ({ match: { params }, history }) => (
|
const render = ({ match: { params }, history }) => (
|
||||||
|
Loading…
Reference in New Issue
Block a user