mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01: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 {
|
||||
border-radius: 25px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.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 activeTogglesArray = getActiveToggles();
|
||||
const potentiallyStaleToggles = getPotentiallyStaleToggles(
|
||||
activeTogglesArray
|
||||
);
|
||||
const potentiallyStaleToggles =
|
||||
getPotentiallyStaleToggles(activeTogglesArray);
|
||||
|
||||
const activeTogglesCount = activeTogglesArray.length;
|
||||
const staleTogglesCount = features.length - activeTogglesCount;
|
||||
@ -95,6 +94,17 @@ const ReportCard = ({ features }) => {
|
||||
return (
|
||||
<Paper className={styles.card}>
|
||||
<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}>
|
||||
<h2 className={styles.header}>Toggle report</h2>
|
||||
<ul className={styles.reportCardList}>
|
||||
@ -118,17 +128,7 @@ const ReportCard = ({ features }) => {
|
||||
</li>
|
||||
</ul>
|
||||
</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}>
|
||||
<h2 className={styles.header}>Potential actions</h2>
|
||||
<div className={styles.reportCardActionContainer}>
|
||||
|
@ -8,12 +8,27 @@ import CheckIcon from '@material-ui/icons/Check';
|
||||
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
||||
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 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 history = useHistory();
|
||||
|
||||
@ -80,17 +95,26 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke
|
||||
|
||||
const formatReportStatus = () => {
|
||||
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 diff = getDiffInDays(date, now);
|
||||
|
||||
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 = () => {
|
||||
@ -102,7 +126,12 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke
|
||||
});
|
||||
|
||||
return (
|
||||
<tr role="button" tabIndex={0} onClick={navigateToFeature} className={styles.tableRow}>
|
||||
<tr
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={navigateToFeature}
|
||||
className={styles.tableRow}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={bulkActionsOn}
|
||||
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 {
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,6 @@ export const FormButtons = ({
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
startIcon={<Icon>add</Icon>}
|
||||
>
|
||||
{submitText}
|
||||
</Button>
|
||||
|
@ -14,6 +14,8 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -24,6 +26,7 @@ import AccessContext from '../../../contexts/AccessContext';
|
||||
const ContextList = ({ removeContextField, history, contextFields }) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||
const [name, setName] = useState();
|
||||
|
||||
const styles = useStyles();
|
||||
@ -63,11 +66,27 @@ const ContextList = ({ removeContextField, history, contextFields }) => {
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_CONTEXT_FIELD)}
|
||||
show={
|
||||
<Tooltip title="Add context type">
|
||||
<IconButton onClick={() => history.push('/context/create')}>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={smallScreen}
|
||||
show={
|
||||
<Tooltip title="Add context type">
|
||||
<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 { useStyles } from './styles';
|
||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||
|
||||
const FeatureToggleList = ({
|
||||
fetcher,
|
||||
@ -41,7 +42,7 @@ const FeatureToggleList = ({
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const styles = useStyles();
|
||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||
const smallScreen = useMediaQuery('(max-width:800px)');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
fetcher();
|
||||
@ -102,13 +103,12 @@ const FeatureToggleList = ({
|
||||
</ListItem>
|
||||
}
|
||||
elseShow={
|
||||
<ListItem className={styles.emptyStateListItem}>
|
||||
No features available. Get started by adding a
|
||||
new feature toggle.
|
||||
<Link to="/features/create">
|
||||
Add your first toggle
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListPlaceholder
|
||||
text="No features available. Get started by adding a
|
||||
new feature toggle."
|
||||
link="/features/create"
|
||||
linkText="Add your first toggle"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import { MenuItem, Typography } from '@material-ui/core';
|
||||
import { MenuItemWithIcon } from '../../../common';
|
||||
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
|
||||
import ProjectSelect from '../../../common/ProjectSelect';
|
||||
@ -66,6 +66,9 @@ const FeatureToggleListActions = ({
|
||||
|
||||
return (
|
||||
<div className={styles.actions} ref={ref}>
|
||||
<Typography variant="body2" data-loading>
|
||||
Sorted by:
|
||||
</Typography>
|
||||
<DropdownMenu
|
||||
id={'metric'}
|
||||
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
||||
@ -73,6 +76,7 @@ const FeatureToggleListActions = ({
|
||||
callback={toggleMetrics}
|
||||
renderOptions={renderMetricsOptions}
|
||||
className=""
|
||||
style={{ textTransform: 'lowercase' }}
|
||||
data-loading
|
||||
/>
|
||||
<DropdownMenu
|
||||
@ -82,11 +86,13 @@ const FeatureToggleListActions = ({
|
||||
renderOptions={renderSortingOptions}
|
||||
title="Sort by"
|
||||
className=""
|
||||
style={{ textTransform: 'lowercase' }}
|
||||
data-loading
|
||||
/>
|
||||
<ProjectSelect
|
||||
settings={settings}
|
||||
updateSetting={updateSetting}
|
||||
style={{ textTransform: 'lowercase' }}
|
||||
data-loading
|
||||
/>
|
||||
</div>
|
||||
|
@ -6,5 +6,7 @@ export const useStyles = makeStyles({
|
||||
margin: '0 0.25rem',
|
||||
},
|
||||
marginRight: '0.25rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
@ -66,6 +66,12 @@ exports[`renders correctly with one feature 1`] = `
|
||||
<div
|
||||
className="makeStyles-actions-15"
|
||||
>
|
||||
<p
|
||||
className="MuiTypography-root MuiTypography-body2"
|
||||
data-loading={true}
|
||||
>
|
||||
Sorted by:
|
||||
</p>
|
||||
<button
|
||||
aria-controls="metric"
|
||||
aria-haspopup="true"
|
||||
@ -85,6 +91,11 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
title="Metric interval"
|
||||
type="button"
|
||||
@ -124,6 +135,11 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
title="Sort by"
|
||||
type="button"
|
||||
@ -268,6 +284,12 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
<div
|
||||
className="makeStyles-actions-15"
|
||||
>
|
||||
<p
|
||||
className="MuiTypography-root MuiTypography-body2"
|
||||
data-loading={true}
|
||||
>
|
||||
Sorted by:
|
||||
</p>
|
||||
<button
|
||||
aria-controls="metric"
|
||||
aria-haspopup="true"
|
||||
@ -287,6 +309,11 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
title="Metric interval"
|
||||
type="button"
|
||||
@ -329,6 +356,11 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"textTransform": "lowercase",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
title="Sort by"
|
||||
type="button"
|
||||
|
@ -318,10 +318,6 @@ const FeatureView = ({
|
||||
<AddTagDialog
|
||||
featureToggleName={featureToggle.name}
|
||||
/>
|
||||
<StatusUpdateComponent
|
||||
stale={featureToggle.stale}
|
||||
updateStale={updateStale}
|
||||
/>
|
||||
<Button
|
||||
title="Create new feature toggle by cloning configuration"
|
||||
component={Link}
|
||||
@ -329,6 +325,10 @@ const FeatureView = ({
|
||||
>
|
||||
Clone
|
||||
</Button>
|
||||
<StatusUpdateComponent
|
||||
stale={featureToggle.stale}
|
||||
updateStale={updateStale}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!hasAccess(DELETE_FEATURE, project)}
|
||||
@ -368,6 +368,7 @@ const FeatureView = ({
|
||||
setDelDialog(false);
|
||||
removeToggle();
|
||||
}}
|
||||
onClose={() => setDelDialog(false)}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import InputPercentage from './input-percentage';
|
||||
import Select from '../../../common/select';
|
||||
import { TextField, Typography } from '@material-ui/core';
|
||||
import { Icon, TextField, Tooltip, Typography } from '@material-ui/core';
|
||||
|
||||
const builtInStickinessOptions = [
|
||||
{ key: 'default', label: 'default' },
|
||||
@ -21,7 +21,9 @@ const FlexibleStrategy = ({ updateParameter, parameters, context }) => {
|
||||
builtInStickinessOptions.concat(
|
||||
context
|
||||
.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 }))
|
||||
);
|
||||
|
||||
@ -33,13 +35,37 @@ const FlexibleStrategy = ({ updateParameter, parameters, context }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputPercentage name="Rollout" value={1 * rollout} onChange={onUpdate('rollout')} />
|
||||
<InputPercentage
|
||||
name="Rollout"
|
||||
value={1 * rollout}
|
||||
onChange={onUpdate('rollout')}
|
||||
/>
|
||||
<br />
|
||||
<div>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Stickiness
|
||||
</Typography>
|
||||
<br />
|
||||
<Tooltip
|
||||
placement="right-start"
|
||||
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."
|
||||
>
|
||||
<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
|
||||
name="stickiness"
|
||||
label="Stickiness"
|
||||
@ -48,6 +74,32 @@ const FlexibleStrategy = ({ updateParameter, parameters, context }) => {
|
||||
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
|
||||
label="groupId"
|
||||
size="small"
|
||||
|
@ -4,12 +4,22 @@ import { Tooltip, Icon, Typography } from '@material-ui/core';
|
||||
import StrategyConstraintInputField from '../StrategyConstraintInputField';
|
||||
import { useCommonStyles } from '../../../../../common.styles';
|
||||
|
||||
const StrategyConstraintInput = ({ constraints, updateConstraints, contextNames, contextFields, enabled }) => {
|
||||
const StrategyConstraintInput = ({
|
||||
constraints,
|
||||
updateConstraints,
|
||||
contextNames,
|
||||
contextFields,
|
||||
enabled,
|
||||
}) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
const addConstraint = evt => {
|
||||
evt.preventDefault();
|
||||
const updatedConstraints = [...constraints];
|
||||
updatedConstraints.push({ contextName: contextNames[0], operator: 'IN', values: [] });
|
||||
updatedConstraints.push({
|
||||
contextName: contextNames[0],
|
||||
operator: 'IN',
|
||||
values: [],
|
||||
});
|
||||
|
||||
updateConstraints(updatedConstraints);
|
||||
};
|
||||
@ -35,12 +45,22 @@ const StrategyConstraintInput = ({ constraints, updateConstraints, contextNames,
|
||||
|
||||
return (
|
||||
<div className={commonStyles.contentSpacingY}>
|
||||
<Typography variant="subtitle2">
|
||||
{'Constraints '}
|
||||
<Tooltip title={<span>Use context fields to constrain the activation strategy.</span>}>
|
||||
<Icon style={{ fontSize: '0.9em', color: 'gray' }}>info</Icon>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
<Tooltip
|
||||
placement="right-start"
|
||||
title={
|
||||
<span>
|
||||
Use context fields to constrain the activation strategy.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Typography variant="subtitle2">
|
||||
{'Constraints '}
|
||||
|
||||
<Icon style={{ fontSize: '0.9rem', color: 'gray' }}>
|
||||
info
|
||||
</Icon>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<table style={{ margin: 0 }}>
|
||||
<tbody>
|
||||
{constraints.map((c, index) => (
|
||||
@ -56,7 +76,11 @@ const StrategyConstraintInput = ({ constraints, updateConstraints, contextNames,
|
||||
</tbody>
|
||||
</table>
|
||||
<small>
|
||||
<a href="#add-constraint" title="Add constraint" onClick={addConstraint}>
|
||||
<a
|
||||
href="#add-constraint"
|
||||
title="Add constraint"
|
||||
onClick={addConstraint}
|
||||
>
|
||||
Add constraint
|
||||
</a>
|
||||
</small>
|
||||
|
@ -216,6 +216,32 @@ exports[`renders correctly with one feature 1`] = `
|
||||
<AddTagDialog
|
||||
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
|
||||
aria-controls="feature-stale-dropdown"
|
||||
aria-haspopup="true"
|
||||
@ -258,32 +284,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</span>
|
||||
</span>
|
||||
</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
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
disabled={false}
|
||||
|
@ -35,7 +35,7 @@ import AdminAuth from '../../page/admin/auth';
|
||||
import Reporting from '../../page/reporting';
|
||||
import Login from '../user/Login';
|
||||
import { P, C } from '../common/flags';
|
||||
import NewUser from '../user/NewUser/NewUser';
|
||||
import NewUser from '../user/NewUser';
|
||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
@ -15,6 +15,8 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
Button,
|
||||
useMediaQuery,
|
||||
} from '@material-ui/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ConfirmDialogue from '../../common/Dialogue';
|
||||
@ -24,6 +26,7 @@ import AccessContext from '../../../contexts/AccessContext';
|
||||
|
||||
const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||
const [project, setProject] = useState(undefined);
|
||||
const styles = useStyles();
|
||||
@ -35,14 +38,27 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_PROJECT)}
|
||||
show={
|
||||
<Tooltip title="Add new project">
|
||||
<IconButton
|
||||
aria-label="add-project"
|
||||
onClick={() => history.push('/projects/create')}
|
||||
>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={smallScreen}
|
||||
show={
|
||||
<Tooltip title="Add new project">
|
||||
<IconButton
|
||||
onClick={() => history.push('/projects/create')}
|
||||
>
|
||||
<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 { Typography, Button, List } from '@material-ui/core';
|
||||
import { Typography, Button, List, ListItem } from '@material-ui/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
@ -7,8 +7,9 @@ import PageContent from '../../common/PageContent';
|
||||
|
||||
import FeatureToggleListItem from '../../feature/FeatureToggleList/FeatureToggleListItem';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import ListPlaceholder from '../../common/ListPlaceholder/ListPlaceholder';
|
||||
|
||||
const ViewProject = ({
|
||||
const ProjectView = ({
|
||||
project,
|
||||
features,
|
||||
settings,
|
||||
@ -81,10 +82,21 @@ const ViewProject = ({
|
||||
<Typography variant="subtitle2">
|
||||
Feature toggles in this project
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewProject;
|
||||
export default ProjectView;
|
@ -3,7 +3,7 @@ import {
|
||||
fetchFeatureToggles,
|
||||
toggleFeature,
|
||||
} from '../../../store/feature-toggle/actions';
|
||||
import ViewProject from './ViewProject';
|
||||
import ViewProject from './ProjectView';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const projectBase = { id: '', name: '', description: '' };
|
@ -68,7 +68,7 @@ function AddUserComponent({ roles, addUserToRole }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container justify="center" spacing={3} alignItems="flex-end">
|
||||
<Grid container justify="left" spacing={3} alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Autocomplete
|
||||
id="add-user-component"
|
||||
@ -93,6 +93,8 @@ function AddUserComponent({ roles, addUserToRole }) {
|
||||
<TextField
|
||||
{...params}
|
||||
label="User"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
name="search"
|
||||
onChange={handleQueryUpdate}
|
||||
InputProps={{
|
||||
@ -119,8 +121,15 @@ function AddUserComponent({ roles, addUserToRole }) {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FormControl>
|
||||
<InputLabel id="add-user-select-role-label">
|
||||
<FormControl
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{ minWidth: '125px' }}
|
||||
>
|
||||
<InputLabel
|
||||
style={{ backgroundColor: '#fff' }}
|
||||
id="add-user-select-role-label"
|
||||
>
|
||||
Role
|
||||
</InputLabel>
|
||||
<Select
|
||||
|
@ -1,15 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
InputLabel,
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
@ -19,21 +18,28 @@ import {
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import AddUserComponent from './access-add-user';
|
||||
|
||||
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 }) {
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [error, setError] = useState();
|
||||
const history = useHistory();
|
||||
|
||||
const fetchAccess = async () => {
|
||||
const access = await projectApi.fetchAccess(projectId);
|
||||
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(() => {
|
||||
@ -85,8 +91,23 @@ function AccessComponent({ projectId, project }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ minHeight: '400px' }}>
|
||||
<CardHeader title={`Managed Access for project "${project.name}"`} />
|
||||
<PageContent
|
||||
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} />
|
||||
<Dialog
|
||||
open={!!error}
|
||||
@ -96,14 +117,29 @@ function AccessComponent({ projectId, project }) {
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">{error}</DialogContentText>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{error}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseError} color="secondary" autoFocus>
|
||||
<Button
|
||||
onClick={handleCloseError}
|
||||
color="secondary"
|
||||
autoFocus
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
width: '106.65%',
|
||||
marginLeft: '-2rem',
|
||||
backgroundColor: '#efefef',
|
||||
marginTop: '2rem',
|
||||
}}
|
||||
></div>
|
||||
<List>
|
||||
{users.map(user => {
|
||||
const labelId = `checkbox-list-secondary-label-${user.id}`;
|
||||
@ -112,25 +148,50 @@ function AccessComponent({ projectId, project }) {
|
||||
<ListItemAvatar>
|
||||
<Avatar alt={user.name} src={user.imageUrl} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText id={labelId} primary={user.name} secondary={user.email || user.username} />
|
||||
<ListItemSecondaryAction>
|
||||
<Select
|
||||
labelId={`role-${user.id}-select-label`}
|
||||
id={`role-${user.id}-select`}
|
||||
placeholder="Choose role"
|
||||
value={user.roleId}
|
||||
onChange={handleRoleChange(user.id, user.roleId)}
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
Choose role
|
||||
</MenuItem>
|
||||
{roles.map(role => (
|
||||
<MenuItem key={`${user.id}:${role.id}`} value={role.id}>
|
||||
{role.name}
|
||||
<ListItemText
|
||||
id={labelId}
|
||||
primary={user.name}
|
||||
secondary={user.email || user.username}
|
||||
/>
|
||||
<ListItemSecondaryAction
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<FormControl variant="outlined" size="small">
|
||||
<InputLabel
|
||||
style={{ backgroundColor: '#fff' }}
|
||||
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>
|
||||
))}
|
||||
</Select>
|
||||
{roles.map(role => (
|
||||
<MenuItem
|
||||
key={`${user.id}:${role.id}`}
|
||||
value={role.id}
|
||||
>
|
||||
{role.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
style={{ marginLeft: '0.5rem' }}
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
title="Remove access"
|
||||
@ -143,7 +204,7 @@ function AccessComponent({ projectId, project }) {
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Card>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -75,8 +75,15 @@ class ProjectFormComponent extends Component {
|
||||
};
|
||||
|
||||
onCancel = evt => {
|
||||
const { editMode } = this.props;
|
||||
const { project } = this.state;
|
||||
|
||||
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 => {
|
||||
@ -87,7 +94,7 @@ class ProjectFormComponent extends Component {
|
||||
|
||||
if (valid) {
|
||||
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';
|
||||
|
||||
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>}>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
@ -169,8 +179,6 @@ class ProjectFormComponent extends Component {
|
||||
this.setValue('description', v.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_PROJECT)}
|
||||
|
@ -56,12 +56,8 @@ class WrapperComponent extends Component {
|
||||
|
||||
onSubmit = async evt => {
|
||||
evt.preventDefault();
|
||||
const {
|
||||
createStrategy,
|
||||
updateStrategy,
|
||||
history,
|
||||
editMode,
|
||||
} = this.props;
|
||||
const { createStrategy, updateStrategy, history, editMode } =
|
||||
this.props;
|
||||
const { strategy } = this.state;
|
||||
|
||||
const parameters = (strategy.parameters || [])
|
||||
|
@ -60,16 +60,6 @@ exports[`it supports editMode 1`] = `
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-startIcon MuiButton-iconSizeMedium"
|
||||
>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="material-icons MuiIcon-root"
|
||||
>
|
||||
add
|
||||
</span>
|
||||
</span>
|
||||
Update
|
||||
</span>
|
||||
</button>
|
||||
@ -166,16 +156,6 @@ exports[`renders correctly for creating 1`] = `
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-startIcon MuiButton-iconSizeMedium"
|
||||
>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="material-icons MuiIcon-root"
|
||||
>
|
||||
add
|
||||
</span>
|
||||
</span>
|
||||
Create
|
||||
</span>
|
||||
</button>
|
||||
|
@ -2,13 +2,12 @@ import React, { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Grid, TextField, Typography } from '@material-ui/core';
|
||||
import LockRounded from '@material-ui/icons/LockRounded';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import { useStyles } from './HostedAuth.styles';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GoogleSvg } from './Icons';
|
||||
import useQueryParams from '../../../hooks/useQueryParams';
|
||||
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||
|
||||
const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
@ -64,7 +63,6 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const { usernameError, passwordError, apiError } = errors;
|
||||
const { options = [] } = authDetails;
|
||||
|
||||
@ -72,19 +70,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
<div>
|
||||
<br />
|
||||
<div>
|
||||
{options.map(o => (
|
||||
<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>
|
||||
))}
|
||||
<AuthOptions options={options} />
|
||||
</div>
|
||||
<p className={styles.fancyLine}>or</p>
|
||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||
@ -140,9 +126,12 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
</Grid>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -23,6 +23,9 @@ export const useStyles = makeStyles(theme => ({
|
||||
marginBottom: '0.5rem',
|
||||
fontSize: '1.1rem',
|
||||
},
|
||||
passwordHeader: {
|
||||
marginTop: '2rem',
|
||||
},
|
||||
emailField: {
|
||||
minWidth: '300px',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
|
@ -10,15 +10,16 @@ import useResetPassword from '../../../hooks/useResetPassword';
|
||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
||||
import { IAuthStatus } from '../../../interfaces/user';
|
||||
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||
|
||||
const NewUser = () => {
|
||||
const {
|
||||
token,
|
||||
data,
|
||||
loading,
|
||||
setLoading,
|
||||
invalidToken,
|
||||
} = useResetPassword();
|
||||
interface INewUserProps {
|
||||
user: IAuthStatus;
|
||||
}
|
||||
|
||||
const NewUser = ({ user }: INewUserProps) => {
|
||||
const { token, data, loading, setLoading, invalidToken } =
|
||||
useResetPassword();
|
||||
const ref = useLoading(loading);
|
||||
const commonStyles = useCommonStyles();
|
||||
const styles = useStyles();
|
||||
@ -57,7 +58,7 @@ const NewUser = () => {
|
||||
</Typography>
|
||||
<TextField
|
||||
data-loading
|
||||
value={data?.email}
|
||||
value={data?.email || ''}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
className={styles.emailField}
|
||||
@ -79,9 +80,46 @@ const NewUser = () => {
|
||||
className={commonStyles.largeDivider}
|
||||
data-loading
|
||||
/>
|
||||
<Typography variant="body1" data-loading>
|
||||
Set a password for your account.
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
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>
|
||||
</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: {
|
||||
position: 'fixed',
|
||||
position: 'absolute',
|
||||
bottom: '40px',
|
||||
display: 'flex',
|
||||
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"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
value={password || ''}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
autoComplete="password"
|
||||
data-loading
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
value={confirmPassword || ''}
|
||||
placeholder="Confirm password"
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
autoComplete="confirm-password"
|
||||
data-loading
|
||||
/>
|
||||
<PasswordMatcher
|
||||
|
@ -14,7 +14,13 @@ interface IAuthDetails {
|
||||
type: string;
|
||||
path: string;
|
||||
message: string;
|
||||
options: string[];
|
||||
options: IAuthOptions[];
|
||||
}
|
||||
|
||||
export interface IAuthOptions {
|
||||
type: string;
|
||||
message: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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 = {
|
||||
display: 'flex',
|
||||
@ -18,38 +18,48 @@ const activeNavLinkStyle = {
|
||||
padding: '0.8rem 1.5rem',
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
marginRight: '5px',
|
||||
};
|
||||
|
||||
function AdminMenu({history}) {
|
||||
function AdminMenu({ history }) {
|
||||
const { location } = history;
|
||||
const { pathname } = location;
|
||||
return (
|
||||
<Paper style={{ marginBottom: '1rem' }}>
|
||||
<Tabs centered value={pathname} >
|
||||
<Tab value="/admin/users" label={
|
||||
<NavLink to="/admin/users" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
|
||||
<Icon style={iconStyle}>supervised_user_circle</Icon>
|
||||
<Tabs centered value={pathname}>
|
||||
<Tab
|
||||
value="/admin/users"
|
||||
label={
|
||||
<NavLink
|
||||
to="/admin/users"
|
||||
activeStyle={activeNavLinkStyle}
|
||||
style={navLinkStyle}
|
||||
>
|
||||
<span>Users</span>
|
||||
</NavLink>
|
||||
</NavLink>
|
||||
}
|
||||
>
|
||||
</Tab>
|
||||
<Tab value="/admin/api" label={
|
||||
<NavLink to="/admin/api" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
|
||||
<Icon style={iconStyle}>apps</Icon>
|
||||
API Access
|
||||
</NavLink>
|
||||
}>
|
||||
</Tab>
|
||||
<Tab value="/admin/auth" label={
|
||||
<NavLink to="/admin/auth" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
|
||||
<Icon style={iconStyle}>lock</Icon>
|
||||
Authentication
|
||||
</NavLink>
|
||||
}>
|
||||
</Tab>
|
||||
></Tab>
|
||||
<Tab
|
||||
value="/admin/api"
|
||||
label={
|
||||
<NavLink
|
||||
to="/admin/api"
|
||||
activeStyle={activeNavLinkStyle}
|
||||
style={navLinkStyle}
|
||||
>
|
||||
API Access
|
||||
</NavLink>
|
||||
}
|
||||
></Tab>
|
||||
<Tab
|
||||
value="/admin/auth"
|
||||
label={
|
||||
<NavLink
|
||||
to="/admin/auth"
|
||||
activeStyle={activeNavLinkStyle}
|
||||
style={navLinkStyle}
|
||||
>
|
||||
Authentication
|
||||
</NavLink>
|
||||
}
|
||||
></Tab>
|
||||
</Tabs>
|
||||
</Paper>
|
||||
);
|
||||
|
@ -13,10 +13,9 @@ import classnames from 'classnames';
|
||||
import { styles as commonStyles } from '../../../component/common';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
function CreateApiKey({ addKey }) {
|
||||
function CreateApiKey({ addKey, show, setShow }) {
|
||||
const styles = useStyles();
|
||||
const [type, setType] = useState('CLIENT');
|
||||
const [show, setShow] = useState(false);
|
||||
const [username, setUsername] = useState();
|
||||
const [error, setError] = useState();
|
||||
|
||||
@ -107,6 +106,9 @@ function CreateApiKey({ addKey }) {
|
||||
|
||||
CreateApiKey.propTypes = {
|
||||
addKey: PropTypes.func.isRequired,
|
||||
setShow: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CreateApiKey;
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
DELETE_API_TOKEN,
|
||||
CREATE_API_TOKEN,
|
||||
} from '../../../component/AccessProvider/permissions';
|
||||
import PageContent from '../../../component/common/PageContent';
|
||||
import HeaderTitle from '../../../component/common/HeaderTitle';
|
||||
|
||||
function ApiKeyList({
|
||||
location,
|
||||
@ -30,6 +32,8 @@ function ApiKeyList({
|
||||
keys,
|
||||
unleashUrl,
|
||||
}) {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [delKey, setDelKey] = useState(undefined);
|
||||
@ -45,93 +49,111 @@ function ApiKeyList({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title="API Access"
|
||||
actions={
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_API_TOKEN)}
|
||||
show={
|
||||
<CreateApiKey
|
||||
addKey={addKey}
|
||||
setShow={setShow}
|
||||
show={show}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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 />
|
||||
<strong>API URL: </strong>{' '}
|
||||
<pre style={{ display: 'inline' }}>{unleashUrl}/api/</pre>
|
||||
</Alert>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<br />
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell>Username</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>
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell>Username</TableCell>
|
||||
<TableCell>Access Type</TableCell>
|
||||
<TableCell>Secret</TableCell>
|
||||
<TableCell>Action</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>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(CREATE_API_TOKEN)}
|
||||
show={<CreateApiKey addKey={addKey} />}
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
))}
|
||||
</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 ApiKeyList from './api-key-list-container';
|
||||
|
||||
import AdminMenu from '../admin-menu';
|
||||
import PageContent from '../../../component/common/PageContent/PageContent';
|
||||
|
||||
const render = ({history}) => (
|
||||
<div>
|
||||
<AdminMenu history={history} />
|
||||
<PageContent headerContent="API Access">
|
||||
const render = ({ history }) => {
|
||||
return (
|
||||
<div>
|
||||
<AdminMenu history={history} />
|
||||
<ApiKeyList />
|
||||
</PageContent>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
|
@ -40,7 +40,7 @@ const ConfirmUserLink = ({
|
||||
Want to avoid this step in the future?{' '}
|
||||
{/* TODO - ADD LINK HERE ONCE IT EXISTS*/}
|
||||
<a
|
||||
href="https://docs.getunleash.ai/"
|
||||
href="https://docs.getunleash.io/docs/deploy/email"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
@ -2,7 +2,6 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@ -23,7 +22,7 @@ import UserListItem from './UserListItem/UserListItem';
|
||||
import loadingData from './loadingData';
|
||||
import useLoading from '../../../../hooks/useLoading';
|
||||
|
||||
function UsersList({ location }) {
|
||||
function UsersList({ location, closeDialog, showDialog }) {
|
||||
const { users, roles, refetch, loading } = useUsers();
|
||||
const {
|
||||
addUser,
|
||||
@ -35,7 +34,6 @@ function UsersList({ location }) {
|
||||
userApiErrors,
|
||||
} = useAdminUsersApi();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [showDialog, setDialog] = useState(false);
|
||||
const [pwDialog, setPwDialog] = useState({ open: false });
|
||||
const [delDialog, setDelDialog] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
@ -45,15 +43,6 @@ function UsersList({ location }) {
|
||||
const [updateDialog, setUpdateDialog] = useState({ open: false });
|
||||
const ref = useLoading(loading);
|
||||
|
||||
const openDialog = e => {
|
||||
e.preventDefault();
|
||||
setDialog(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialog(false);
|
||||
};
|
||||
|
||||
const closeDelDialog = () => {
|
||||
setDelDialog(false);
|
||||
setDelUser(undefined);
|
||||
@ -177,19 +166,6 @@ function UsersList({ location }) {
|
||||
<TableBody>{renderUsers()}</TableBody>
|
||||
</Table>
|
||||
<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
|
||||
open={showConfirm}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import UsersList from './UsersList';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
location: state.settings.toJS().location || {},
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
location: state.settings.toJS().location || {},
|
||||
};
|
||||
};
|
||||
|
||||
const Container = connect(mapStateToProps)(UsersList);
|
||||
|
||||
|
@ -78,7 +78,7 @@ const DelUserComponent = ({
|
||||
DelUserComponent.propTypes = {
|
||||
showDialog: propTypes.bool.isRequired,
|
||||
closeDialog: propTypes.func.isRequired,
|
||||
user: propTypes.object.isRequired,
|
||||
user: propTypes.object,
|
||||
removeUser: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import UsersList from './UsersList';
|
||||
import AdminMenu from '../admin-menu';
|
||||
@ -7,17 +7,60 @@ import AccessContext from '../../../contexts/AccessContext';
|
||||
import ConditionallyRender from '../../../component/common/ConditionallyRender';
|
||||
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import HeaderTitle from '../../../component/common/HeaderTitle';
|
||||
import { Button } from '@material-ui/core';
|
||||
|
||||
const UsersAdmin = ({ history }) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [showDialog, setDialog] = useState(false);
|
||||
|
||||
const openDialog = e => {
|
||||
e.preventDefault();
|
||||
setDialog(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<UsersList />}
|
||||
show={
|
||||
<UsersList
|
||||
openDialog={openDialog}
|
||||
closeDialog={closeDialog}
|
||||
showDialog={showDialog}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<Alert severity="error">
|
||||
You need instance admin to access this section.
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ViewProject from '../../component/project/ViewProject';
|
||||
import ViewProject from '../../component/project/ProjectView';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const render = ({ match: { params }, history }) => (
|
||||
|
Loading…
Reference in New Issue
Block a user