mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-23 13:46:45 +02:00
feat/rbac roles (#562)
* feat: create screen * fix: import accordion summary * feat: add accordions * fix: add codebox * feat: select permissions * fix: permission checker * fix: update permission checker * feat: wire up role list * fix: change icon color in project roles list * fix: add color to icon in project roles * add confirm dialog on role deletion * feat: add created screen * fix: cleanup * fix: update access permissions * fix: update admin panel * feat: add edit screen * fix: use color from palette and show toast when fails * fix: refactor * feat: validation * feat: implement checked all * fix: experimental toast * fix: error handling * fix: toast * feat: unique name validation * fix: update toasts * fix: remove toast * fix: reset flag * fix: remove unused vars * fix: update tests * feat: add error icon for toast * fix: replace wrong import for setToastData * feat: Patch keying on ui to handle uniqueness for permissions across multiple envs * fix: hasAccess handles * * fix: update permission switch * fix: use flag for environments rbac * fix: do not include check all keys in payload * fix: filter roles * fix: account for new permissions in variants list * fix: use effect on length property * fix: set polling interval on user * 4.5.0-beta.0 * fix: set initial permissions correctly to avoid race condition * fix: handle activeEnvironment when it is null * fix: remove unused imports * fix: unused imports * fix: Include missing project in hasAccess for deleteinng a tag * fix: Move add/delete tag to use update feature permissions * fix: use rest parameter * fix: remove sandbox from scripts * 4.6.0-beta.1 * fix: remove loading deduping * fix: disable editing on builtin roles * fix: check all * fix: feature overview environment * fix: refetch user on project create * fix: update snaphots * fix: frontend permissions * fix: delete create confirm * fix: remove unused permission * 4.6.0-beta.4 * fix: update permissions * fix: permissions * fix: set error to string * 4.6.0-beta.5 * fix: add permissions for project view * fix: add permissions to useEffect deps * fix: update permission for move feature toggle * fix: add permissions data to useEffect * fix: move settings * fix: key on confetti * fix: refetch project permissions on environment create/delete * fix: optional coalescing error object * fix: remove logging error * fix: reorder disable importance in permissionbutton * fix: add project roles to menu * fix: add disabled check to revive * fix: update snapshots * fix: change text to select all * fix: change text to select * 4.6.0-beta.6 Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com> Co-authored-by: sighphyre <liquidwicked64@gmail.com>
This commit is contained in:
parent
8b538e4ded
commit
182d566895
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unleash-frontend",
|
||||
"description": "unleash your features",
|
||||
"version": "4.4.1",
|
||||
"version": "4.6.0-beta.6",
|
||||
"keywords": [
|
||||
"unleash",
|
||||
"feature toggle",
|
||||
|
@ -65,6 +65,19 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
fadeInBottomStartNoPosition: {
|
||||
transform: 'translateY(400px)',
|
||||
opacity: '0',
|
||||
boxShadow: `rgb(129 129 129 / 40%) 4px 5px 11px 4px`,
|
||||
zIndex: 500,
|
||||
width: '100%',
|
||||
backgroundColor: '#fff',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: '300px',
|
||||
position: 'fixed',
|
||||
},
|
||||
fadeInBottomStart: {
|
||||
opacity: '0',
|
||||
position: 'fixed',
|
||||
|
@ -13,12 +13,12 @@ import IAuthStatus from '../interfaces/user';
|
||||
import { useState, useEffect } from 'react';
|
||||
import NotFound from './common/NotFound/NotFound';
|
||||
import Feedback from './common/Feedback';
|
||||
import useToast from '../hooks/useToast';
|
||||
import SWRProvider from './providers/SWRProvider/SWRProvider';
|
||||
import ConditionallyRender from './common/ConditionallyRender';
|
||||
import EnvironmentSplash from './common/EnvironmentSplash/EnvironmentSplash';
|
||||
import Loader from './common/Loader/Loader';
|
||||
import useUser from '../hooks/api/getters/useUser/useUser';
|
||||
import ToastRenderer from './common/ToastRenderer/ToastRenderer';
|
||||
|
||||
interface IAppProps extends RouteComponentProps {
|
||||
user: IAuthStatus;
|
||||
@ -26,9 +26,9 @@ interface IAppProps extends RouteComponentProps {
|
||||
feedback: any;
|
||||
}
|
||||
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
const { toast, setToastData } = useToast();
|
||||
// because we need the userId when the component load.
|
||||
const { splash, user: userFromUseUser, authDetails } = useUser();
|
||||
|
||||
const [showSplash, setShowSplash] = useState(false);
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
useEffect(() => {
|
||||
@ -101,7 +101,6 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
|
||||
return (
|
||||
<SWRProvider
|
||||
setToastData={setToastData}
|
||||
isUnauthorized={isUnauthorized}
|
||||
setShowLoader={setShowLoader}
|
||||
>
|
||||
@ -141,7 +140,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{toast}
|
||||
<ToastRenderer />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -1,63 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import usePagination from '../../../hooks/usePagination';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import PaginateUI from '../../common/PaginateUI/PaginateUI';
|
||||
import RoleListItem from './RolesListItem/RoleListItem';
|
||||
import useProjectRoles from '../../../hooks/api/getters/useProjectRoles/useProjectRoles';
|
||||
|
||||
const RolesList = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { roles } = useProjectRoles();
|
||||
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
||||
usePagination(roles, 10);
|
||||
|
||||
const renderRoles = () => {
|
||||
return page.map(role => {
|
||||
return (
|
||||
<RoleListItem
|
||||
key={role.id}
|
||||
name={role.name}
|
||||
description={role.description}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (!roles) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell>Project Role</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell align="right">
|
||||
{hasAccess(ADMIN) ? 'Action' : ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{renderRoles()}</TableBody>
|
||||
<PaginateUI
|
||||
pages={pages}
|
||||
pageIndex={pageIndex}
|
||||
setPageIndex={setPageIndex}
|
||||
nextPage={nextPage}
|
||||
prevPage={prevPage}
|
||||
/>
|
||||
</Table>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RolesList;
|
@ -1,61 +0,0 @@
|
||||
import { useStyles } from './RoleListItem.styles';
|
||||
import { TableRow, TableCell, Typography } from '@material-ui/core';
|
||||
import { Edit, Delete } from '@material-ui/icons';
|
||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
||||
import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton';
|
||||
|
||||
interface IRoleListItemProps {
|
||||
key: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const RoleListItem = ({ key, name, description }: IRoleListItemProps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<TableRow key={key} className={styles.tableRow}>
|
||||
<TableCell>
|
||||
<SupervisedUserCircleIcon />
|
||||
</TableCell>
|
||||
<TableCell className={styles.leftTableCell}>
|
||||
<Typography variant="body2" data-loading>
|
||||
{name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell className={styles.leftTableCell}>
|
||||
<Typography variant="body2" data-loading>
|
||||
{description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="right">
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
aria-label="Edit"
|
||||
tooltip="Edit"
|
||||
onClick={() => {
|
||||
console.log('hi');
|
||||
}}
|
||||
permission={ADMIN}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
aria-label="Remove user"
|
||||
tooltip="Remove role"
|
||||
onClick={() => {
|
||||
console.log('hi');
|
||||
}}
|
||||
permission={ADMIN}
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleListItem;
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Paper, Tabs, Tab } from '@material-ui/core';
|
||||
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
const navLinkStyle = {
|
||||
display: 'flex',
|
||||
@ -19,12 +20,19 @@ const activeNavLinkStyle = {
|
||||
};
|
||||
|
||||
function AdminMenu({ history }) {
|
||||
const SHOW_PROJECT_ROLES = false;
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { flags } = uiConfig;
|
||||
|
||||
const { location } = history;
|
||||
const { pathname } = location;
|
||||
return (
|
||||
<Paper style={{ marginBottom: '1rem' }}>
|
||||
<Paper
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
borderRadius: '12.5px',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<Tabs centered value={pathname}>
|
||||
<Tab
|
||||
value="/admin/users"
|
||||
@ -38,7 +46,7 @@ function AdminMenu({ history }) {
|
||||
</NavLink>
|
||||
}
|
||||
></Tab>
|
||||
{SHOW_PROJECT_ROLES && (
|
||||
{flags.RE && (
|
||||
<Tab
|
||||
value="/admin/roles"
|
||||
label={
|
||||
|
@ -54,7 +54,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [delToken, setDeleteToken] = useState<IApiToken>();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { tokens, loading, refetch, error } = useApiTokens();
|
||||
const { deleteToken, createToken } = useApiTokensApi();
|
||||
const ref = useLoading(loading);
|
||||
@ -80,19 +80,25 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
|
||||
};
|
||||
|
||||
const onCreateToken = async (token: IApiTokenCreate) => {
|
||||
await createToken(token);
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
text: 'Successfully created API token.',
|
||||
});
|
||||
try {
|
||||
await createToken(token);
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Created token',
|
||||
text: 'Successfully created API token',
|
||||
confetti: true,
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
const copyToken = (value: string) => {
|
||||
if (copy(value)) {
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
title: 'Token copied',
|
||||
confetti: true,
|
||||
text: `Token is copied to clipboard`,
|
||||
});
|
||||
}
|
||||
@ -161,7 +167,10 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
|
||||
<TableBody>
|
||||
{tokens.map(item => {
|
||||
return (
|
||||
<TableRow key={item.secret} className={styles.tableRow}>
|
||||
<TableRow
|
||||
key={item.secret}
|
||||
className={styles.tableRow}
|
||||
>
|
||||
<TableCell
|
||||
align="left"
|
||||
className={styles.hideSM}
|
||||
@ -302,7 +311,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
|
||||
elseShow={renderApiTokens(tokens)}
|
||||
/>
|
||||
</div>
|
||||
{toast}
|
||||
|
||||
<ApiTokenCreate
|
||||
showDialog={showDialog}
|
||||
createToken={onCreateToken}
|
||||
|
@ -0,0 +1,101 @@
|
||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
||||
import useProjectRolesApi from '../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
|
||||
import useProjectRoleForm from '../hooks/useProjectRoleForm';
|
||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
|
||||
const CreateProjectRole = () => {
|
||||
/* @ts-ignore */
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const history = useHistory();
|
||||
const {
|
||||
roleName,
|
||||
roleDesc,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
checkedPermissions,
|
||||
handlePermissionChange,
|
||||
checkAllProjectPermissions,
|
||||
checkAllEnvironmentPermissions,
|
||||
getProjectRolePayload,
|
||||
validatePermissions,
|
||||
validateName,
|
||||
validateNameUniqueness,
|
||||
errors,
|
||||
clearErrors,
|
||||
getRoleKey,
|
||||
} = useProjectRoleForm();
|
||||
|
||||
const { createRole, loading } = useProjectRolesApi();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
const validName = validateName();
|
||||
const validPermissions = validatePermissions();
|
||||
|
||||
if (validName && validPermissions) {
|
||||
const payload = getProjectRolePayload();
|
||||
try {
|
||||
await createRole(payload);
|
||||
history.push('/admin/roles');
|
||||
setToastData({
|
||||
title: 'Project role created',
|
||||
text: 'Now you can start assigning your project roles to project members.',
|
||||
confetti: true,
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request POST '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/roles' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push('/admin/roles');
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Create project role"
|
||||
description="A project role can be
|
||||
customised to limit access
|
||||
to resources within a project"
|
||||
documentationLink="https://docs.getunleash.io/"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<ProjectRoleForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
roleName={roleName}
|
||||
setRoleName={setRoleName}
|
||||
roleDesc={roleDesc}
|
||||
setRoleDesc={setRoleDesc}
|
||||
checkedPermissions={checkedPermissions}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
checkAllProjectPermissions={checkAllProjectPermissions}
|
||||
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions}
|
||||
submitButtonText="Create"
|
||||
clearErrors={clearErrors}
|
||||
validateNameUniqueness={validateNameUniqueness}
|
||||
getRoleKey={getRoleKey}
|
||||
/>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateProjectRole;
|
@ -0,0 +1,128 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
||||
|
||||
import useProjectRolesApi from '../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
|
||||
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
|
||||
import useProjectRoleForm from '../hooks/useProjectRoleForm';
|
||||
import useProjectRole from '../../../../hooks/api/getters/useProjectRole/useProjectRole';
|
||||
import { IPermission } from '../../../../interfaces/project';
|
||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
|
||||
const EditProjectRole = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const { id } = useParams();
|
||||
const { role } = useProjectRole(id);
|
||||
|
||||
const history = useHistory();
|
||||
const {
|
||||
roleName,
|
||||
roleDesc,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
checkedPermissions,
|
||||
handlePermissionChange,
|
||||
checkAllProjectPermissions,
|
||||
checkAllEnvironmentPermissions,
|
||||
getProjectRolePayload,
|
||||
validatePermissions,
|
||||
validateName,
|
||||
errors,
|
||||
clearErrors,
|
||||
getRoleKey,
|
||||
handleInitialCheckedPermissions,
|
||||
permissions,
|
||||
} = useProjectRoleForm(role.name, role.description);
|
||||
|
||||
useEffect(() => {
|
||||
const initialCheckedPermissions = role?.permissions?.reduce(
|
||||
(acc: { [key: string]: IPermission }, curr: IPermission) => {
|
||||
acc[getRoleKey(curr)] = curr;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
handleInitialCheckedPermissions(initialCheckedPermissions || {});
|
||||
/* eslint-disable-next-line */
|
||||
}, [
|
||||
role?.permissions?.length,
|
||||
permissions?.project?.length,
|
||||
permissions?.environments?.length,
|
||||
]);
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/roles/${role.id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const { refetch } = useProjectRole(id);
|
||||
const { editRole, loading } = useProjectRolesApi();
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
const payload = getProjectRolePayload();
|
||||
|
||||
const validName = validateName();
|
||||
const validPermissions = validatePermissions();
|
||||
|
||||
if (validName && validPermissions) {
|
||||
try {
|
||||
await editRole(id, payload);
|
||||
refetch();
|
||||
history.push('/admin/roles');
|
||||
setToastData({
|
||||
title: 'Project role updated',
|
||||
text: 'Your role changes will automatically be applied to the users with this role.',
|
||||
confetti: true,
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push('/admin/roles');
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Edit project role"
|
||||
description="A project role can be
|
||||
customised to limit access
|
||||
to resources within a project"
|
||||
documentationLink="https://docs.getunleash.io/"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<ProjectRoleForm
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
roleName={roleName}
|
||||
setRoleName={setRoleName}
|
||||
roleDesc={roleDesc}
|
||||
setRoleDesc={setRoleDesc}
|
||||
checkedPermissions={checkedPermissions}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
checkAllProjectPermissions={checkAllProjectPermissions}
|
||||
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions}
|
||||
submitButtonText="Edit"
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
getRoleKey={getRoleKey}
|
||||
/>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProjectRole;
|
@ -0,0 +1,35 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
environmentPermissionContainer: {
|
||||
marginBottom: '1.25rem',
|
||||
},
|
||||
accordionSummary: {
|
||||
boxShadow: 'none',
|
||||
padding: '0',
|
||||
},
|
||||
label: {
|
||||
minWidth: '300px',
|
||||
[theme.breakpoints.down(600)]: {
|
||||
minWidth: 'auto',
|
||||
},
|
||||
},
|
||||
accordionHeader: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(500)]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
},
|
||||
accordionBody: {
|
||||
padding: '0',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
header: {
|
||||
color: theme.palette.primary.light,
|
||||
},
|
||||
icon: {
|
||||
fill: theme.palette.primary.light,
|
||||
},
|
||||
}));
|
@ -0,0 +1,147 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
import { ExpandMore } from '@material-ui/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
IPermission,
|
||||
IProjectEnvironmentPermissions,
|
||||
} from '../../../../../interfaces/project';
|
||||
import StringTruncator from '../../../../common/StringTruncator/StringTruncator';
|
||||
import { ICheckedPermission } from '../../hooks/useProjectRoleForm';
|
||||
import { useStyles } from './EnvironmentPermissionAccordion.styles';
|
||||
|
||||
type PermissionMap = { [key: string]: boolean };
|
||||
|
||||
interface IEnvironmentPermissionAccordionProps {
|
||||
environment: IProjectEnvironmentPermissions;
|
||||
handlePermissionChange: (permission: IPermission, type: string) => void;
|
||||
checkAllEnvironmentPermissions: (envName: string) => void;
|
||||
checkedPermissions: ICheckedPermission;
|
||||
getRoleKey: (permission: { id: number; environment?: string }) => string;
|
||||
}
|
||||
|
||||
const EnvironmentPermissionAccordion = ({
|
||||
environment,
|
||||
handlePermissionChange,
|
||||
checkAllEnvironmentPermissions,
|
||||
checkedPermissions,
|
||||
getRoleKey,
|
||||
}: IEnvironmentPermissionAccordionProps) => {
|
||||
const [permissionMap, setPermissionMap] = useState<PermissionMap>({});
|
||||
const [permissionCount, setPermissionCount] = useState(0);
|
||||
const styles = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
const permissionMap = environment?.permissions?.reduce(
|
||||
(acc: PermissionMap, curr: IPermission) => {
|
||||
acc[getRoleKey(curr)] = true;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
setPermissionMap(permissionMap);
|
||||
/* eslint-disable-next-line */
|
||||
}, [environment?.permissions?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
let count = 0;
|
||||
Object.keys(checkedPermissions).forEach(key => {
|
||||
if (permissionMap[key]) {
|
||||
count = count + 1;
|
||||
}
|
||||
});
|
||||
|
||||
setPermissionCount(count);
|
||||
/* eslint-disable-next-line */
|
||||
}, [checkedPermissions]);
|
||||
|
||||
const renderPermissions = () => {
|
||||
const envPermissions = environment?.permissions?.map(
|
||||
(permission: IPermission) => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
classes={{ root: styles.label }}
|
||||
key={getRoleKey(permission)}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
checkedPermissions[getRoleKey(permission)]
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onChange={() =>
|
||||
handlePermissionChange(
|
||||
permission,
|
||||
environment.name
|
||||
)
|
||||
}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={permission.displayName || 'Dummy permission'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
envPermissions.push(
|
||||
<FormControlLabel
|
||||
key={`check-all-environment-${environment?.name}`}
|
||||
classes={{ root: styles.label }}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
checkedPermissions[
|
||||
`check-all-environment-${environment?.name}`
|
||||
]
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onChange={() =>
|
||||
checkAllEnvironmentPermissions(environment?.name)
|
||||
}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={'Select all permissions for this env'}
|
||||
/>
|
||||
);
|
||||
|
||||
return envPermissions;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.environmentPermissionContainer}>
|
||||
<Accordion style={{ boxShadow: 'none' }}>
|
||||
<AccordionSummary
|
||||
className={styles.accordionSummary}
|
||||
expandIcon={<ExpandMore className={styles.icon} />}
|
||||
>
|
||||
<div className={styles.accordionHeader}>
|
||||
<StringTruncator
|
||||
text={environment.name}
|
||||
className={styles.header}
|
||||
maxWidth="120"
|
||||
/>
|
||||
|
||||
<p className={styles.header}>
|
||||
({permissionCount} /{' '}
|
||||
{environment?.permissions?.length} permissions)
|
||||
</p>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={styles.accordionBody}>
|
||||
{renderPermissions()}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentPermissionAccordion;
|
@ -0,0 +1,41 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
maxWidth: '400px',
|
||||
},
|
||||
input: { width: '100%', marginBottom: '1rem' },
|
||||
label: {
|
||||
minWidth: '300px',
|
||||
[theme.breakpoints.down(600)]: {
|
||||
minWidth: 'auto',
|
||||
},
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
cancelButton: {
|
||||
marginRight: '1.5rem',
|
||||
},
|
||||
inputDescription: {
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
formHeader: {
|
||||
fontWeight: 'normal',
|
||||
marginTop: '0',
|
||||
},
|
||||
header: {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
permissionErrorContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
errorMessage: {
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.error.main,
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
},
|
||||
}));
|
@ -0,0 +1,190 @@
|
||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||
import Input from '../../../common/Input/Input';
|
||||
import EnvironmentPermissionAccordion from './EnvironmentPermissionAccordion/EnvironmentPermissionAccordion';
|
||||
import {
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
TextField,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import useProjectRolePermissions from '../../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
|
||||
import { useStyles } from './ProjectRoleForm.styles';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import React from 'react';
|
||||
import { IPermission } from '../../../../interfaces/project';
|
||||
import {
|
||||
ICheckedPermission,
|
||||
PROJECT_CHECK_ALL_KEY,
|
||||
} from '../hooks/useProjectRoleForm';
|
||||
|
||||
interface IProjectRoleForm {
|
||||
roleName: string;
|
||||
roleDesc: string;
|
||||
setRoleName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setRoleDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||
checkedPermissions: ICheckedPermission;
|
||||
handlePermissionChange: (permission: IPermission, type: string) => void;
|
||||
checkAllProjectPermissions: () => void;
|
||||
checkAllEnvironmentPermissions: (envName: string) => void;
|
||||
handleSubmit: (e: any) => void;
|
||||
handleCancel: () => void;
|
||||
errors: { [key: string]: string };
|
||||
submitButtonText: string;
|
||||
clearErrors: () => void;
|
||||
getRoleKey: (permission: { id: number; environment?: string }) => string;
|
||||
}
|
||||
|
||||
const ProjectRoleForm = ({
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
roleName,
|
||||
roleDesc,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
checkedPermissions,
|
||||
handlePermissionChange,
|
||||
checkAllProjectPermissions,
|
||||
checkAllEnvironmentPermissions,
|
||||
errors,
|
||||
submitButtonText,
|
||||
validateNameUniqueness,
|
||||
clearErrors,
|
||||
getRoleKey,
|
||||
}: IProjectRoleForm) => {
|
||||
const styles = useStyles();
|
||||
const { permissions } = useProjectRolePermissions({
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const { project, environments } = permissions;
|
||||
|
||||
const renderProjectPermissions = () => {
|
||||
const projectPermissions = project.map(permission => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={getRoleKey(permission)}
|
||||
classes={{ root: styles.label }}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
checkedPermissions[getRoleKey(permission)]
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onChange={() =>
|
||||
handlePermissionChange(permission, 'project')
|
||||
}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={permission.displayName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
projectPermissions.push(
|
||||
<FormControlLabel
|
||||
key={PROJECT_CHECK_ALL_KEY}
|
||||
classes={{ root: styles.label }}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
checkedPermissions[PROJECT_CHECK_ALL_KEY]
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onChange={() => checkAllProjectPermissions()}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={'Select all project permissions'}
|
||||
/>
|
||||
);
|
||||
|
||||
return projectPermissions;
|
||||
};
|
||||
|
||||
const renderEnvironmentPermissions = () => {
|
||||
return environments.map(environment => {
|
||||
return (
|
||||
<EnvironmentPermissionAccordion
|
||||
environment={environment}
|
||||
key={environment.name}
|
||||
checkedPermissions={checkedPermissions}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
checkAllEnvironmentPermissions={
|
||||
checkAllEnvironmentPermissions
|
||||
}
|
||||
getRoleKey={getRoleKey}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h3 className={styles.formHeader}>Role information</h3>
|
||||
|
||||
<div className={styles.container}>
|
||||
<p className={styles.inputDescription}>
|
||||
What is your role name?
|
||||
</p>
|
||||
<Input
|
||||
className={styles.input}
|
||||
label="Role name"
|
||||
value={roleName}
|
||||
onChange={e => setRoleName(e.target.value)}
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => clearErrors()}
|
||||
onBlur={validateNameUniqueness}
|
||||
/>
|
||||
|
||||
<p className={styles.inputDescription}>
|
||||
What is this role for?
|
||||
</p>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
label="Role description"
|
||||
variant="outlined"
|
||||
multiline
|
||||
maxRows={4}
|
||||
value={roleDesc}
|
||||
onChange={e => setRoleDesc(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.permissionErrorContainer}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(errors.permissions)}
|
||||
show={
|
||||
<span className={styles.errorMessage}>
|
||||
You must select at least one permission for a role.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<h3 className={styles.header}>Project permissions</h3>
|
||||
<div>{renderProjectPermissions()}</div>
|
||||
<h3 className={styles.header}>Environment permissions</h3>
|
||||
<div>{renderEnvironmentPermissions()}</div>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button onClick={handleCancel} className={styles.cancelButton}>
|
||||
Cancel
|
||||
</Button>
|
||||
<PermissionButton
|
||||
onClick={handleSubmit}
|
||||
permission={ADMIN}
|
||||
type="submit"
|
||||
>
|
||||
{submitButtonText} role
|
||||
</PermissionButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoleForm;
|
@ -0,0 +1,10 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
deleteParagraph: {
|
||||
marginTop: '2rem',
|
||||
},
|
||||
roleDeleteInput: {
|
||||
marginTop: '1rem',
|
||||
},
|
||||
}));
|
@ -0,0 +1,70 @@
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { IProjectRole } from '../../../../../interfaces/role';
|
||||
import Dialogue from '../../../../common/Dialogue';
|
||||
import Input from '../../../../common/Input/Input';
|
||||
import { useStyles } from './ProjectRoleDeleteConfirm.styles';
|
||||
|
||||
interface IProjectRoleDeleteConfirmProps {
|
||||
role: IProjectRole;
|
||||
open: boolean;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteRole: (id: number) => Promise<void>;
|
||||
confirmName: string;
|
||||
setConfirmName: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const ProjectRoleDeleteConfirm = ({
|
||||
role,
|
||||
open,
|
||||
setDeldialogue,
|
||||
handleDeleteRole,
|
||||
confirmName,
|
||||
setConfirmName,
|
||||
}: IProjectRoleDeleteConfirmProps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setConfirmName(e.currentTarget.value);
|
||||
|
||||
const handleCancel = () => {
|
||||
setDeldialogue(false);
|
||||
setConfirmName('');
|
||||
};
|
||||
const formId = 'delete-project-role-confirmation-form';
|
||||
return (
|
||||
<Dialogue
|
||||
title="Are you sure you want to delete this role?"
|
||||
open={open}
|
||||
primaryButtonText="Delete project role"
|
||||
secondaryButtonText="Cancel"
|
||||
onClick={() => handleDeleteRole(role.id)}
|
||||
disabledPrimaryButton={role?.name !== confirmName}
|
||||
onClose={handleCancel}
|
||||
formId={formId}
|
||||
>
|
||||
<Alert severity="error">
|
||||
Danger. Deleting this role will result in removing all
|
||||
permissions that are active in this environment across all
|
||||
feature toggles.
|
||||
</Alert>
|
||||
|
||||
<p className={styles.deleteParagraph}>
|
||||
In order to delete this role, please enter the name of the role
|
||||
in the textfield below: <strong>{role?.name}</strong>
|
||||
</p>
|
||||
|
||||
<form id={formId}>
|
||||
<Input
|
||||
autoFocus
|
||||
onChange={handleChange}
|
||||
value={confirmName}
|
||||
label="Role name"
|
||||
className={styles.roleDeleteInput}
|
||||
/>
|
||||
</form>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoleDeleteConfirm;
|
@ -0,0 +1,106 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import AccessContext from '../../../../../contexts/AccessContext';
|
||||
import usePagination from '../../../../../hooks/usePagination';
|
||||
import { ADMIN } from '../../../../providers/AccessProvider/permissions';
|
||||
import PaginateUI from '../../../../common/PaginateUI/PaginateUI';
|
||||
import ProjectRoleListItem from './ProjectRoleListItem/ProjectRoleListItem';
|
||||
import useProjectRoles from '../../../../../hooks/api/getters/useProjectRoles/useProjectRoles';
|
||||
import { IProjectRole } from '../../../../../interfaces/role';
|
||||
import useProjectRolesApi from '../../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
|
||||
import useToast from '../../../../../hooks/useToast';
|
||||
import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm';
|
||||
|
||||
const ROOTROLE = 'root';
|
||||
|
||||
const ProjectRoleList = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { roles } = useProjectRoles();
|
||||
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
||||
usePagination(roles, 10);
|
||||
const { deleteRole } = useProjectRolesApi();
|
||||
const { refetch } = useProjectRoles();
|
||||
const [currentRole, setCurrentRole] = useState<IProjectRole | null>(null);
|
||||
const [delDialog, setDelDialog] = useState(false);
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const deleteProjectRole = async () => {
|
||||
if (!currentRole?.id) return;
|
||||
try {
|
||||
await deleteRole(currentRole?.id);
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Successfully deleted role',
|
||||
text: 'Your role is now deleted',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
setDelDialog(false);
|
||||
setConfirmName('');
|
||||
};
|
||||
|
||||
const renderRoles = () => {
|
||||
return page
|
||||
.filter(role => role?.type !== ROOTROLE)
|
||||
.map((role: IProjectRole) => {
|
||||
return (
|
||||
<ProjectRoleListItem
|
||||
key={role.id}
|
||||
id={role.id}
|
||||
name={role.name}
|
||||
type={role.type}
|
||||
description={role.description}
|
||||
setCurrentRole={setCurrentRole}
|
||||
setDelDialog={setDelDialog}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (!roles) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell>Project Role</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell align="right">
|
||||
{hasAccess(ADMIN) ? 'Action' : ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{renderRoles()}</TableBody>
|
||||
<PaginateUI
|
||||
pages={pages}
|
||||
pageIndex={pageIndex}
|
||||
setPageIndex={setPageIndex}
|
||||
nextPage={nextPage}
|
||||
prevPage={prevPage}
|
||||
/>
|
||||
</Table>
|
||||
<br />
|
||||
<ProjectRoleDeleteConfirm
|
||||
role={currentRole}
|
||||
open={delDialog}
|
||||
setDeldialogue={setDelDialog}
|
||||
handleDeleteRole={deleteProjectRole}
|
||||
confirmName={confirmName}
|
||||
setConfirmName={setConfirmName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoleList;
|
@ -6,8 +6,11 @@ export const useStyles = makeStyles(theme => ({
|
||||
backgroundColor: theme.palette.grey[200],
|
||||
},
|
||||
},
|
||||
leftTableCell:{
|
||||
leftTableCell: {
|
||||
textAlign: 'left',
|
||||
maxWidth: '300px'
|
||||
}
|
||||
maxWidth: '300px',
|
||||
},
|
||||
icon: {
|
||||
color: theme.palette.grey[600],
|
||||
},
|
||||
}));
|
@ -0,0 +1,81 @@
|
||||
import { useStyles } from './ProjectRoleListItem.styles';
|
||||
import { TableRow, TableCell, Typography } from '@material-ui/core';
|
||||
import { Edit, Delete } from '@material-ui/icons';
|
||||
import { ADMIN } from '../../../../../providers/AccessProvider/permissions';
|
||||
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
||||
import PermissionIconButton from '../../../../../common/PermissionIconButton/PermissionIconButton';
|
||||
import { IProjectRole } from '../../../../../../interfaces/role';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
interface IRoleListItemProps {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
setCurrentRole: React.Dispatch<React.SetStateAction<IProjectRole>>;
|
||||
setDelDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const BUILTIN_ROLE_TYPE = 'project';
|
||||
|
||||
const RoleListItem = ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
setCurrentRole,
|
||||
setDelDialog,
|
||||
}: IRoleListItemProps) => {
|
||||
const history = useHistory();
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow className={styles.tableRow}>
|
||||
<TableCell>
|
||||
<SupervisedUserCircleIcon className={styles.icon} />
|
||||
</TableCell>
|
||||
<TableCell className={styles.leftTableCell}>
|
||||
<Typography variant="body2" data-loading>
|
||||
{name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell className={styles.leftTableCell}>
|
||||
<Typography variant="body2" data-loading>
|
||||
{description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="right">
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
aria-label="Edit"
|
||||
tooltip="Edit"
|
||||
disabled={type === BUILTIN_ROLE_TYPE}
|
||||
onClick={() => {
|
||||
history.push(`/admin/roles/${id}/edit`);
|
||||
}}
|
||||
permission={ADMIN}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
aria-label="Remove role"
|
||||
tooltip="Remove role"
|
||||
disabled={type === BUILTIN_ROLE_TYPE}
|
||||
onClick={() => {
|
||||
setCurrentRole({ id, name, description });
|
||||
setDelDialog(true);
|
||||
}}
|
||||
permission={ADMIN}
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleListItem;
|
@ -2,14 +2,14 @@ import { Button } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import PageContent from '../../common/PageContent';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import AdminMenu from '../admin-menu';
|
||||
import AccessContext from '../../../../contexts/AccessContext';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import HeaderTitle from '../../../common/HeaderTitle';
|
||||
import PageContent from '../../../common/PageContent';
|
||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||
import AdminMenu from '../../admin-menu';
|
||||
import { useStyles } from './ProjectRoles.styles';
|
||||
import RolesList from './RolesList';
|
||||
import ProjectRoleList from './ProjectRoleList/ProjectRoleList';
|
||||
|
||||
const ProjectRoles = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
@ -31,7 +31,11 @@ const ProjectRoles = () => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => console.log('hi')}
|
||||
onClick={() =>
|
||||
history.push(
|
||||
'/admin/create-project-role'
|
||||
)
|
||||
}
|
||||
>
|
||||
New Project role
|
||||
</Button>
|
||||
@ -48,7 +52,7 @@ const ProjectRoles = () => {
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<RolesList location={location} />}
|
||||
show={<ProjectRoleList />}
|
||||
elseShow={
|
||||
<Alert severity="error">
|
||||
You need instance admin to access this section.
|
@ -0,0 +1,269 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IPermission } from '../../../../interfaces/project';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import useProjectRolePermissions from '../../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
import useProjectRolesApi from '../../../../hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
|
||||
|
||||
export interface ICheckedPermission {
|
||||
[key: string]: IPermission;
|
||||
}
|
||||
|
||||
export const PROJECT_CHECK_ALL_KEY = 'check-all-project';
|
||||
export const ENVIRONMENT_CHECK_ALL_KEY = 'check-all-environment';
|
||||
|
||||
const useProjectRoleForm = (
|
||||
initialRoleName = '',
|
||||
initialRoleDesc = '',
|
||||
initialCheckedPermissions = {}
|
||||
) => {
|
||||
const { permissions } = useProjectRolePermissions({
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const [roleName, setRoleName] = useState(initialRoleName);
|
||||
const [roleDesc, setRoleDesc] = useState(initialRoleDesc);
|
||||
const [checkedPermissions, setCheckedPermissions] =
|
||||
useState<ICheckedPermission>(initialCheckedPermissions);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const { validateRole } = useProjectRolesApi();
|
||||
|
||||
useEffect(() => {
|
||||
setRoleName(initialRoleName);
|
||||
}, [initialRoleName]);
|
||||
|
||||
useEffect(() => {
|
||||
setRoleDesc(initialRoleDesc);
|
||||
}, [initialRoleDesc]);
|
||||
|
||||
const handleInitialCheckedPermissions = (
|
||||
initialCheckedPermissions: ICheckedPermission
|
||||
) => {
|
||||
const formattedInitialCheckedPermissions =
|
||||
isAllEnvironmentPermissionsChecked(
|
||||
isAllProjectPermissionsChecked(initialCheckedPermissions)
|
||||
);
|
||||
|
||||
setCheckedPermissions(formattedInitialCheckedPermissions || {});
|
||||
};
|
||||
|
||||
const isAllProjectPermissionsChecked = (
|
||||
initialCheckedPermissions: ICheckedPermission
|
||||
) => {
|
||||
const { project } = permissions;
|
||||
if (!project || project.length === 0) return;
|
||||
const isAllChecked = project.every((permission: IPermission) => {
|
||||
return initialCheckedPermissions[getRoleKey(permission)];
|
||||
});
|
||||
|
||||
if (isAllChecked) {
|
||||
initialCheckedPermissions[PROJECT_CHECK_ALL_KEY] = true;
|
||||
} else {
|
||||
delete initialCheckedPermissions[PROJECT_CHECK_ALL_KEY];
|
||||
}
|
||||
|
||||
return initialCheckedPermissions;
|
||||
};
|
||||
|
||||
const isAllEnvironmentPermissionsChecked = (
|
||||
initialCheckedPermissions: ICheckedPermission
|
||||
) => {
|
||||
const { environments } = permissions;
|
||||
if (!environments || environments.length === 0) return;
|
||||
environments.forEach(env => {
|
||||
const isAllChecked = env.permissions.every(
|
||||
(permission: IPermission) => {
|
||||
return initialCheckedPermissions[getRoleKey(permission)];
|
||||
}
|
||||
);
|
||||
|
||||
const key = `${ENVIRONMENT_CHECK_ALL_KEY}-${env.name}`;
|
||||
|
||||
if (isAllChecked) {
|
||||
initialCheckedPermissions[key] = true;
|
||||
} else {
|
||||
delete initialCheckedPermissions[key];
|
||||
}
|
||||
});
|
||||
return initialCheckedPermissions;
|
||||
};
|
||||
|
||||
const getCheckAllKeys = () => {
|
||||
const { environments } = permissions;
|
||||
const envKeys = environments.map(env => {
|
||||
return `${ENVIRONMENT_CHECK_ALL_KEY}-${env.name}`;
|
||||
});
|
||||
|
||||
return [...envKeys, PROJECT_CHECK_ALL_KEY];
|
||||
};
|
||||
|
||||
const handlePermissionChange = (permission: IPermission, type: string) => {
|
||||
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
|
||||
|
||||
if (checkedPermissionsCopy[getRoleKey(permission)]) {
|
||||
delete checkedPermissionsCopy[getRoleKey(permission)];
|
||||
} else {
|
||||
checkedPermissionsCopy[getRoleKey(permission)] = { ...permission };
|
||||
}
|
||||
|
||||
if (type === 'project') {
|
||||
checkedPermissionsCopy = isAllProjectPermissionsChecked(
|
||||
checkedPermissionsCopy
|
||||
);
|
||||
} else {
|
||||
checkedPermissionsCopy = isAllEnvironmentPermissionsChecked(
|
||||
checkedPermissionsCopy
|
||||
);
|
||||
}
|
||||
|
||||
setCheckedPermissions(checkedPermissionsCopy);
|
||||
};
|
||||
|
||||
const checkAllProjectPermissions = () => {
|
||||
const { project } = permissions;
|
||||
const checkedPermissionsCopy = cloneDeep(checkedPermissions);
|
||||
const checkedAll = checkedPermissionsCopy[PROJECT_CHECK_ALL_KEY];
|
||||
project.forEach((permission: IPermission, index: number) => {
|
||||
const lastItem = project.length - 1 === index;
|
||||
if (checkedAll) {
|
||||
if (checkedPermissionsCopy[getRoleKey(permission)]) {
|
||||
delete checkedPermissionsCopy[getRoleKey(permission)];
|
||||
}
|
||||
|
||||
if (lastItem) {
|
||||
delete checkedPermissionsCopy[PROJECT_CHECK_ALL_KEY];
|
||||
}
|
||||
} else {
|
||||
checkedPermissionsCopy[getRoleKey(permission)] = {
|
||||
...permission,
|
||||
};
|
||||
|
||||
if (lastItem) {
|
||||
checkedPermissionsCopy[PROJECT_CHECK_ALL_KEY] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setCheckedPermissions(checkedPermissionsCopy);
|
||||
};
|
||||
|
||||
const checkAllEnvironmentPermissions = (envName: string) => {
|
||||
const { environments } = permissions;
|
||||
const checkedPermissionsCopy = cloneDeep(checkedPermissions);
|
||||
const environmentCheckAllKey = `${ENVIRONMENT_CHECK_ALL_KEY}-${envName}`;
|
||||
const env = environments.find(env => env.name === envName);
|
||||
if (!env) return;
|
||||
const checkedAll = checkedPermissionsCopy[environmentCheckAllKey];
|
||||
|
||||
env.permissions.forEach((permission: IPermission, index: number) => {
|
||||
const lastItem = env.permissions.length - 1 === index;
|
||||
if (checkedAll) {
|
||||
if (checkedPermissionsCopy[getRoleKey(permission)]) {
|
||||
delete checkedPermissionsCopy[getRoleKey(permission)];
|
||||
}
|
||||
|
||||
if (lastItem) {
|
||||
delete checkedPermissionsCopy[environmentCheckAllKey];
|
||||
}
|
||||
} else {
|
||||
checkedPermissionsCopy[getRoleKey(permission)] = {
|
||||
...permission,
|
||||
};
|
||||
|
||||
if (lastItem) {
|
||||
checkedPermissionsCopy[environmentCheckAllKey] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setCheckedPermissions(checkedPermissionsCopy);
|
||||
};
|
||||
|
||||
const getProjectRolePayload = () => {
|
||||
const checkAllKeys = getCheckAllKeys();
|
||||
const permissions = Object.keys(checkedPermissions)
|
||||
.filter(key => {
|
||||
return !checkAllKeys.includes(key);
|
||||
})
|
||||
.map(permission => {
|
||||
return checkedPermissions[permission];
|
||||
});
|
||||
return {
|
||||
name: roleName,
|
||||
description: roleDesc,
|
||||
permissions,
|
||||
};
|
||||
};
|
||||
const NAME_EXISTS_ERROR =
|
||||
'BadRequestError: There already exists a role with the name';
|
||||
const validateNameUniqueness = async () => {
|
||||
const payload = getProjectRolePayload();
|
||||
|
||||
try {
|
||||
await validateRole(payload);
|
||||
} catch (e) {
|
||||
if (e.toString().includes(NAME_EXISTS_ERROR)) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
name: 'There already exists a role with this role name',
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateName = () => {
|
||||
if (roleName.length === 0) {
|
||||
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validatePermissions = () => {
|
||||
if (Object.keys(checkedPermissions).length === 0) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
permissions: 'You must include at least one permission.',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const getRoleKey = (permission: {
|
||||
id: number;
|
||||
environment?: string;
|
||||
}): string => {
|
||||
return permission.environment
|
||||
? `${permission.id}-${permission.environment}`
|
||||
: `${permission.id}`;
|
||||
};
|
||||
|
||||
return {
|
||||
roleName,
|
||||
roleDesc,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
handlePermissionChange,
|
||||
checkAllProjectPermissions,
|
||||
checkAllEnvironmentPermissions,
|
||||
checkedPermissions,
|
||||
getProjectRolePayload,
|
||||
validatePermissions,
|
||||
validateName,
|
||||
handleInitialCheckedPermissions,
|
||||
clearErrors,
|
||||
validateNameUniqueness,
|
||||
errors,
|
||||
getRoleKey,
|
||||
permissions,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectRoleForm;
|
@ -107,31 +107,6 @@ exports[`renders correctly with permissions 1`] = `
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textSecondary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
title="Delete application"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -153,377 +128,6 @@ exports[`renders correctly with permissions 1`] = `
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-10 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-scroller MuiTabs-fixed"
|
||||
onScroll={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": null,
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer MuiTabs-centered"
|
||||
onKeyDown={[Function]}
|
||||
role="tablist"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabpanel-0"
|
||||
aria-selected={true}
|
||||
className="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary Mui-selected"
|
||||
disabled={false}
|
||||
id="tab-0"
|
||||
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="tab"
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper"
|
||||
>
|
||||
Application overview
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-11 PrivateTabIndicator-colorPrimary-12 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-1"
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary"
|
||||
disabled={false}
|
||||
id="tab-1"
|
||||
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="tab"
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper"
|
||||
>
|
||||
Edit application
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-labelledby="wrapped-tab-0"
|
||||
hidden={false}
|
||||
id="wrapped-tabpanel-0"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-container"
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-md-6 MuiGrid-grid-xl-6"
|
||||
>
|
||||
<h6
|
||||
className="MuiTypography-root MuiTypography-subtitle1"
|
||||
style={
|
||||
Object {
|
||||
"padding": "1rem 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
Toggles
|
||||
</h6>
|
||||
<hr />
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemAvatar-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M14.4 6l-.24-1.2c-.09-.46-.5-.8-.98-.8H6c-.55 0-1 .45-1 1v15c0 .55.45 1 1 1s1-.45 1-1v-6h5.6l.24 1.2c.09.47.5.8.98.8H19c.55 0 1-.45 1-1V7c0-.55-.45-1-1-1h-4.6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="MuiListItemText-root MuiListItemText-multiline"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/projects/default/features/ToggleA/strategies/ToggleA"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleA
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
this is A toggle
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemAvatar-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
style={
|
||||
Object {
|
||||
"color": "red",
|
||||
}
|
||||
}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="MuiListItemText-root MuiListItemText-multiline"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/projects/default/create-toggle?name=ToggleB"
|
||||
onClick={[Function]}
|
||||
>
|
||||
ToggleB
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
Missing, want to create?
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-md-6 MuiGrid-grid-xl-6"
|
||||
>
|
||||
<h6
|
||||
className="MuiTypography-root MuiTypography-subtitle1"
|
||||
style={
|
||||
Object {
|
||||
"padding": "1rem 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
Implemented strategies
|
||||
</h6>
|
||||
<hr />
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemAvatar-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="MuiListItemText-root MuiListItemText-multiline"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/strategies/view/StrategyA"
|
||||
onClick={[Function]}
|
||||
>
|
||||
StrategyA
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
A description
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemAvatar-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
style={
|
||||
Object {
|
||||
"color": "red",
|
||||
}
|
||||
}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="MuiListItemText-root MuiListItemText-multiline"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="/strategies/create"
|
||||
onClick={[Function]}
|
||||
>
|
||||
StrategyB
|
||||
</a>
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
Missing, want to create?
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-md-12 MuiGrid-grid-xl-12"
|
||||
>
|
||||
<h6
|
||||
className="MuiTypography-root MuiTypography-subtitle1"
|
||||
style={
|
||||
Object {
|
||||
"padding": "1rem 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
1
|
||||
Instances registered
|
||||
</h6>
|
||||
<hr />
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemAvatar-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M23 8c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="MuiListItemText-root MuiListItemText-multiline"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
instance-1 (4.0)
|
||||
</span>
|
||||
<p
|
||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||
>
|
||||
<span>
|
||||
123.123.123.123
|
||||
last seen at
|
||||
|
||||
<small>
|
||||
23/02/2017, 15:56:49
|
||||
</small>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="wrapped-tab-1"
|
||||
hidden={true}
|
||||
id="wrapped-tabpanel-1"
|
||||
role="tabpanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -1,11 +1,21 @@
|
||||
import { Check } from '@material-ui/icons';
|
||||
import { Check, Close } from '@material-ui/icons';
|
||||
import { useStyles } from './CheckMarkBadge.styles';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const CheckMarkBadge = () => {
|
||||
interface ICheckMarkBadgeProps {
|
||||
className: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const CheckMarkBadge = ({ type, className }: ICheckMarkBadgeProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.badge}>
|
||||
<Check className={styles.check} />
|
||||
<div className={classnames(styles.badge, className)}>
|
||||
{type === 'error' ? (
|
||||
<Close className={styles.check} />
|
||||
) : (
|
||||
<Check className={styles.check} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
19
frontend/src/component/common/Codebox/Codebox.styles.ts
Normal file
19
frontend/src/component/common/Codebox/Codebox.styles.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
padding: '1rem',
|
||||
borderRadius: '3px',
|
||||
position: 'relative',
|
||||
},
|
||||
code: { wordBreak: 'break-all', color: '#fff', whiteSpace: 'pre-wrap' },
|
||||
icon: {
|
||||
fill: '#fff',
|
||||
},
|
||||
iconButton: {
|
||||
position: 'absolute',
|
||||
bottom: '10px',
|
||||
right: '20px',
|
||||
},
|
||||
}));
|
16
frontend/src/component/common/Codebox/Codebox.tsx
Normal file
16
frontend/src/component/common/Codebox/Codebox.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useStyles } from './Codebox.styles';
|
||||
|
||||
interface ICodeboxProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Codebox = ({ text }: ICodeboxProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<pre className={styles.code}>{text}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Codebox;
|
@ -0,0 +1,75 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
minHeight: '80vh',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
margin: '0 auto',
|
||||
[theme.breakpoints.down(900)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
sidebar: {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
padding: '2rem',
|
||||
width: '35%',
|
||||
borderTopLeftRadius: '12.5px',
|
||||
borderBottomLeftRadius: '12.5px',
|
||||
[theme.breakpoints.down(900)]: {
|
||||
width: '100%',
|
||||
borderBottomLeftRadius: '0',
|
||||
borderTopRightRadius: '12.5px',
|
||||
},
|
||||
[theme.breakpoints.down(500)]: {
|
||||
padding: '2rem 1rem',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
color: '#fff',
|
||||
marginBottom: '1rem',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
subtitle: {
|
||||
color: '#fff',
|
||||
marginBottom: '1rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
description: {
|
||||
color: '#fff',
|
||||
},
|
||||
linkContainer: {
|
||||
margin: '1.5rem 0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
linkIcon: {
|
||||
marginRight: '0.5rem',
|
||||
color: '#fff',
|
||||
},
|
||||
documentationLink: {
|
||||
color: '#fff',
|
||||
display: 'block',
|
||||
},
|
||||
formContent: {
|
||||
backgroundColor: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '2rem',
|
||||
width: '65%',
|
||||
borderTopRightRadius: '12.5px',
|
||||
borderBottomRightRadius: '12.5px',
|
||||
[theme.breakpoints.down(900)]: {
|
||||
width: '100%',
|
||||
borderBottomLeftRadius: '12.5px',
|
||||
borderTopRightRadius: '0',
|
||||
},
|
||||
[theme.breakpoints.down(500)]: {
|
||||
padding: '2rem 1rem',
|
||||
},
|
||||
},
|
||||
icon: { fill: '#fff' },
|
||||
}));
|
98
frontend/src/component/common/FormTemplate/FormTemplate.tsx
Normal file
98
frontend/src/component/common/FormTemplate/FormTemplate.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { useStyles } from './FormTemplate.styles';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import Codebox from '../Codebox/Codebox';
|
||||
import { IconButton, useMediaQuery } from '@material-ui/core';
|
||||
import { FileCopy } from '@material-ui/icons';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
import Loader from '../Loader/Loader';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
|
||||
interface ICreateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
documentationLink: string;
|
||||
loading?: boolean;
|
||||
formatApiCode: () => string;
|
||||
}
|
||||
|
||||
const FormTemplate: React.FC<ICreateProps> = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
documentationLink,
|
||||
loading,
|
||||
formatApiCode,
|
||||
}) => {
|
||||
// @ts-ignore-next-line
|
||||
const { setToastData } = useToast();
|
||||
const styles = useStyles();
|
||||
const smallScreen = useMediaQuery(`(max-width:${900}px)`);
|
||||
|
||||
const copyCommand = () => {
|
||||
if (copy(formatApiCode())) {
|
||||
setToastData({
|
||||
title: 'Successfully copied the command',
|
||||
text: 'The command should now be automatically copied to your clipboard',
|
||||
autoHideDuration: 6000,
|
||||
type: 'success',
|
||||
show: true,
|
||||
});
|
||||
} else {
|
||||
setToastData({
|
||||
title: 'Could not copy the command',
|
||||
text: 'Sorry, but we could not copy the command.',
|
||||
autoHideDuration: 6000,
|
||||
type: 'error',
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<aside className={styles.sidebar}>
|
||||
<h2 className={styles.title}>{title}</h2>
|
||||
<p className={styles.description}>{description}</p>
|
||||
|
||||
<div className={styles.linkContainer}>
|
||||
<MenuBookIcon className={styles.linkIcon} />
|
||||
<a
|
||||
className={styles.documentationLink}
|
||||
href={documentationLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={!smallScreen}
|
||||
show={
|
||||
<>
|
||||
<h3 className={styles.subtitle}>
|
||||
API Command{' '}
|
||||
<IconButton
|
||||
className={styles.iconButton}
|
||||
onClick={copyCommand}
|
||||
>
|
||||
<FileCopy className={styles.icon} />
|
||||
</IconButton>
|
||||
</h3>
|
||||
<Codebox text={formatApiCode()} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</aside>
|
||||
<div className={styles.formContent}>
|
||||
<ConditionallyRender
|
||||
condition={loading || false}
|
||||
show={<Loader />}
|
||||
elseShow={<>{children}</>}
|
||||
/>{' '}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormTemplate;
|
@ -1,4 +1,4 @@
|
||||
import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
||||
import { CREATE_FEATURE_STRATEGY } from '../../../providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../ConditionallyRender';
|
||||
import PermissionButton from '../../PermissionButton/PermissionButton';
|
||||
import StringTruncator from '../../StringTruncator/StringTruncator';
|
||||
@ -49,8 +49,9 @@ const NoItemsStrategies = ({
|
||||
show={
|
||||
<PermissionButton
|
||||
variant="contained"
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={envName}
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -11,7 +11,7 @@ const PageContent = ({
|
||||
headerContent,
|
||||
disablePadding = false,
|
||||
disableBorder = false,
|
||||
bodyClass = undefined,
|
||||
bodyClass = '',
|
||||
...rest
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { Button, Tooltip } from '@material-ui/core';
|
||||
import { OverridableComponent } from '@material-ui/core/OverridableComponent';
|
||||
import { Lock } from '@material-ui/icons';
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
|
||||
interface IPermissionIconButtonProps extends OverridableComponent<any> {
|
||||
permission: string;
|
||||
tooltip: string;
|
||||
interface IPermissionIconButtonProps
|
||||
extends React.HTMLProps<HTMLButtonElement> {
|
||||
permission: string | string[];
|
||||
tooltip?: string;
|
||||
onClick?: (e: any) => void;
|
||||
disabled?: boolean;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
}
|
||||
|
||||
const PermissionButton: React.FC<IPermissionIconButtonProps> = ({
|
||||
@ -20,13 +21,38 @@ const PermissionButton: React.FC<IPermissionIconButtonProps> = ({
|
||||
children,
|
||||
disabled,
|
||||
projectId,
|
||||
environmentId,
|
||||
...rest
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
let access;
|
||||
|
||||
const access = projectId
|
||||
? hasAccess(permission, projectId)
|
||||
: hasAccess(permission);
|
||||
const handleAccess = () => {
|
||||
let access;
|
||||
if (Array.isArray(permission)) {
|
||||
access = permission.some(permission => {
|
||||
if (projectId && environmentId) {
|
||||
return hasAccess(permission, projectId, environmentId);
|
||||
} else if (projectId) {
|
||||
return hasAccess(permission, projectId);
|
||||
} else {
|
||||
return hasAccess(permission);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (projectId && environmentId) {
|
||||
access = hasAccess(permission, projectId, environmentId);
|
||||
} else if (projectId) {
|
||||
access = hasAccess(permission, projectId);
|
||||
} else {
|
||||
access = hasAccess(permission);
|
||||
}
|
||||
}
|
||||
|
||||
return access;
|
||||
};
|
||||
|
||||
access = handleAccess();
|
||||
|
||||
const tooltipText = access
|
||||
? tooltip
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { IconButton, Tooltip } from '@material-ui/core';
|
||||
import { OverridableComponent } from '@material-ui/core/OverridableComponent';
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
|
||||
interface IPermissionIconButtonProps extends OverridableComponent<any> {
|
||||
interface IPermissionIconButtonProps
|
||||
extends React.HTMLProps<HTMLButtonElement> {
|
||||
permission: string;
|
||||
Icon?: React.ElementType;
|
||||
tooltip: string;
|
||||
onClick?: (e: any) => void;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
}
|
||||
|
||||
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
|
||||
@ -18,13 +19,19 @@ const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
|
||||
onClick,
|
||||
projectId,
|
||||
children,
|
||||
environmentId,
|
||||
...rest
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
let access;
|
||||
|
||||
const access = projectId
|
||||
? hasAccess(permission, projectId)
|
||||
: hasAccess(permission);
|
||||
if (projectId && environmentId) {
|
||||
access = hasAccess(permission, projectId, environmentId);
|
||||
} else if (projectId) {
|
||||
access = hasAccess(permission, projectId);
|
||||
} else {
|
||||
access = hasAccess(permission);
|
||||
}
|
||||
|
||||
const tooltipText = access
|
||||
? tooltip || ''
|
||||
|
@ -9,6 +9,7 @@ interface IPermissionSwitchProps extends OverridableComponent<any> {
|
||||
onChange?: (e: any) => void;
|
||||
disabled?: boolean;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
@ -17,14 +18,21 @@ const PermissionSwitch: React.FC<IPermissionSwitchProps> = ({
|
||||
tooltip = '',
|
||||
disabled,
|
||||
projectId,
|
||||
environmentId,
|
||||
checked,
|
||||
onChange,
|
||||
...rest
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const access = projectId
|
||||
? hasAccess(permission, projectId)
|
||||
: hasAccess(permission);
|
||||
|
||||
let access;
|
||||
if (projectId && environmentId) {
|
||||
access = hasAccess(permission, projectId, environmentId);
|
||||
} else if (projectId) {
|
||||
access = hasAccess(permission, projectId);
|
||||
} else {
|
||||
access = hasAccess(permission);
|
||||
}
|
||||
|
||||
const tooltipText = access
|
||||
? tooltip
|
||||
|
@ -10,6 +10,7 @@ interface IResponsiveButtonProps {
|
||||
disabled?: boolean;
|
||||
permission?: string;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
maxWidth: string;
|
||||
}
|
||||
|
||||
@ -21,6 +22,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
disabled = false,
|
||||
children,
|
||||
permission,
|
||||
environmentId,
|
||||
projectId,
|
||||
...rest
|
||||
}) => {
|
||||
@ -35,6 +37,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
onClick={onClick}
|
||||
permission={permission}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
data-loading
|
||||
{...rest}
|
||||
>
|
||||
@ -49,6 +52,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={disabled}
|
||||
environmentId={environmentId}
|
||||
data-loading
|
||||
{...rest}
|
||||
>
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { Portal, Snackbar } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import { IToast } from '../../../hooks/useToast';
|
||||
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
||||
|
||||
interface IToastProps extends IToast {
|
||||
onClose: () => void;
|
||||
autoHideDuration?: number;
|
||||
}
|
||||
|
||||
const Toast = ({
|
||||
show,
|
||||
onClose,
|
||||
type,
|
||||
text,
|
||||
autoHideDuration = 6000,
|
||||
}: IToastProps) => {
|
||||
const styles = useCommonStyles();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<AnimateOnMount
|
||||
mounted={show}
|
||||
start={styles.fadeInBottomStartWithoutFixed}
|
||||
enter={styles.fadeInBottomEnter}
|
||||
leave={styles.fadeInBottomLeave}
|
||||
container={styles.fullWidth}
|
||||
>
|
||||
<Snackbar
|
||||
open={show}
|
||||
onClose={onClose}
|
||||
autoHideDuration={autoHideDuration}
|
||||
style={{ bottom: '40px' }}
|
||||
>
|
||||
<Alert variant="filled" severity={type} onClose={onClose}>
|
||||
{text}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</AnimateOnMount>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
@ -0,0 +1,66 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
width: '450px',
|
||||
background: '#fff',
|
||||
boxShadow: '2px 2px 4px rgba(0,0,0,0.4)',
|
||||
zIndex: 500,
|
||||
margin: '0 auto',
|
||||
borderRadius: '12.5px',
|
||||
padding: '2rem',
|
||||
},
|
||||
innerContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
starting: {
|
||||
opacity: 0,
|
||||
},
|
||||
headerContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
confettiContainer: {
|
||||
position: 'relative',
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: '1rem',
|
||||
},
|
||||
headerStyles: {
|
||||
fontWeight: 'normal',
|
||||
margin: 0,
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
createdContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
anim: {
|
||||
animation: `$drop 10s 3s`,
|
||||
},
|
||||
checkMark: {
|
||||
width: '65px',
|
||||
height: '65px',
|
||||
},
|
||||
buttonStyle: {
|
||||
position: 'absolute',
|
||||
top: '-33px',
|
||||
right: '-29px',
|
||||
},
|
||||
'@keyframes drop': {
|
||||
'0%': {
|
||||
opacity: '0%',
|
||||
top: '0%',
|
||||
},
|
||||
'10%': {
|
||||
opacity: '100%',
|
||||
},
|
||||
'100%': {
|
||||
top: '100%',
|
||||
},
|
||||
},
|
||||
}));
|
94
frontend/src/component/common/ToastRenderer/Toast/Toast.tsx
Normal file
94
frontend/src/component/common/ToastRenderer/Toast/Toast.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useStyles } from './Toast.styles';
|
||||
import classnames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import CheckMarkBadge from '../../CheckmarkBadge/CheckMarkBadge';
|
||||
import UIContext, { IToastData } from '../../../../contexts/UIContext';
|
||||
import ConditionallyRender from '../../ConditionallyRender';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
|
||||
const Toast = ({ title, text, type, confetti }: IToastData) => {
|
||||
// @ts-ignore
|
||||
const { setToast } = useContext(UIContext);
|
||||
|
||||
const styles = useStyles();
|
||||
const confettiColors = ['#d13447', '#ffbf00', '#263672'];
|
||||
const confettiAmount = 200;
|
||||
|
||||
const getRandomNumber = (input: number) => {
|
||||
return Math.floor(Math.random() * input) + 1;
|
||||
};
|
||||
|
||||
const renderConfetti = () => {
|
||||
const elements = new Array(confettiAmount).fill(1);
|
||||
|
||||
const styledElements = elements.map((el, index) => {
|
||||
const width = getRandomNumber(8);
|
||||
const length = getRandomNumber(100);
|
||||
|
||||
const style = {
|
||||
position: 'absolute' as 'absolute',
|
||||
width: `${width}px`,
|
||||
height: `${width * 0.4}px`,
|
||||
backgroundColor: confettiColors[getRandomNumber(2)],
|
||||
left: `${length}%`,
|
||||
transform: `rotate(${getRandomNumber(101)}deg)`,
|
||||
animationDelay: `${getRandomNumber(5)}s`,
|
||||
animationDuration: `${getRandomNumber(3)}s`,
|
||||
animationEase: `${getRandomNumber(2)}s`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={style}
|
||||
className={classnames(styles.starting, styles.anim)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return styledElements;
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
setToast((prev: IToastData) => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.innerContainer}>
|
||||
<div className={styles.confettiContainer}>
|
||||
{confetti && renderConfetti()}
|
||||
<div className={styles.createdContainer}>
|
||||
<div className={styles.headerContainer}>
|
||||
<div>
|
||||
<CheckMarkBadge
|
||||
type={type}
|
||||
className={styles.checkMark}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.textContainer}>
|
||||
<h3 className={styles.headerStyles}>{title}</h3>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(text)}
|
||||
show={<p>{text}</p>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={hide}
|
||||
className={styles.buttonStyle}
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
@ -0,0 +1,44 @@
|
||||
import { Portal } from '@material-ui/core';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import UIContext, { IToastData } from '../../../contexts/UIContext';
|
||||
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
||||
import Toast from './Toast/Toast';
|
||||
|
||||
const ToastRenderer = () => {
|
||||
// @ts-ignore-next-line
|
||||
const { toastData, setToast } = useContext(UIContext);
|
||||
const styles = useCommonStyles();
|
||||
|
||||
const hide = () => {
|
||||
setToast((prev: IToastData) => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!toastData.autoHideDuration) return;
|
||||
let timeout = setTimeout(() => {
|
||||
hide();
|
||||
}, toastData.autoHideDuration);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
/* eslint-disable-next-line */
|
||||
}, [toastData?.show]);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<AnimateOnMount
|
||||
mounted={toastData?.show}
|
||||
start={styles.fadeInBottomStartWithoutFixed}
|
||||
enter={styles.fadeInBottomEnter}
|
||||
leave={styles.fadeInBottomLeave}
|
||||
container={styles.fullWidth}
|
||||
>
|
||||
<Toast {...toastData} />
|
||||
</AnimateOnMount>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToastRenderer;
|
@ -3,4 +3,5 @@ export const C = 'C';
|
||||
export const E = 'E';
|
||||
export const RBAC = 'RBAC';
|
||||
export const EEA = 'EEA';
|
||||
export const RE = 'RE';
|
||||
export const PROJECTFILTERING = false;
|
||||
|
@ -14,6 +14,7 @@ import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/Environment
|
||||
import Input from '../../common/Input/Input';
|
||||
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
|
||||
const NAME_EXISTS_ERROR = 'Error: Environment';
|
||||
|
||||
@ -27,7 +28,8 @@ const CreateEnvironment = () => {
|
||||
const { validateEnvName, createEnvironment, loading } = useEnvironmentApi();
|
||||
const { environments } = useEnvironments();
|
||||
const ref = useLoading(loading);
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastApiError } = useToast();
|
||||
const { refetch } = useProjectRolePermissions();
|
||||
|
||||
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
setType(event.currentTarget.value);
|
||||
@ -72,9 +74,10 @@ const CreateEnvironment = () => {
|
||||
|
||||
try {
|
||||
await createEnvironment(environment);
|
||||
refetch();
|
||||
setCreateSucceess(true);
|
||||
} catch (e) {
|
||||
setToastData({ show: true, type: 'error', text: e.toString() });
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -83,94 +86,107 @@ const CreateEnvironment = () => {
|
||||
<PageContent headerContent={<HeaderTitle title="Create environment" />}>
|
||||
<ConditionallyRender
|
||||
condition={createSuccess}
|
||||
show={
|
||||
<CreateEnvironmentSuccess
|
||||
name={envName}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
show={<CreateEnvironmentSuccess name={envName} type={type} />}
|
||||
elseShow={
|
||||
<ConditionallyRender condition={canCreateMoreEnvs} show={
|
||||
<div ref={ref}>
|
||||
<p className={styles.helperText} data-loading>
|
||||
Environments allow you to manage your product
|
||||
lifecycle from local development through production.
|
||||
Your projects and feature toggles are accessible in
|
||||
all your environments, but they can take different
|
||||
configurations per environment. This means that you
|
||||
can enable a feature toggle in a development or test
|
||||
environment without enabling the feature toggle in
|
||||
the production environment.
|
||||
</p>
|
||||
<ConditionallyRender
|
||||
condition={canCreateMoreEnvs}
|
||||
show={
|
||||
<div ref={ref}>
|
||||
<p className={styles.helperText} data-loading>
|
||||
Environments allow you to manage your
|
||||
product lifecycle from local development
|
||||
through production. Your projects and
|
||||
feature toggles are accessible in all your
|
||||
environments, but they can take different
|
||||
configurations per environment. This means
|
||||
that you can enable a feature toggle in a
|
||||
development or test environment without
|
||||
enabling the feature toggle in the
|
||||
production environment.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormControl component="fieldset">
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment Id and name
|
||||
</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormControl component="fieldset">
|
||||
<h3
|
||||
className={styles.formHeader}
|
||||
data-loading
|
||||
>
|
||||
Environment Id and name
|
||||
</h3>
|
||||
|
||||
<div
|
||||
data-loading
|
||||
className={
|
||||
styles.environmentDetailsContainer
|
||||
}
|
||||
>
|
||||
<div
|
||||
data-loading
|
||||
className={
|
||||
styles.environmentDetailsContainer
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Unique env name for SDK
|
||||
configurations.
|
||||
</p>
|
||||
<Input
|
||||
label="Environment Id"
|
||||
onFocus={clearNameError}
|
||||
placeholder="A unique name for your environment"
|
||||
onBlur={validateEnvironmentName}
|
||||
error={Boolean(nameError)}
|
||||
errorText={nameError}
|
||||
value={envName}
|
||||
onChange={handleEnvNameChange}
|
||||
className={styles.inputField}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EnvironmentTypeSelector
|
||||
onChange={handleTypeChange}
|
||||
value={type}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={styles.btnContainer}>
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-loading
|
||||
>
|
||||
Submit
|
||||
</Button>{' '}
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={goBack}
|
||||
data-loading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<Alert severity="error">
|
||||
<p>
|
||||
Unique env name for SDK configurations.
|
||||
Currently Unleash does not support more
|
||||
than 5 environments. If you need more
|
||||
please reach out.
|
||||
</p>
|
||||
<Input
|
||||
label="Environment Id"
|
||||
onFocus={clearNameError}
|
||||
placeholder="A unique name for your environment"
|
||||
onBlur={validateEnvironmentName}
|
||||
error={Boolean(nameError)}
|
||||
errorText={nameError}
|
||||
value={envName}
|
||||
onChange={handleEnvNameChange}
|
||||
className={styles.inputField}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EnvironmentTypeSelector
|
||||
onChange={handleTypeChange}
|
||||
value={type}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={styles.btnContainer}>
|
||||
</Alert>
|
||||
<br />
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
onClick={goBack}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-loading
|
||||
>
|
||||
Submit
|
||||
</Button>{' '}
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={goBack}
|
||||
data-loading
|
||||
>
|
||||
Cancel
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
} elseShow={
|
||||
<>
|
||||
<Alert severity="error">
|
||||
<p>Currently Unleash does not support more than 5 environments. If you need more please reach out.</p>
|
||||
</Alert>
|
||||
<br />
|
||||
<Button onClick={goBack} variant="contained" color="primary">Go back</Button>
|
||||
</>
|
||||
} />
|
||||
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{toast}
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
|
||||
import { mutate } from 'swr';
|
||||
import EditEnvironment from '../EditEnvironment/EditEnvironment';
|
||||
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
||||
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
|
||||
const EnvironmentList = () => {
|
||||
const defaultEnv = {
|
||||
@ -31,6 +32,8 @@ const EnvironmentList = () => {
|
||||
};
|
||||
const { environments, refetch } = useEnvironments();
|
||||
const [editEnvironment, setEditEnvironment] = useState(false);
|
||||
const { refetch: refetchProjectRolePermissions } =
|
||||
useProjectRolePermissions();
|
||||
|
||||
const [selectedEnv, setSelectedEnv] = useState(defaultEnv);
|
||||
const [delDialog, setDeldialogue] = useState(false);
|
||||
@ -38,7 +41,7 @@ const EnvironmentList = () => {
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
|
||||
const history = useHistory();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const {
|
||||
deleteEnvironment,
|
||||
changeSortOrder,
|
||||
@ -72,11 +75,7 @@ const EnvironmentList = () => {
|
||||
await sortOrderAPICall(sortOrder);
|
||||
refetch();
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
@ -84,28 +83,21 @@ const EnvironmentList = () => {
|
||||
try {
|
||||
await changeSortOrder(sortOrder);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEnvironment = async () => {
|
||||
try {
|
||||
await deleteEnvironment(selectedEnv.name);
|
||||
refetchProjectRolePermissions();
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully deleted environment.',
|
||||
title: 'Project environment deleted',
|
||||
text: 'You have successfully deleted the project environment.',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
} finally {
|
||||
setDeldialogue(false);
|
||||
setSelectedEnv(defaultEnv);
|
||||
@ -125,17 +117,14 @@ const EnvironmentList = () => {
|
||||
try {
|
||||
await toggleEnvironmentOn(selectedEnv.name);
|
||||
setToggleDialog(false);
|
||||
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully enabled environment.',
|
||||
title: 'Project environment enabled',
|
||||
text: 'Your environment is enabled',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
@ -146,16 +135,12 @@ const EnvironmentList = () => {
|
||||
await toggleEnvironmentOff(selectedEnv.name);
|
||||
setToggleDialog(false);
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully disabled environment.',
|
||||
title: 'Project environment disabled',
|
||||
text: 'Your environment is disabled.',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
@ -223,7 +208,6 @@ const EnvironmentList = () => {
|
||||
setToggleDialog={setToggleDialog}
|
||||
handleConfirmToggleEnvironment={handleConfirmToggleEnvironment}
|
||||
/>
|
||||
{toast}
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
@ -148,7 +148,10 @@ const FeatureToggleListItem = ({
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={project}
|
||||
disabled={!projectExists()}
|
||||
disabled={
|
||||
!hasAccess(UPDATE_FEATURE, project) ||
|
||||
!projectExists()
|
||||
}
|
||||
onClick={reviveFeature}
|
||||
>
|
||||
<Undo />
|
||||
|
@ -14,7 +14,7 @@ import FeatureType from '../../FeatureView2/FeatureType/FeatureType';
|
||||
import classNames from 'classnames';
|
||||
import CreatedAt from './CreatedAt';
|
||||
import useProject from '../../../../hooks/api/getters/useProject/useProject';
|
||||
import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
||||
import { UPDATE_FEATURE_ENVIRONMENT } from '../../../providers/AccessProvider/permissions';
|
||||
import PermissionSwitch from '../../../common/PermissionSwitch/PermissionSwitch';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ENVIRONMENT_STRATEGY_ERROR } from '../../../../constants/apiErrors';
|
||||
@ -37,7 +37,7 @@ const FeatureToggleListNewItem = ({
|
||||
projectId,
|
||||
createdAt,
|
||||
}: IFeatureToggleListNewItemProps) => {
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { toggleFeatureByEnvironment } = useToggleFeatureByEnv(
|
||||
projectId,
|
||||
name
|
||||
@ -65,8 +65,8 @@ const FeatureToggleListNewItem = ({
|
||||
toggleFeatureByEnvironment(env.name, env.enabled)
|
||||
.then(() => {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
title: 'Updated toggle status',
|
||||
text: 'Successfully updated toggle status.',
|
||||
});
|
||||
refetch();
|
||||
@ -75,11 +75,7 @@ const FeatureToggleListNewItem = ({
|
||||
if (e.message === ENVIRONMENT_STRATEGY_ERROR) {
|
||||
setShowInfoBox(true);
|
||||
} else {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.message,
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -149,8 +145,9 @@ const FeatureToggleListNewItem = ({
|
||||
<span data-loading style={{ display: 'block' }}>
|
||||
<PermissionSwitch
|
||||
checked={env.enabled}
|
||||
environmentId={env.name}
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={UPDATE_FEATURE_ENVIRONMENT}
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
handleToggle(env);
|
||||
@ -162,7 +159,6 @@ const FeatureToggleListNewItem = ({
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
{toast}
|
||||
<EnvironmentStrategyDialog
|
||||
open={showInfoBox}
|
||||
onClose={closeInfoBox}
|
||||
|
@ -69,7 +69,7 @@ const FeatureView = ({
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { project } = featureToggle || {};
|
||||
const { changeFeatureProject } = useFeatureApi();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const archive = !Boolean(isFeatureView);
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
@ -249,17 +249,13 @@ const FeatureView = ({
|
||||
.then(() => {
|
||||
fetchFeatureToggles();
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Updated toggle project',
|
||||
type: 'success',
|
||||
text: 'Successfully updated toggle project.',
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
});
|
||||
};
|
||||
|
||||
@ -435,7 +431,6 @@ const FeatureView = ({
|
||||
}}
|
||||
onClose={() => setDelDialog(false)}
|
||||
/>
|
||||
{toast}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
@ -30,7 +30,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
|
||||
const { addTagToFeature, loading } = useFeatureApi();
|
||||
const { refetch } = useTags(featureId);
|
||||
const [errors, setErrors] = useState({ tagError: '' });
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const [tag, setTag] = useState(DEFAULT_TAG);
|
||||
|
||||
const onCancel = () => {
|
||||
@ -58,10 +58,12 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
text: 'Successfully created tag',
|
||||
title: 'Added tag to toggle',
|
||||
text: 'We successfully added a tag to your toggle',
|
||||
confetti: true,
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.message);
|
||||
setErrors({ tagError: e.message });
|
||||
}
|
||||
};
|
||||
@ -108,7 +110,6 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
|
||||
</form>
|
||||
</>
|
||||
</Dialogue>
|
||||
{toast}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,16 +2,15 @@ import { useParams } from 'react-router';
|
||||
import { ENVIRONMENT_STRATEGY_ERROR } from '../../../../../../constants/apiErrors';
|
||||
import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import { TSetToastData } from '../../../../../../hooks/useToast';
|
||||
import useToast from '../../../../../../hooks/useToast';
|
||||
import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle';
|
||||
import { IFeatureViewParams } from '../../../../../../interfaces/params';
|
||||
import PermissionSwitch from '../../../../../common/PermissionSwitch/PermissionSwitch';
|
||||
import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import { UPDATE_FEATURE_ENVIRONMENT } from '../../../../../providers/AccessProvider/permissions';
|
||||
|
||||
interface IFeatureOverviewEnvSwitchProps {
|
||||
env: IFeatureEnvironment;
|
||||
setToastData: TSetToastData;
|
||||
callback?: () => void;
|
||||
text?: string;
|
||||
showInfoBox?: () => void;
|
||||
@ -19,7 +18,6 @@ interface IFeatureOverviewEnvSwitchProps {
|
||||
|
||||
const FeatureOverviewEnvSwitch = ({
|
||||
env,
|
||||
setToastData,
|
||||
callback,
|
||||
text,
|
||||
showInfoBox,
|
||||
@ -28,14 +26,15 @@ const FeatureOverviewEnvSwitch = ({
|
||||
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||
useFeatureApi();
|
||||
const { refetch } = useFeature(projectId, featureId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const handleToggleEnvironmentOn = async () => {
|
||||
try {
|
||||
await toggleFeatureEnvironmentOn(projectId, featureId, env.name);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
text: 'Successfully turned environment on.',
|
||||
title: 'Environment turned on',
|
||||
text: 'Successfully turned environment on. Strategies are executing in this environment.',
|
||||
});
|
||||
refetch();
|
||||
if (callback) {
|
||||
@ -45,11 +44,7 @@ const FeatureOverviewEnvSwitch = ({
|
||||
if (e.message === ENVIRONMENT_STRATEGY_ERROR) {
|
||||
showInfoBox(true);
|
||||
} else {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.message,
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -59,19 +54,15 @@ const FeatureOverviewEnvSwitch = ({
|
||||
await toggleFeatureEnvironmentOff(projectId, featureId, env.name);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
text: 'Successfully turned environment off.',
|
||||
title: 'Environment turned off',
|
||||
text: 'Successfully turned environment off. Strategies are no longer executing in this environment.',
|
||||
});
|
||||
refetch();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} catch (e: any) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -97,10 +88,11 @@ const FeatureOverviewEnvSwitch = ({
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<PermissionSwitch
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={UPDATE_FEATURE_ENVIRONMENT}
|
||||
projectId={projectId}
|
||||
checked={env.enabled}
|
||||
onChange={toggleEnvironment}
|
||||
environmentId={env.name}
|
||||
tooltip={''}
|
||||
/>
|
||||
{content}
|
||||
|
@ -3,7 +3,6 @@ import { useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import useToast from '../../../../../hooks/useToast';
|
||||
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||
import EnvironmentStrategyDialog from '../../../../common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
import FeatureOverviewEnvSwitch from './FeatureOverviewEnvSwitch/FeatureOverviewEnvSwitch';
|
||||
@ -14,7 +13,7 @@ const FeatureOverviewEnvSwitches = () => {
|
||||
const { featureId, projectId } = useParams<IFeatureViewParams>();
|
||||
useFeatureApi();
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const { toast, setToastData } = useToast();
|
||||
|
||||
const [showInfoBox, setShowInfoBox] = useState(false);
|
||||
const [environmentName, setEnvironmentName] = useState('');
|
||||
|
||||
@ -28,7 +27,6 @@ const FeatureOverviewEnvSwitches = () => {
|
||||
<FeatureOverviewEnvSwitch
|
||||
key={env.name}
|
||||
env={env}
|
||||
setToastData={setToastData}
|
||||
showInfoBox={() => {
|
||||
setEnvironmentName(env.name);
|
||||
setShowInfoBox(true);
|
||||
@ -49,7 +47,6 @@ const FeatureOverviewEnvSwitches = () => {
|
||||
</h3>
|
||||
</Tooltip>
|
||||
{renderEnvironmentSwitches()}
|
||||
{toast}
|
||||
<EnvironmentStrategyDialog
|
||||
open={showInfoBox}
|
||||
onClose={closeInfoBox}
|
||||
|
@ -22,7 +22,7 @@ import DisabledIndicator from '../../../../../common/DisabledIndicator/DisabledI
|
||||
import EnvironmentIcon from '../../../../../common/EnvironmentIcon/EnvironmentIcon';
|
||||
import PermissionButton from '../../../../../common/PermissionButton/PermissionButton';
|
||||
import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import { CREATE_FEATURE_STRATEGY } from '../../../../../providers/AccessProvider/permissions';
|
||||
|
||||
import { useStyles } from './FeatureOverviewEnvironment.styles';
|
||||
import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody';
|
||||
@ -108,7 +108,9 @@ const FeatureOverviewEnvironment = ({
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<PermissionButton
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={env.name}
|
||||
onClick={() => history.push(strategiesLink)}
|
||||
className={styles.addStrategyButton}
|
||||
>
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { useParams, useHistory, Link } from 'react-router-dom';
|
||||
import { IFeatureViewParams } from '../../../../../../../interfaces/params';
|
||||
import ConditionallyRender from '../../../../../../common/ConditionallyRender';
|
||||
import NoItemsStrategies from '../../../../../../common/NoItems/NoItemsStrategies/NoItemsStrategies';
|
||||
import FeatureOverviewEnvironmentStrategies from '../FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies';
|
||||
|
||||
import { useStyles } from '../FeatureOverviewEnvironment.styles';
|
||||
import { IFeatureEnvironment } from '../../../../../../../interfaces/featureToggle';
|
||||
import { UPDATE_FEATURE } from '../../../../../../providers/AccessProvider/permissions';
|
||||
import ResponsiveButton from '../../../../../../common/ResponsiveButton/ResponsiveButton';
|
||||
import { Add } from '@material-ui/icons';
|
||||
import { CREATE_FEATURE_STRATEGY } from '../../../../../../providers/AccessProvider/permissions';
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from '../../../../../../../contexts/AccessContext';
|
||||
|
||||
interface IFeatureOverviewEnvironmentBodyProps {
|
||||
getOverviewText: () => string;
|
||||
@ -22,6 +21,7 @@ const FeatureOverviewEnvironmentBody = ({
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
const styles = useStyles();
|
||||
const history = useHistory();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const strategiesLink = `/projects/${projectId}/features2/${featureId}/strategies?environment=${featureEnvironment?.name}&addStrategy=true`;
|
||||
|
||||
if (!featureEnvironment) return null;
|
||||
@ -41,16 +41,20 @@ const FeatureOverviewEnvironmentBody = ({
|
||||
condition={featureEnvironment?.strategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
<div className={styles.linkContainer}>
|
||||
<ResponsiveButton
|
||||
Icon={Add}
|
||||
onClick={() => history.push(strategiesLink)}
|
||||
maxWidth="700px"
|
||||
permission={UPDATE_FEATURE}
|
||||
>
|
||||
Add strategy
|
||||
</ResponsiveButton>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
featureEnvironment.name
|
||||
)}
|
||||
show={
|
||||
<div className={styles.linkContainer}>
|
||||
<Link to={strategiesLink}>
|
||||
Edit strategies
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<FeatureOverviewEnvironmentStrategies
|
||||
strategies={featureEnvironment?.strategies}
|
||||
environmentName={featureEnvironment.name}
|
||||
|
@ -78,7 +78,7 @@ const FeatureOverviewMetaData = () => {
|
||||
condition={tags.length > 0}
|
||||
show={
|
||||
<div className={styles.paddingContainerBottom}>
|
||||
<FeatureOverviewTags />
|
||||
<FeatureOverviewTags projectId={projectId} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -14,11 +14,18 @@ import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/use
|
||||
import Dialogue from '../../../../../common/Dialogue';
|
||||
import { ITag } from '../../../../../../interfaces/tags';
|
||||
import useToast from '../../../../../../hooks/useToast';
|
||||
import { DELETE_TAG } from '../../../../../providers/AccessProvider/permissions';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||
import AccessContext from '../../../../../../contexts/AccessContext';
|
||||
|
||||
const FeatureOverviewTags = () => {
|
||||
interface IFeatureOverviewTagsProps extends React.HTMLProps<HTMLButtonElement> {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
|
||||
projectId,
|
||||
...rest
|
||||
}) => {
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [selectedTag, setSelectedTag] = useState<ITag>({
|
||||
value: '',
|
||||
@ -29,9 +36,9 @@ const FeatureOverviewTags = () => {
|
||||
const { tags, refetch } = useTags(featureId);
|
||||
const { tagTypes } = useTagTypes();
|
||||
const { deleteTagFromFeature } = useFeatureApi();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const canDeleteTag = hasAccess(DELETE_TAG);
|
||||
const canDeleteTag = hasAccess(UPDATE_FEATURE, projectId);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
@ -43,15 +50,11 @@ const FeatureOverviewTags = () => {
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
title: 'Tag deleted',
|
||||
text: 'Successfully deleted tag',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -114,7 +117,7 @@ const FeatureOverviewTags = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} {...rest}>
|
||||
<Dialogue
|
||||
open={showDelDialog}
|
||||
onClose={() => {
|
||||
@ -136,7 +139,6 @@ const FeatureOverviewTags = () => {
|
||||
elseShow={<p data-loading>No tags to display</p>}
|
||||
/>
|
||||
</div>
|
||||
{toast}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ const FeatureSettingsMetadata = () => {
|
||||
const [description, setDescription] = useState(feature.description);
|
||||
const [type, setType] = useState(feature.type);
|
||||
const editable = hasAccess(UPDATE_FEATURE, projectId);
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const { patchFeatureToggle } = useFeatureApi();
|
||||
|
||||
@ -48,18 +48,14 @@ const FeatureSettingsMetadata = () => {
|
||||
const patch = createPatch();
|
||||
await patchFeatureToggle(projectId, featureId, patch);
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Updated metadata',
|
||||
type: 'success',
|
||||
text: 'Successfully updated feature toggle metadata',
|
||||
});
|
||||
setDirty(false);
|
||||
refetch();
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
@ -95,8 +91,6 @@ const FeatureSettingsMetadata = () => {
|
||||
</PermissionButton>
|
||||
}
|
||||
/>
|
||||
|
||||
{toast}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -6,15 +6,12 @@ import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import useUser from '../../../../../hooks/api/getters/useUser/useUser';
|
||||
import useToast from '../../../../../hooks/useToast';
|
||||
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||
import { projectFilterGenerator } from '../../../../../utils/project-filter-generator';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from '../../../../providers/AccessProvider/permissions';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../../../../providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../../../common/ConditionallyRender';
|
||||
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
|
||||
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
|
||||
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
|
||||
import { IPermission } from '../../../../../interfaces/user';
|
||||
|
||||
const FeatureSettingsProject = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
@ -23,10 +20,10 @@ const FeatureSettingsProject = () => {
|
||||
const [project, setProject] = useState(feature.project);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const editable = hasAccess(UPDATE_FEATURE, projectId);
|
||||
const editable = hasAccess(MOVE_FEATURE_TOGGLE, projectId);
|
||||
const { permissions } = useUser();
|
||||
const { changeFeatureProject } = useFeatureApi();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
@ -38,13 +35,24 @@ const FeatureSettingsProject = () => {
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [project]);
|
||||
|
||||
useEffect(() => {
|
||||
const movableTargets = createMoveTargets();
|
||||
|
||||
if (!movableTargets[project]) {
|
||||
setDirty(false);
|
||||
setProject(projectId);
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, [permissions?.length]);
|
||||
|
||||
const updateProject = async () => {
|
||||
const newProject = project;
|
||||
try {
|
||||
await changeFeatureProject(projectId, featureId, newProject);
|
||||
refetch();
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Updated project',
|
||||
confetti: true,
|
||||
type: 'success',
|
||||
text: 'Successfully updated toggle project.',
|
||||
});
|
||||
@ -54,14 +62,31 @@ const FeatureSettingsProject = () => {
|
||||
`/projects/${newProject}/features2/${featureId}/settings`
|
||||
);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const createMoveTargets = () => {
|
||||
return permissions.reduce(
|
||||
(acc: { [key: string]: boolean }, permission: IPermission) => {
|
||||
if (permission.permission === MOVE_FEATURE_TOGGLE) {
|
||||
acc[permission.project] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
const filterProjects = () => {
|
||||
const validTargets = createMoveTargets();
|
||||
|
||||
return (project: string) => {
|
||||
if (validTargets[project]) {
|
||||
return project;
|
||||
}
|
||||
};
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<FeatureProjectSelect
|
||||
@ -69,13 +94,13 @@ const FeatureSettingsProject = () => {
|
||||
onChange={e => setProject(e.target.value)}
|
||||
label="Project"
|
||||
enabled={editable}
|
||||
filter={projectFilterGenerator({ permissions }, CREATE_FEATURE)}
|
||||
filter={filterProjects()}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={dirty}
|
||||
show={
|
||||
<PermissionButton
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={MOVE_FEATURE_TOGGLE}
|
||||
tooltip="Update feature"
|
||||
onClick={() => setShowConfirmDialog(true)}
|
||||
projectId={projectId}
|
||||
@ -91,7 +116,6 @@ const FeatureSettingsProject = () => {
|
||||
onClose={() => setShowConfirmDialog(false)}
|
||||
onClick={updateProject}
|
||||
/>
|
||||
{toast}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -16,14 +16,11 @@ import { PRODUCTION } from '../../../../../../constants/environmentTypes';
|
||||
import { ADD_NEW_STRATEGY_SAVE_ID } from '../../../../../../testIds';
|
||||
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import { scrollToTop } from '../../../../../common/util';
|
||||
import useToast from '../../../../../../hooks/useToast';
|
||||
|
||||
interface IFeatureStrategiesConfigure {
|
||||
setToastData: React.Dispatch<React.SetStateAction<IToastType>>;
|
||||
}
|
||||
const FeatureStrategiesConfigure = ({
|
||||
setToastData,
|
||||
}: IFeatureStrategiesConfigure) => {
|
||||
const FeatureStrategiesConfigure = () => {
|
||||
const history = useHistory();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
const { refetch } = useFeature(projectId, featureId);
|
||||
@ -89,19 +86,16 @@ const FeatureStrategiesConfigure = ({
|
||||
setConfigureNewStrategy(null);
|
||||
setExpandedSidebar(false);
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully added strategy.',
|
||||
text: 'Successfully added strategy',
|
||||
title: 'Added strategy',
|
||||
confetti: true,
|
||||
});
|
||||
history.replace(history.location.pathname);
|
||||
refetch();
|
||||
scrollToTop();
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -34,7 +34,6 @@ const FeatureStrategiesEnvironmentList = ({
|
||||
|
||||
const {
|
||||
activeEnvironmentsRef,
|
||||
toast,
|
||||
setToastData,
|
||||
deleteStrategy,
|
||||
updateStrategy,
|
||||
@ -180,7 +179,6 @@ const FeatureStrategiesEnvironmentList = ({
|
||||
/>
|
||||
|
||||
{dropboxMarkup}
|
||||
{toast}
|
||||
{delDialogueMarkup}
|
||||
{productionGuardMarkup}
|
||||
</div>
|
||||
|
@ -24,7 +24,7 @@ const useFeatureStrategiesEnvironmentList = () => {
|
||||
featureCache,
|
||||
} = useContext(FeatureStrategiesUIContext);
|
||||
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const [delDialog, setDelDialog] = useState({ strategyId: '', show: false });
|
||||
const [productionGuard, setProductionGuard] = useState({
|
||||
show: false,
|
||||
@ -65,7 +65,8 @@ const useFeatureStrategiesEnvironmentList = () => {
|
||||
);
|
||||
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Updates strategy',
|
||||
confetti: true,
|
||||
type: 'success',
|
||||
text: `Successfully updated strategy`,
|
||||
});
|
||||
@ -85,11 +86,7 @@ const useFeatureStrategiesEnvironmentList = () => {
|
||||
history.replace(history.location.pathname);
|
||||
setFeatureCache(feature);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -116,23 +113,18 @@ const useFeatureStrategiesEnvironmentList = () => {
|
||||
|
||||
setDelDialog({ strategyId: '', show: false });
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
title: 'Deleted strategy',
|
||||
text: `Successfully deleted strategy from ${featureId}`,
|
||||
});
|
||||
history.replace(history.location.pathname);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
activeEnvironmentsRef,
|
||||
toast,
|
||||
setToastData,
|
||||
deleteStrategy,
|
||||
updateStrategy,
|
||||
|
@ -7,10 +7,9 @@ import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||
import { ADD_NEW_STRATEGY_ID } from '../../../../../testIds';
|
||||
import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
|
||||
import { CREATE_FEATURE_STRATEGY } from '../../../../providers/AccessProvider/permissions';
|
||||
|
||||
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import useToast from '../../../../../hooks/useToast';
|
||||
import useTabs from '../../../../../hooks/useTabs';
|
||||
import useQueryParams from '../../../../../hooks/useQueryParams';
|
||||
|
||||
@ -34,7 +33,6 @@ const FeatureStrategiesEnvironments = () => {
|
||||
|
||||
const startingTabId = 0;
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
const { toast, setToastData } = useToast();
|
||||
const [showRefreshPrompt, setShowRefreshPrompt] = useState(false);
|
||||
|
||||
const styles = useStyles();
|
||||
@ -45,6 +43,7 @@ const FeatureStrategiesEnvironments = () => {
|
||||
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(startingTabId);
|
||||
const {
|
||||
setActiveEnvironment,
|
||||
activeEnvironment,
|
||||
configureNewStrategy,
|
||||
expandedSidebar,
|
||||
setExpandedSidebar,
|
||||
@ -250,7 +249,11 @@ const FeatureStrategiesEnvironments = () => {
|
||||
|
||||
const listContainerClasses = classNames(styles.listContainer, {
|
||||
[styles.listContainerFullWidth]: expandedSidebar,
|
||||
[styles.listContainerWithoutSidebar]: !hasAccess(UPDATE_FEATURE),
|
||||
[styles.listContainerWithoutSidebar]: !hasAccess(
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
activeEnvironment?.name
|
||||
),
|
||||
});
|
||||
|
||||
return featureCache?.environments?.map((env, index) => {
|
||||
@ -276,7 +279,8 @@ const FeatureStrategiesEnvironments = () => {
|
||||
Icon={Add}
|
||||
maxWidth="700px"
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
environmentId={activeEnvironment.name}
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
>
|
||||
Add new strategy
|
||||
</ResponsiveButton>
|
||||
@ -376,14 +380,9 @@ const FeatureStrategiesEnvironments = () => {
|
||||
{renderTabPanels()}
|
||||
<ConditionallyRender
|
||||
condition={configureNewStrategy}
|
||||
show={
|
||||
<FeatureStrategiesConfigure
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
}
|
||||
show={<FeatureStrategiesConfigure />}
|
||||
/>
|
||||
</div>
|
||||
{toast}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -21,8 +21,10 @@ import {
|
||||
STRATEGY_ACCORDION_ID,
|
||||
UPDATE_STRATEGY_BUTTON_ID,
|
||||
} from '../../../../../../testIds';
|
||||
import AccessContext from '../../../../../../contexts/AccessContext';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import {
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
} from '../../../../../providers/AccessProvider/permissions';
|
||||
import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import PermissionIconButton from '../../../../../common/PermissionIconButton/PermissionIconButton';
|
||||
import PermissionButton from '../../../../../common/PermissionButton/PermissionButton';
|
||||
@ -40,7 +42,6 @@ const FeatureStrategyEditable = ({
|
||||
setDelDialog,
|
||||
index,
|
||||
}: IFeatureStrategyEditable) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { loading } = useFeatureApi();
|
||||
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
@ -143,27 +144,23 @@ const FeatureStrategyEditable = ({
|
||||
setStrategyConstraints={setStrategyConstraints}
|
||||
dirty={dirty[strategy.id]}
|
||||
actions={
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_FEATURE)}
|
||||
show={
|
||||
<Tooltip title="Delete strategy">
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
data-test={`${DELETE_STRATEGY_ID}-${strategy.name}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setDelDialog({
|
||||
strategyId: strategy.id,
|
||||
show: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Tooltip title="Delete strategy">
|
||||
<PermissionIconButton
|
||||
permission={DELETE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={activeEnvironment.name}
|
||||
data-test={`${DELETE_STRATEGY_ID}-${strategy.name}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setDelDialog({
|
||||
strategyId: strategy.id,
|
||||
show: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
@ -172,8 +169,9 @@ const FeatureStrategyEditable = ({
|
||||
<>
|
||||
<div className={styles.buttonContainer}>
|
||||
<PermissionButton
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={UPDATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={activeEnvironment?.name}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={styles.editButton}
|
||||
@ -189,8 +187,9 @@ const FeatureStrategyEditable = ({
|
||||
disabled={loading}
|
||||
color="tertiary"
|
||||
variant="text"
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={UPDATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={activeEnvironment?.name}
|
||||
>
|
||||
Discard changes
|
||||
</PermissionButton>
|
||||
|
@ -14,9 +14,10 @@ import {
|
||||
getFeatureStrategyIcon,
|
||||
getHumanReadableStrategyName,
|
||||
} from '../../../../../../utils/strategy-names';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import { CREATE_FEATURE_STRATEGY } from '../../../../../providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../../../../common/ConditionallyRender';
|
||||
import { useStyles } from './FeatureStrategyCard.styles';
|
||||
import PermissionIconButton from '../../../../../common/PermissionIconButton/PermissionIconButton';
|
||||
|
||||
interface IFeatureStrategyCardProps {
|
||||
name: string;
|
||||
@ -33,15 +34,12 @@ const FeatureStrategyCard = ({
|
||||
configureNewStrategy,
|
||||
index,
|
||||
}: IFeatureStrategyCardProps) => {
|
||||
const { featureId } = useParams<IFeatureViewParams>();
|
||||
const { featureId, projectId } = useParams<IFeatureViewParams>();
|
||||
const { strategies } = useStrategies();
|
||||
|
||||
const { setConfigureNewStrategy, setExpandedSidebar } = useContext(
|
||||
FeatureStrategiesUIContext
|
||||
);
|
||||
const { setConfigureNewStrategy, setExpandedSidebar, activeEnvironment } =
|
||||
useContext(FeatureStrategiesUIContext);
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const canUpdateFeature = hasAccess(UPDATE_FEATURE);
|
||||
|
||||
const handleClick = () => {
|
||||
const strategy = getStrategyObject(strategies, name, featureId);
|
||||
if (!strategy) return;
|
||||
@ -54,7 +52,11 @@ const FeatureStrategyCard = ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, drag] = useDrag({
|
||||
type: FEATURE_STRATEGIES_DRAG_TYPE,
|
||||
canDrag: canUpdateFeature,
|
||||
canDrag: hasAccess(
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
activeEnvironment?.name
|
||||
),
|
||||
item: () => {
|
||||
return { name };
|
||||
},
|
||||
@ -80,16 +82,18 @@ const FeatureStrategyCard = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rightSection}>
|
||||
<IconButton
|
||||
<PermissionIconButton
|
||||
className={styles.addButton}
|
||||
onClick={handleClick}
|
||||
data-test={`${ADD_NEW_STRATEGY_CARD_BUTTON_ID}-${
|
||||
index + 1
|
||||
}`}
|
||||
disabled={!canUpdateFeature}
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={activeEnvironment?.name}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</PermissionIconButton>
|
||||
<Tooltip title={readableName}>
|
||||
<p className={styles.title}>{readableName}</p>
|
||||
</Tooltip>
|
||||
|
@ -17,11 +17,15 @@ import Dialogue from '../../../../../common/Dialogue';
|
||||
import DefaultStrategy from '../../common/DefaultStrategy/DefaultStrategy';
|
||||
import { ADD_CONSTRAINT_ID } from '../../../../../../testIds';
|
||||
import AccessContext from '../../../../../../contexts/AccessContext';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import {
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
} from '../../../../../providers/AccessProvider/permissions';
|
||||
import Constraint from '../../../../../common/Constraint/Constraint';
|
||||
import PermissionButton from '../../../../../common/PermissionButton/PermissionButton';
|
||||
import { useParams } from 'react-router';
|
||||
import { IFeatureViewParams } from '../../../../../../interfaces/params';
|
||||
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
|
||||
|
||||
interface IFeatureStrategyAccordionBodyProps {
|
||||
strategy: IFeatureStrategy;
|
||||
@ -32,175 +36,190 @@ interface IFeatureStrategyAccordionBodyProps {
|
||||
setStrategyConstraints: React.Dispatch<React.SetStateAction<IConstraint[]>>;
|
||||
}
|
||||
|
||||
const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps> =
|
||||
({
|
||||
strategy,
|
||||
updateParameters,
|
||||
children,
|
||||
constraints,
|
||||
updateConstraints,
|
||||
setStrategyConstraints,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const { projectId } = useParams<IFeatureViewParams>();
|
||||
const [constraintError, setConstraintError] = useState({});
|
||||
const { strategies } = useStrategies();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const [showConstraints, setShowConstraints] = useState(false);
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const FeatureStrategyAccordionBody: React.FC<
|
||||
IFeatureStrategyAccordionBodyProps
|
||||
> = ({
|
||||
strategy,
|
||||
updateParameters,
|
||||
children,
|
||||
constraints,
|
||||
updateConstraints,
|
||||
setStrategyConstraints,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const { projectId } = useParams<IFeatureViewParams>();
|
||||
const [constraintError, setConstraintError] = useState({});
|
||||
const { strategies } = useStrategies();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const [showConstraints, setShowConstraints] = useState(false);
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { activeEnvironment } = useContext(FeatureStrategiesUIContext);
|
||||
|
||||
const { context } = useUnleashContext();
|
||||
const { context } = useUnleashContext();
|
||||
|
||||
const resolveInputType = () => {
|
||||
switch (strategy?.name) {
|
||||
case 'default':
|
||||
return DefaultStrategy;
|
||||
case 'flexibleRollout':
|
||||
return FlexibleStrategy;
|
||||
case 'userWithId':
|
||||
return UserWithIdStrategy;
|
||||
default:
|
||||
return GeneralStrategy;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleConstraints = () => setShowConstraints(prev => !prev);
|
||||
|
||||
const resolveStrategyDefinition = () => {
|
||||
const definition = strategies.find(
|
||||
definition => definition.name === strategy.name
|
||||
);
|
||||
|
||||
return definition;
|
||||
};
|
||||
|
||||
const saveConstraintsLocally = () => {
|
||||
let valid = true;
|
||||
|
||||
constraints.forEach((constraint, index) => {
|
||||
const { values } = constraint;
|
||||
|
||||
if (values.length === 0) {
|
||||
setConstraintError(prev => ({
|
||||
...prev,
|
||||
[`${constraint.contextName}-${index}`]:
|
||||
'You need to specify at least one value',
|
||||
}));
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
setShowConstraints(false);
|
||||
setStrategyConstraints(constraints);
|
||||
}
|
||||
};
|
||||
|
||||
const renderConstraints = () => {
|
||||
if (constraints.length === 0) {
|
||||
return (
|
||||
<p className={styles.noConstraints}>
|
||||
No constraints configured
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return constraints.map((constraint, index) => {
|
||||
return (
|
||||
<Constraint
|
||||
constraint={constraint}
|
||||
editCallback={() => {
|
||||
setShowConstraints(true);
|
||||
}}
|
||||
deleteCallback={() => {
|
||||
removeConstraint(index);
|
||||
}}
|
||||
key={`${constraint.contextName}-${index}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const removeConstraint = (index: number) => {
|
||||
const updatedConstraints = [...constraints];
|
||||
updatedConstraints.splice(index, 1);
|
||||
|
||||
updateConstraints(updatedConstraints);
|
||||
};
|
||||
|
||||
const closeConstraintDialog = () => {
|
||||
setShowConstraints(false);
|
||||
const filteredConstraints = constraints.filter(constraint => {
|
||||
return constraint.values.length > 0;
|
||||
});
|
||||
updateConstraints(filteredConstraints);
|
||||
};
|
||||
|
||||
const Type = resolveInputType();
|
||||
const definition = resolveStrategyDefinition();
|
||||
|
||||
const { parameters } = strategy;
|
||||
const ON = uiConfig.flags[C];
|
||||
|
||||
return (
|
||||
<div className={styles.accordionContainer}>
|
||||
<ConditionallyRender
|
||||
condition={ON}
|
||||
show={
|
||||
<>
|
||||
<p className={styles.constraintHeader}>
|
||||
Constraints
|
||||
</p>
|
||||
{renderConstraints()}
|
||||
|
||||
<PermissionButton
|
||||
className={styles.addConstraintBtn}
|
||||
onClick={toggleConstraints}
|
||||
variant={'text'}
|
||||
data-test={ADD_CONSTRAINT_ID}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
>
|
||||
+ Add constraints
|
||||
</PermissionButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Dialogue
|
||||
title="Define constraints"
|
||||
open={showConstraints}
|
||||
onClick={saveConstraintsLocally}
|
||||
primaryButtonText="Update constraints"
|
||||
secondaryButtonText="Cancel"
|
||||
onClose={closeConstraintDialog}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
>
|
||||
<StrategyConstraints
|
||||
updateConstraints={updateConstraints}
|
||||
constraints={constraints || []}
|
||||
constraintError={constraintError}
|
||||
setConstraintError={setConstraintError}
|
||||
/>
|
||||
</Dialogue>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(UPDATE_FEATURE)}
|
||||
show={
|
||||
<Type
|
||||
parameters={parameters}
|
||||
updateParameter={updateParameters}
|
||||
strategyDefinition={definition}
|
||||
context={context}
|
||||
editable
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const resolveInputType = () => {
|
||||
switch (strategy?.name) {
|
||||
case 'default':
|
||||
return DefaultStrategy;
|
||||
case 'flexibleRollout':
|
||||
return FlexibleStrategy;
|
||||
case 'userWithId':
|
||||
return UserWithIdStrategy;
|
||||
default:
|
||||
return GeneralStrategy;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleConstraints = () => setShowConstraints(prev => !prev);
|
||||
|
||||
const resolveStrategyDefinition = () => {
|
||||
const definition = strategies.find(
|
||||
definition => definition.name === strategy.name
|
||||
);
|
||||
|
||||
return definition;
|
||||
};
|
||||
|
||||
const saveConstraintsLocally = () => {
|
||||
let valid = true;
|
||||
|
||||
constraints.forEach((constraint, index) => {
|
||||
const { values } = constraint;
|
||||
|
||||
if (values.length === 0) {
|
||||
setConstraintError(prev => ({
|
||||
...prev,
|
||||
[`${constraint.contextName}-${index}`]:
|
||||
'You need to specify at least one value',
|
||||
}));
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
setShowConstraints(false);
|
||||
setStrategyConstraints(constraints);
|
||||
}
|
||||
};
|
||||
|
||||
const renderConstraints = () => {
|
||||
if (constraints.length === 0) {
|
||||
return (
|
||||
<p className={styles.noConstraints}>
|
||||
No constraints configured
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return constraints.map((constraint, index) => {
|
||||
return (
|
||||
<Constraint
|
||||
constraint={constraint}
|
||||
editCallback={() => {
|
||||
setShowConstraints(true);
|
||||
}}
|
||||
deleteCallback={() => {
|
||||
removeConstraint(index);
|
||||
}}
|
||||
key={`${constraint.contextName}-${index}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const removeConstraint = (index: number) => {
|
||||
const updatedConstraints = [...constraints];
|
||||
updatedConstraints.splice(index, 1);
|
||||
|
||||
updateConstraints(updatedConstraints);
|
||||
};
|
||||
|
||||
const closeConstraintDialog = () => {
|
||||
setShowConstraints(false);
|
||||
const filteredConstraints = constraints.filter(constraint => {
|
||||
return constraint.values.length > 0;
|
||||
});
|
||||
updateConstraints(filteredConstraints);
|
||||
};
|
||||
|
||||
const Type = resolveInputType();
|
||||
const definition = resolveStrategyDefinition();
|
||||
|
||||
const { parameters } = strategy;
|
||||
const ON = uiConfig.flags[C];
|
||||
|
||||
return (
|
||||
<div className={styles.accordionContainer}>
|
||||
<ConditionallyRender
|
||||
condition={ON}
|
||||
show={
|
||||
<>
|
||||
<p className={styles.constraintHeader}>Constraints</p>
|
||||
{renderConstraints()}
|
||||
|
||||
<PermissionButton
|
||||
className={styles.addConstraintBtn}
|
||||
onClick={toggleConstraints}
|
||||
variant={'text'}
|
||||
data-test={ADD_CONSTRAINT_ID}
|
||||
permission={[
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
]}
|
||||
environmentId={activeEnvironment.name}
|
||||
projectId={projectId}
|
||||
>
|
||||
+ Add constraints
|
||||
</PermissionButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Dialogue
|
||||
title="Define constraints"
|
||||
open={showConstraints}
|
||||
onClick={saveConstraintsLocally}
|
||||
primaryButtonText="Update constraints"
|
||||
secondaryButtonText="Cancel"
|
||||
onClose={closeConstraintDialog}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
>
|
||||
<StrategyConstraints
|
||||
updateConstraints={updateConstraints}
|
||||
constraints={constraints || []}
|
||||
constraintError={constraintError}
|
||||
setConstraintError={setConstraintError}
|
||||
/>
|
||||
</Dialogue>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasAccess(
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
activeEnvironment.name
|
||||
) ||
|
||||
hasAccess(
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
activeEnvironment.name
|
||||
)
|
||||
}
|
||||
show={
|
||||
<Type
|
||||
parameters={parameters}
|
||||
updateParameter={updateParameters}
|
||||
strategyDefinition={definition}
|
||||
context={context}
|
||||
editable
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureStrategyAccordionBody;
|
||||
|
@ -20,7 +20,7 @@ import { useCommonStyles } from '../../../../../../common.styles';
|
||||
import Dialogue from '../../../../../common/Dialogue';
|
||||
import { trim, modalStyles } from '../../../../../common/util';
|
||||
import PermissionSwitch from '../../../../../common/PermissionSwitch/PermissionSwitch';
|
||||
import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
|
||||
import { UPDATE_FEATURE_VARIANTS } from '../../../../../providers/AccessProvider/permissions';
|
||||
import useFeature from '../../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { IFeatureViewParams } from '../../../../../../interfaces/params';
|
||||
@ -256,7 +256,10 @@ const AddVariant = ({
|
||||
<Grid container>
|
||||
{/* If we're editing, we need to have at least 2 existing variants, since we require at least 1 variable. If adding, we could be adding nr 2, and as such should be allowed to set weightType to variable */}
|
||||
<ConditionallyRender
|
||||
condition={(editing && variants.length > 1) || (!editing && variants.length > 0)}
|
||||
condition={
|
||||
(editing && variants.length > 1) ||
|
||||
(!editing && variants.length > 0)
|
||||
}
|
||||
show={
|
||||
<Grid
|
||||
item
|
||||
@ -267,7 +270,10 @@ const AddVariant = ({
|
||||
<FormControlLabel
|
||||
control={
|
||||
<PermissionSwitch
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={
|
||||
UPDATE_FEATURE_VARIANTS
|
||||
}
|
||||
projectId={projectId}
|
||||
name="weightType"
|
||||
checked={isFixWeight}
|
||||
data-test={
|
||||
|
@ -2,7 +2,14 @@ import classnames from 'classnames';
|
||||
import * as jsonpatch from 'fast-json-patch';
|
||||
|
||||
import styles from './variants.module.scss';
|
||||
import { Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@material-ui/core';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import AddVariant from './AddFeatureVariant/AddFeatureVariant';
|
||||
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
@ -11,7 +18,7 @@ import { useParams } from 'react-router';
|
||||
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||
import AccessContext from '../../../../../contexts/AccessContext';
|
||||
import FeatureVariantListItem from './FeatureVariantsListItem/FeatureVariantsListItem';
|
||||
import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
|
||||
import { UPDATE_FEATURE_VARIANTS } from '../../../../providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../../../common/ConditionallyRender';
|
||||
import useUnleashContext from '../../../../../hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import GeneralSelect from '../../../../common/GeneralSelect/GeneralSelect';
|
||||
@ -31,7 +38,7 @@ const FeatureOverviewVariants = () => {
|
||||
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const { context } = useUnleashContext();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { patchFeatureVariants } = useFeatureApi();
|
||||
const [editVariant, setEditVariant] = useState({});
|
||||
const [showAddVariant, setShowAddVariant] = useState(false);
|
||||
@ -54,7 +61,7 @@ const FeatureOverviewVariants = () => {
|
||||
setStickinessOptions(options);
|
||||
}, [context]);
|
||||
|
||||
const editable = hasAccess(UPDATE_FEATURE, projectId);
|
||||
const editable = hasAccess(UPDATE_FEATURE_VARIANTS, projectId);
|
||||
|
||||
const setClonedVariants = clonedVariants =>
|
||||
setVariants(cloneDeep(clonedVariants));
|
||||
@ -104,7 +111,7 @@ const FeatureOverviewVariants = () => {
|
||||
return (
|
||||
<section style={{ paddingTop: '16px' }}>
|
||||
<GeneralSelect
|
||||
label='Stickiness'
|
||||
label="Stickiness"
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@ -118,9 +125,9 @@ const FeatureOverviewVariants = () => {
|
||||
is used to ensure consistent traffic allocation across
|
||||
variants.{' '}
|
||||
<a
|
||||
href='https://docs.getunleash.io/advanced/toggle_variants'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
href="https://docs.getunleash.io/advanced/toggle_variants"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
@ -144,16 +151,13 @@ const FeatureOverviewVariants = () => {
|
||||
const { variants } = await res.json();
|
||||
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Updated variant',
|
||||
confetti: true,
|
||||
type: 'success',
|
||||
text: 'Successfully updated variant stickiness',
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -162,20 +166,16 @@ const FeatureOverviewVariants = () => {
|
||||
try {
|
||||
await updateVariants(
|
||||
updatedVariants,
|
||||
'Successfully removed variant',
|
||||
'Successfully removed variant'
|
||||
);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
};
|
||||
const updateVariant = async (variant: IFeatureVariant) => {
|
||||
const updatedVariants = cloneDeep(variants);
|
||||
const variantIdxToUpdate = updatedVariants.findIndex(
|
||||
(v: IFeatureVariant) => v.name === variant.name,
|
||||
(v: IFeatureVariant) => v.name === variant.name
|
||||
);
|
||||
updatedVariants[variantIdxToUpdate] = variant;
|
||||
await updateVariants(updatedVariants, 'Successfully updated variant');
|
||||
@ -190,13 +190,13 @@ const FeatureOverviewVariants = () => {
|
||||
|
||||
await updateVariants(
|
||||
[...variants, variant],
|
||||
'Successfully added a variant',
|
||||
'Successfully added a variant'
|
||||
);
|
||||
};
|
||||
|
||||
const updateVariants = async (
|
||||
variants: IFeatureVariant[],
|
||||
successText: string,
|
||||
successText: string
|
||||
) => {
|
||||
const newVariants = updateWeight(variants, 1000);
|
||||
const patch = createPatch(newVariants);
|
||||
@ -208,18 +208,13 @@ const FeatureOverviewVariants = () => {
|
||||
const { variants } = await res.json();
|
||||
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Updated variant',
|
||||
type: 'success',
|
||||
text: successText,
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const validateName = (name: string) => {
|
||||
@ -241,7 +236,7 @@ const FeatureOverviewVariants = () => {
|
||||
removeVariant(delDialog.name);
|
||||
setDelDialog({ name: '', show: false });
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Deleted variant',
|
||||
type: 'success',
|
||||
text: `Successfully deleted variant`,
|
||||
});
|
||||
@ -250,13 +245,12 @@ const FeatureOverviewVariants = () => {
|
||||
});
|
||||
|
||||
const createPatch = (newVariants: IFeatureVariant[]) => {
|
||||
return jsonpatch
|
||||
.compare(feature.variants, newVariants);
|
||||
return jsonpatch.compare(feature.variants, newVariants);
|
||||
};
|
||||
|
||||
return (
|
||||
<section style={{ padding: '16px' }}>
|
||||
<Typography variant='body1'>
|
||||
<Typography variant="body1">
|
||||
Variants allows you to return a variant object if the feature
|
||||
toggle is considered enabled for the current request. When using
|
||||
variants you should use the{' '}
|
||||
@ -298,12 +292,15 @@ const FeatureOverviewVariants = () => {
|
||||
}}
|
||||
className={styles.addVariantButton}
|
||||
data-test={'ADD_VARIANT_BUTTON'}
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={UPDATE_FEATURE_VARIANTS}
|
||||
projectId={projectId}
|
||||
>
|
||||
Add variant
|
||||
</PermissionButton>
|
||||
{renderStickiness()}
|
||||
<ConditionallyRender
|
||||
condition={editable}
|
||||
show={renderStickiness()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AddVariant
|
||||
@ -323,7 +320,6 @@ const FeatureOverviewVariants = () => {
|
||||
title={editing ? 'Edit variant' : 'Add variant'}
|
||||
/>
|
||||
|
||||
{toast}
|
||||
{delDialogueMarkup}
|
||||
</section>
|
||||
);
|
||||
|
@ -8,7 +8,11 @@ import useProject from '../../../hooks/api/getters/useProject/useProject';
|
||||
import useTabs from '../../../hooks/useTabs';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
||||
import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from '../../providers/AccessProvider/permissions';
|
||||
import Dialogue from '../../common/Dialogue';
|
||||
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
||||
import FeatureLog from './FeatureLog/FeatureLog';
|
||||
@ -33,7 +37,7 @@ const FeatureView2 = () => {
|
||||
const [openTagDialog, setOpenTagDialog] = useState(false);
|
||||
const { a11yProps } = useTabs(0);
|
||||
const { archiveFeatureToggle } = useFeatureApi();
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
||||
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
|
||||
@ -49,19 +53,15 @@ const FeatureView2 = () => {
|
||||
try {
|
||||
await archiveFeatureToggle(projectId, featureId);
|
||||
setToastData({
|
||||
text: 'Feature archived',
|
||||
text: 'Your feature toggle has been archived',
|
||||
type: 'success',
|
||||
show: true,
|
||||
title: 'Feature archived',
|
||||
});
|
||||
setShowDelDialog(false);
|
||||
projectRefetch();
|
||||
history.push(`/projects/${projectId}`);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.toString());
|
||||
setShowDelDialog(false);
|
||||
}
|
||||
};
|
||||
@ -152,7 +152,7 @@ const FeatureView2 = () => {
|
||||
|
||||
<div className={styles.actions}>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={CREATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltip="Copy"
|
||||
data-loading
|
||||
@ -162,7 +162,7 @@ const FeatureView2 = () => {
|
||||
<FileCopy />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE}
|
||||
permission={DELETE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltip="Archive feature toggle"
|
||||
data-loading
|
||||
@ -246,8 +246,6 @@ const FeatureView2 = () => {
|
||||
open={openTagDialog}
|
||||
setOpen={setOpenTagDialog}
|
||||
/>
|
||||
|
||||
{toast}
|
||||
</div>
|
||||
}
|
||||
elseShow={renderFeatureNotExist()}
|
||||
|
@ -31,7 +31,7 @@ const CopyFeature = props => {
|
||||
const [newToggleName, setNewToggleName] = useState();
|
||||
const { cloneFeatureToggle } = useFeatureApi();
|
||||
const inputRef = useRef();
|
||||
const { name: copyToggleName, id: projectId } = useParams();
|
||||
const { name: copyToggleName, id: projectId } = useParams();
|
||||
const { feature } = useFeature(projectId, copyToggleName);
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
@ -66,16 +66,15 @@ const CopyFeature = props => {
|
||||
}
|
||||
|
||||
try {
|
||||
await cloneFeatureToggle(
|
||||
projectId,
|
||||
copyToggleName,
|
||||
{ name: newToggleName, replaceGroupId }
|
||||
);
|
||||
await cloneFeatureToggle(projectId, copyToggleName, {
|
||||
name: newToggleName,
|
||||
replaceGroupId,
|
||||
});
|
||||
props.history.push(
|
||||
getTogglePath(projectId, newToggleName, uiConfig.flags.E)
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
setApiError(e);
|
||||
setApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
@ -98,7 +97,11 @@ const CopyFeature = props => {
|
||||
You are about to create a new feature toggle by cloning the
|
||||
configuration of feature toggle
|
||||
<Link
|
||||
to={getTogglePath(projectId, copyToggleName, uiConfig.flags.E)}
|
||||
to={getTogglePath(
|
||||
projectId,
|
||||
copyToggleName,
|
||||
uiConfig.flags.E
|
||||
)}
|
||||
>
|
||||
{copyToggleName}
|
||||
</Link>
|
||||
|
@ -496,10 +496,10 @@ exports[`renders correctly with with variants 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-22 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-23 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-24 PrivateNotchedOutline-legendNotched-25"
|
||||
className="PrivateNotchedOutline-legendLabelled-25 PrivateNotchedOutline-legendNotched-26"
|
||||
>
|
||||
<span>
|
||||
Stickiness
|
||||
|
@ -29,7 +29,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiChip-root makeStyles-chip-20 MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
|
||||
className="MuiChip-root makeStyles-chip-21 MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
title="Feature toggle is active."
|
||||
@ -52,44 +52,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiTypography-root MuiTypography-body1"
|
||||
>
|
||||
another's description
|
||||
<a
|
||||
aria-disabled={false}
|
||||
aria-label="toggle description edit"
|
||||
className="MuiButtonBase-root MuiIconButton-root"
|
||||
href="/#edit"
|
||||
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}
|
||||
>
|
||||
<span
|
||||
className="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
@ -105,18 +67,19 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Feature type
|
||||
</label>
|
||||
<div
|
||||
className="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
|
||||
className="MuiInputBase-root MuiOutlinedInput-root Mui-disabled Mui-disabled MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
aria-haspopup="listbox"
|
||||
className="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
|
||||
className="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input Mui-disabled Mui-disabled MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense Mui-disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseDown={null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tabIndex={null}
|
||||
>
|
||||
Release
|
||||
</div>
|
||||
@ -131,7 +94,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
/>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
|
||||
className="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined Mui-disabled"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@ -141,10 +104,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-22 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-23 PrivateNotchedOutline-legendNotched-24"
|
||||
className="PrivateNotchedOutline-legendLabelled-24 PrivateNotchedOutline-legendNotched-25"
|
||||
>
|
||||
<span>
|
||||
Feature type
|
||||
@ -200,10 +163,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-22 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-23 PrivateNotchedOutline-legendNotched-24"
|
||||
className="PrivateNotchedOutline-legendLabelled-24 PrivateNotchedOutline-legendNotched-25"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -234,8 +197,8 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiSwitch-root"
|
||||
>
|
||||
<span
|
||||
aria-disabled={false}
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-25 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
aria-disabled={true}
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-26 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-disabled-28 Mui-disabled Mui-disabled Mui-disabled"
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -247,15 +210,15 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={null}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span
|
||||
className="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="PrivateSwitchBase-input-28 MuiSwitch-input"
|
||||
disabled={false}
|
||||
className="PrivateSwitchBase-input-29 MuiSwitch-input"
|
||||
disabled={true}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
@ -263,9 +226,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiSwitch-thumb"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="MuiSwitch-track"
|
||||
@ -280,8 +240,8 @@ exports[`renders correctly with one feature 1`] = `
|
||||
featureToggleName="Another"
|
||||
/>
|
||||
<a
|
||||
aria-disabled={false}
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
aria-disabled={true}
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text Mui-disabled Mui-disabled"
|
||||
href="/projects/default/features/Another/strategies/copy"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@ -296,7 +256,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tabIndex={-1}
|
||||
title="Create new feature toggle by cloning configuration"
|
||||
>
|
||||
<span
|
||||
@ -304,9 +264,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Clone
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
<button
|
||||
aria-controls="feature-stale-dropdown"
|
||||
@ -358,13 +315,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
disabled={false}
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text Mui-disabled Mui-disabled"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
@ -382,7 +336,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
tabIndex={-1}
|
||||
title="Archive feature toggle"
|
||||
type="button"
|
||||
>
|
||||
@ -391,15 +345,12 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Archive
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-29 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-30 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -447,7 +398,8 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
className="PrivateTabIndicator-root-31 PrivateTabIndicator-colorPrimary-32 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
@ -477,9 +429,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Metrics
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-2"
|
||||
@ -508,9 +457,6 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Variants
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabpanel-3"
|
||||
@ -539,20 +485,8 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
Log
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-30 PrivateTabIndicator-colorPrimary-31 MuiTabs-indicator"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"width": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -566,7 +500,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
role="tabpanel"
|
||||
>
|
||||
<UpdateStrategiesComponent
|
||||
editable={true}
|
||||
editable={false}
|
||||
featureToggle={
|
||||
Object {
|
||||
"createdAt": "2018-02-04T20:27:52.127Z",
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
import theme from '../../../../themes/main-theme';
|
||||
import { createFakeStore } from '../../../../accessStoreFake';
|
||||
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
||||
import UIProvider from '../../../providers/UIProvider/UIProvider';
|
||||
|
||||
jest.mock('../update-strategies-container', () => ({
|
||||
__esModule: true,
|
||||
@ -73,22 +74,24 @@ test('renders correctly with one feature', () => {
|
||||
<MemoryRouter>
|
||||
<Provider store={createStore(mockReducer, mockStore)}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<ViewFeatureToggleComponent
|
||||
activeTab={'strategies'}
|
||||
featureToggleName="another"
|
||||
features={[feature]}
|
||||
featureToggle={feature}
|
||||
fetchFeatureToggles={jest.fn()}
|
||||
history={{}}
|
||||
user={{ permissions: [] }}
|
||||
featureTags={[]}
|
||||
fetchTags={jest.fn()}
|
||||
untagFeature={jest.fn()}
|
||||
/>
|
||||
</AccessProvider>
|
||||
<UIProvider>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<ViewFeatureToggleComponent
|
||||
activeTab={'strategies'}
|
||||
featureToggleName="another"
|
||||
features={[feature]}
|
||||
featureToggle={feature}
|
||||
fetchFeatureToggles={jest.fn()}
|
||||
history={{}}
|
||||
user={{ permissions: [] }}
|
||||
featureTags={[]}
|
||||
fetchTags={jest.fn()}
|
||||
untagFeature={jest.fn()}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
|
@ -54,7 +54,7 @@ const Header = () => {
|
||||
const filteredMainRoutes = {
|
||||
mainNavRoutes: routes.mainNavRoutes.filter(filterByFlags(flags)),
|
||||
mobileRoutes: routes.mobileRoutes.filter(filterByFlags(flags)),
|
||||
adminRoutes: routes.adminRoutes,
|
||||
adminRoutes: routes.adminRoutes.filter(filterByFlags(flags)),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -147,7 +147,7 @@ const Header = () => {
|
||||
|
||||
<NavigationMenu
|
||||
id="admin-navigation"
|
||||
options={routes.adminRoutes}
|
||||
options={filteredMainRoutes.adminRoutes}
|
||||
anchorEl={anchorEl}
|
||||
handleClose={handleClose}
|
||||
style={{ top: '40px', left: '-125px' }}
|
||||
|
@ -146,6 +146,7 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "C",
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/context",
|
||||
@ -155,6 +156,7 @@ Array [
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "C",
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/context",
|
||||
@ -328,6 +330,24 @@ Array [
|
||||
"title": "Archived Toggles",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "RE",
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"path": "/admin/create-project-role",
|
||||
"title": "Create",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "RE",
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"path": "/admin/roles/:id/edit",
|
||||
"title": "Edit",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
@ -367,6 +387,18 @@ Array [
|
||||
"title": "Single Sign-On",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "RE",
|
||||
"layout": "main",
|
||||
"menu": Object {
|
||||
"adminSettings": true,
|
||||
},
|
||||
"parent": "/admin",
|
||||
"path": "/admin/roles",
|
||||
"title": "Project Roles",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"hidden": false,
|
||||
|
@ -28,7 +28,7 @@ import AdminUsers from '../admin/users';
|
||||
import AdminInvoice from '../admin/invoice';
|
||||
import AdminAuth from '../admin/auth';
|
||||
import Login from '../user/Login/Login';
|
||||
import { P, C, E, EEA } from '../common/flags';
|
||||
import { P, C, E, EEA, RE } from '../common/flags';
|
||||
import NewUser from '../user/NewUser';
|
||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||
@ -40,10 +40,13 @@ import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
|
||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
|
||||
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
|
||||
import ProjectRoles from '../admin/ProjectRolesv1/ProjectRoles';
|
||||
import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles';
|
||||
import CreateProjectRole from '../admin/project-roles/CreateProjectRole/CreateProjectRole';
|
||||
import EditProjectRole from '../admin/project-roles/EditProjectRole/EditProjectRole';
|
||||
|
||||
export const routes = [
|
||||
// Project
|
||||
|
||||
{
|
||||
path: '/projects/create',
|
||||
parent: '/projects',
|
||||
@ -192,6 +195,7 @@ export const routes = [
|
||||
component: CreateContextField,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
flag: C,
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
@ -201,6 +205,7 @@ export const routes = [
|
||||
component: EditContextField,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
flag: C,
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
@ -373,6 +378,24 @@ export const routes = [
|
||||
},
|
||||
|
||||
// Admin
|
||||
{
|
||||
path: '/admin/create-project-role',
|
||||
title: 'Create',
|
||||
component: CreateProjectRole,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
flag: RE,
|
||||
},
|
||||
{
|
||||
path: '/admin/roles/:id/edit',
|
||||
title: 'Edit',
|
||||
component: EditProjectRole,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
flag: RE,
|
||||
},
|
||||
{
|
||||
path: '/admin/api',
|
||||
parent: '/admin',
|
||||
@ -416,8 +439,8 @@ export const routes = [
|
||||
component: ProjectRoles,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
hidden: true,
|
||||
flag: RE,
|
||||
menu: { adminSettings: true },
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
|
@ -25,7 +25,7 @@ const Project = () => {
|
||||
const params = useQueryParams();
|
||||
const { project, error, loading, refetch } = useProject(id);
|
||||
const ref = useLoading(loading);
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData } = useToast();
|
||||
const commonStyles = useCommonStyles();
|
||||
const styles = useStyles();
|
||||
const history = useHistory();
|
||||
@ -80,9 +80,8 @@ const Project = () => {
|
||||
if (created || edited) {
|
||||
const text = created ? 'Project created' : 'Project updated';
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text,
|
||||
title: text,
|
||||
});
|
||||
}
|
||||
|
||||
@ -186,7 +185,6 @@ const Project = () => {
|
||||
</div>
|
||||
</div>
|
||||
{renderTabContent()}
|
||||
{toast}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Card, Menu, MenuItem } from '@material-ui/core';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useStyles } from './ProjectCard.styles';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
|
||||
@ -13,6 +12,7 @@ import { Delete, Edit } from '@material-ui/icons';
|
||||
import { getProjectEditPath } from '../../../utils/route-path-helpers';
|
||||
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
||||
import { UPDATE_PROJECT } from '../../../store/project/actions';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
interface IProjectCardProps {
|
||||
name: string;
|
||||
featureCount: number;
|
||||
@ -20,13 +20,6 @@ interface IProjectCardProps {
|
||||
memberCount: number;
|
||||
id: string;
|
||||
onHover: () => void;
|
||||
setToastData: Dispatch<
|
||||
SetStateAction<{
|
||||
show: boolean;
|
||||
type: string;
|
||||
text: string;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
const ProjectCard = ({
|
||||
@ -36,7 +29,6 @@ const ProjectCard = ({
|
||||
memberCount,
|
||||
onHover,
|
||||
id,
|
||||
setToastData,
|
||||
}: IProjectCardProps) => {
|
||||
const styles = useStyles();
|
||||
const { refetch: refetchProjectOverview } = useProjects();
|
||||
@ -44,6 +36,7 @@ const ProjectCard = ({
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const { deleteProject } = useProjectApi();
|
||||
const history = useHistory();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const handleClick = e => {
|
||||
e.preventDefault();
|
||||
@ -127,18 +120,14 @@ const ProjectCard = ({
|
||||
deleteProject(id)
|
||||
.then(() => {
|
||||
setToastData({
|
||||
show: true,
|
||||
title: 'Deleted project',
|
||||
type: 'success',
|
||||
text: 'Successfully deleted project',
|
||||
});
|
||||
refetchProjectOverview();
|
||||
})
|
||||
.catch(e => {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
setToastApiError(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setShowDelDialog(false);
|
||||
|
@ -28,7 +28,7 @@ interface ProjectEnvironmentListProps {
|
||||
const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
|
||||
// api state
|
||||
const [envs, setEnvs] = useState<IProjectEnvironment>([]);
|
||||
const { toast, setToastData } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const {
|
||||
environments,
|
||||
@ -85,24 +85,20 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
|
||||
return;
|
||||
}
|
||||
setToastData({
|
||||
title: 'One environment must be active',
|
||||
text: 'You must always have at least one active environment per project',
|
||||
type: 'error',
|
||||
show: true,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await addEnvironmentToProject(projectId, env.name);
|
||||
setToastData({
|
||||
text: 'Environment successfully enabled.',
|
||||
title: 'Environment enabled',
|
||||
text: 'Environment successfully enabled. You can now use it to segment strategies in your feature toggles.',
|
||||
type: 'success',
|
||||
show: true,
|
||||
});
|
||||
} catch (error) {
|
||||
setToastData({
|
||||
text: errorMsg(true),
|
||||
type: 'error',
|
||||
show: true,
|
||||
});
|
||||
setToastApiError(errorMsg(true));
|
||||
}
|
||||
}
|
||||
refetch();
|
||||
@ -115,16 +111,12 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
|
||||
setSelectedEnv(undefined);
|
||||
setConfirmName('');
|
||||
setToastData({
|
||||
title: 'Environment disabled',
|
||||
text: 'Environment successfully disabled.',
|
||||
type: 'success',
|
||||
show: true,
|
||||
});
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
text: errorMsg(false),
|
||||
type: 'error',
|
||||
show: true,
|
||||
});
|
||||
setToastApiError(errorMsg(false));
|
||||
}
|
||||
|
||||
refetch();
|
||||
@ -229,8 +221,6 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
|
||||
{toast}
|
||||
</PageContent>
|
||||
</div>
|
||||
);
|
||||
|
@ -18,7 +18,6 @@ import { CREATE_PROJECT } from '../../providers/AccessProvider/permissions';
|
||||
|
||||
import { Add } from '@material-ui/icons';
|
||||
import ApiError from '../../common/ApiError/ApiError';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
type projectMap = {
|
||||
@ -47,7 +46,6 @@ function resolveCreateButtonData(isOss: boolean, hasAccess: boolean) {
|
||||
const ProjectListNew = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const history = useHistory();
|
||||
const { toast, setToastData } = useToast();
|
||||
const styles = useStyles();
|
||||
const { projects, loading, error, refetch } = useProjects();
|
||||
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
||||
@ -103,7 +101,6 @@ const ProjectListNew = () => {
|
||||
health={project?.health}
|
||||
id={project?.id}
|
||||
featureCount={project?.featureCount}
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
@ -122,7 +119,6 @@ const ProjectListNew = () => {
|
||||
memberCount={2}
|
||||
health={95}
|
||||
featureCount={4}
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -157,7 +153,6 @@ const ProjectListNew = () => {
|
||||
elseShow={renderProjects()}
|
||||
/>
|
||||
</div>
|
||||
{toast}
|
||||
</PageContent>
|
||||
</div>
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ import { FormEvent } from 'react-router/node_modules/@types/react';
|
||||
import useLoading from '../../hooks/useLoading';
|
||||
import PermissionButton from '../common/PermissionButton/PermissionButton';
|
||||
import { UPDATE_PROJECT } from '../../store/project/actions';
|
||||
import useUser from '../../hooks/api/getters/useUser/useUser';
|
||||
|
||||
interface ProjectFormComponentProps {
|
||||
editMode: boolean;
|
||||
@ -28,7 +29,7 @@ interface ProjectFormComponentProps {
|
||||
|
||||
const ProjectFormComponent = (props: ProjectFormComponentProps) => {
|
||||
const { editMode } = props;
|
||||
|
||||
const { refetch } = useUser();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [project, setProject] = useState(props.project || {});
|
||||
const [errors, setErrors] = useState<any>({});
|
||||
@ -97,6 +98,7 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
|
||||
if (valid) {
|
||||
const query = editMode ? 'edited=true' : 'created=true';
|
||||
await props.submit(project);
|
||||
refetch();
|
||||
props.history.push(`/projects/${project.id}?${query}`);
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { ADMIN } from './permissions';
|
||||
|
||||
// TODO: Type up redux store
|
||||
@ -10,10 +11,12 @@ interface IAccessProvider {
|
||||
|
||||
interface IPermission {
|
||||
permission: string;
|
||||
project: string | null;
|
||||
project?: string | null;
|
||||
environment: string | null;
|
||||
}
|
||||
|
||||
const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
||||
const { permissions } = useUser();
|
||||
const isAdminHigherOrder = () => {
|
||||
let called = false;
|
||||
let result = false;
|
||||
@ -33,19 +36,37 @@ const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
||||
|
||||
const isAdmin = isAdminHigherOrder();
|
||||
|
||||
const hasAccess = (permission: string, project: string) => {
|
||||
const permissions = store.getState().user.get('permissions') || [];
|
||||
|
||||
const hasAccess = (
|
||||
permission: string,
|
||||
project: string,
|
||||
environment?: string
|
||||
) => {
|
||||
const result = permissions.some((p: IPermission) => {
|
||||
if (p.permission === ADMIN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p.permission === permission && p.project === project) {
|
||||
if (
|
||||
p.permission === permission &&
|
||||
(p.project === project || p.project === '*') &&
|
||||
(p.environment === environment || p.environment === '*')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (p.permission === permission && project === undefined) {
|
||||
if (
|
||||
p.permission === permission &&
|
||||
(p.project === project || p.project === '*') &&
|
||||
p.environment === null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
p.permission === permission &&
|
||||
p.project === undefined &&
|
||||
p.environment === null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -26,3 +26,9 @@ export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
||||
export const DELETE_API_TOKEN = 'DELETE_API_TOKEN';
|
||||
export const DELETE_ENVIRONMENT = 'DELETE_ENVIRONMENT';
|
||||
export const UPDATE_ENVIRONMENT = 'UPDATE_ENVIRONMENT';
|
||||
export const CREATE_FEATURE_STRATEGY = 'CREATE_FEATURE_STRATEGY';
|
||||
export const UPDATE_FEATURE_STRATEGY = 'UPDATE_FEATURE_STRATEGY';
|
||||
export const DELETE_FEATURE_STRATEGY = 'DELETE_FEATURE_STRATEGY';
|
||||
export const UPDATE_FEATURE_ENVIRONMENT = 'UPDATE_FEATURE_ENVIRONMENT';
|
||||
export const UPDATE_FEATURE_VARIANTS = 'UPDATE_FEATURE_VARIANTS';
|
||||
export const MOVE_FEATURE_TOGGLE = 'MOVE_FEATURE_TOGGLE';
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { USER_CACHE_KEY } from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { mutate, SWRConfig, useSWRConfig } from 'swr';
|
||||
import { useHistory } from 'react-router';
|
||||
import { IToast } from '../../../hooks/useToast';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
|
||||
interface ISWRProviderProps {
|
||||
setToastData: (toastData: IToast) => void;
|
||||
setShowLoader: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isUnauthorized: () => boolean;
|
||||
}
|
||||
@ -13,16 +12,15 @@ const INVALID_TOKEN_ERROR = 'InvalidTokenError';
|
||||
|
||||
const SWRProvider: React.FC<ISWRProviderProps> = ({
|
||||
children,
|
||||
setToastData,
|
||||
isUnauthorized,
|
||||
setShowLoader,
|
||||
}) => {
|
||||
const { cache } = useSWRConfig();
|
||||
const history = useHistory();
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
const handleFetchError = error => {
|
||||
setShowLoader(false);
|
||||
console.log(error.info.name);
|
||||
if (error.status === 401) {
|
||||
cache.clear();
|
||||
const path = location.pathname;
|
||||
@ -40,11 +38,7 @@ const SWRProvider: React.FC<ISWRProviderProps> = ({
|
||||
}
|
||||
|
||||
if (!isUnauthorized()) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: error.message,
|
||||
});
|
||||
setToastApiError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
26
frontend/src/component/providers/UIProvider/UIProvider.tsx
Normal file
26
frontend/src/component/providers/UIProvider/UIProvider.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import UIContext, { IToastData } from '../../../contexts/UIContext';
|
||||
|
||||
const UIProvider: React.FC = ({ children }) => {
|
||||
const [toastData, setToast] = useState<IToastData>({
|
||||
title: '',
|
||||
text: '',
|
||||
components: [],
|
||||
show: false,
|
||||
persist: false,
|
||||
type: '',
|
||||
});
|
||||
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
setToast,
|
||||
toastData,
|
||||
}),
|
||||
[toastData]
|
||||
);
|
||||
|
||||
return <UIContext.Provider value={context}>{children}</UIContext.Provider>;
|
||||
};
|
||||
|
||||
export default UIProvider;
|
@ -162,51 +162,7 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-headerActions-9"
|
||||
>
|
||||
<span
|
||||
aria-describedby={null}
|
||||
className=""
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
title="Add new strategy"
|
||||
>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||
data-test="ADD_NEW_STRATEGY_ID"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Add new strategy
|
||||
<span
|
||||
className="MuiButton-endIcon MuiButton-iconSizeMedium"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -269,11 +225,11 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
title="Deprecate activation strategy"
|
||||
title="You don't have access to perform this operation"
|
||||
>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiIconButton-root"
|
||||
disabled={false}
|
||||
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
@ -286,7 +242,7 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
@ -303,56 +259,9 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby={null}
|
||||
className=""
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
title="You cannot delete a built-in strategy"
|
||||
>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -43,60 +43,9 @@ exports[`it supports editMode 1`] = `
|
||||
className="addTagTypeForm contentSpacing"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="formButtons"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Update
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="cancel"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
You do not have permissions to save.
|
||||
</span>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
@ -146,60 +95,9 @@ exports[`renders correctly for creating 1`] = `
|
||||
className="addTagTypeForm contentSpacing"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="formButtons"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Create
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="cancel"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
You do not have permissions to save.
|
||||
</span>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -28,35 +28,7 @@ exports[`renders a list with elements correctly 1`] = `
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-headerActions-7"
|
||||
>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Add new tag type
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -104,45 +76,6 @@ exports[`renders a list with elements correctly 1`] = `
|
||||
Some simple description
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-describedby={null}
|
||||
className="MuiButtonBase-root MuiIconButton-root"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
title="Delete simple"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiIconButton-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -177,32 +110,7 @@ exports[`renders an empty list correctly 1`] = `
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-headerActions-7"
|
||||
>
|
||||
<button
|
||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiButton-label"
|
||||
>
|
||||
Add new tag type
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -35,7 +35,6 @@ const Authentication = ({
|
||||
const params = useQueryParams();
|
||||
|
||||
const error = params.get('errorMsg');
|
||||
|
||||
if (!authDetails) return null;
|
||||
|
||||
let content;
|
||||
|
1
frontend/src/constants/flags.ts
Normal file
1
frontend/src/constants/flags.ts
Normal file
@ -0,0 +1 @@
|
||||
export const RBAC_ENV = false;
|
19
frontend/src/contexts/UIContext.ts
Normal file
19
frontend/src/contexts/UIContext.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface IToastData {
|
||||
title: string;
|
||||
text: string;
|
||||
components?: JSX.Element[];
|
||||
show: boolean;
|
||||
persist: boolean;
|
||||
confetti?: boolean;
|
||||
type: string;
|
||||
}
|
||||
interface IFeatureStrategiesUIContext {
|
||||
toastData: IToastData;
|
||||
setToast: React.Dispatch<React.SetStateAction<IToastData>>;
|
||||
}
|
||||
|
||||
const UIContext = React.createContext<IFeatureStrategiesUIContext | null>(null);
|
||||
|
||||
export default UIContext;
|
@ -57,11 +57,12 @@ const useAPI = ({
|
||||
const makeRequest = async (
|
||||
apiCaller: any,
|
||||
requestId?: string,
|
||||
loading: boolean = true
|
||||
loadingOn: boolean = true
|
||||
): Promise<Response> => {
|
||||
if (loading) {
|
||||
if (loadingOn) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiCaller();
|
||||
setLoading(false);
|
||||
|
@ -0,0 +1,88 @@
|
||||
import { IPermission } from '../../../../interfaces/project';
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
interface ICreateRolePayload {
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
const useProjectRolesApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const createRole = async (payload: ICreateRolePayload) => {
|
||||
const path = `api/admin/roles`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const editRole = async (id: string, payload: ICreateRolePayload) => {
|
||||
const path = `api/admin/roles/${id}`;
|
||||
const req = createRequest(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const validateRole = async (payload: ICreateRolePayload) => {
|
||||
const path = `api/admin/roles/validate`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRole = async (id: number) => {
|
||||
const path = `api/admin/roles/${id}`;
|
||||
const req = createRequest(path, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createRole,
|
||||
deleteRole,
|
||||
editRole,
|
||||
validateRole,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectRolesApi;
|
@ -0,0 +1,35 @@
|
||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
const useProjectRole = (id: string, options: SWRConfiguration = {}) => {
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(`api/admin/roles/${id}`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(handleErrorResponses('project role'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
const { data, error } = useSWR(`api/admin/roles/${id}`, fetcher, options);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(`api/admin/roles/${id}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
role: data ? data : {},
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectRole;
|
@ -0,0 +1,61 @@
|
||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
|
||||
import {
|
||||
IProjectEnvironmentPermissions,
|
||||
IProjectRolePermissions,
|
||||
IPermission,
|
||||
} from '../../../../interfaces/project';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
interface IUseProjectRolePermissions {
|
||||
permissions:
|
||||
| IProjectRolePermissions
|
||||
| {
|
||||
project: IPermission[];
|
||||
environments: IProjectEnvironmentPermissions[];
|
||||
};
|
||||
loading: boolean;
|
||||
refetch: () => void;
|
||||
error: any;
|
||||
}
|
||||
|
||||
const useProjectRolePermissions = (
|
||||
options: SWRConfiguration = {}
|
||||
): IUseProjectRolePermissions => {
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(`api/admin/permissions`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(handleErrorResponses('Project permissions'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
const KEY = `api/admin/permissions`;
|
||||
|
||||
const { data, error } = useSWR<{ permissions: IProjectRolePermissions }>(
|
||||
KEY,
|
||||
fetcher,
|
||||
options
|
||||
);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(KEY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
permissions: data?.permissions || { project: [], environments: [] },
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectRolePermissions;
|
@ -12,6 +12,7 @@ const useUser = (
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 15000,
|
||||
}
|
||||
) => {
|
||||
const fetcher = () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import Toast from '../component/common/Toast/Toast';
|
||||
import { Dispatch, SetStateAction, useContext } from 'react';
|
||||
import UIContext, { IToastData } from '../contexts/UIContext';
|
||||
|
||||
export interface IToast {
|
||||
show: boolean;
|
||||
@ -9,26 +9,44 @@ export interface IToast {
|
||||
|
||||
export type TSetToastData = Dispatch<SetStateAction<IToast>>;
|
||||
|
||||
interface IToastOptions {
|
||||
title: string;
|
||||
text?: string;
|
||||
type: string;
|
||||
persist?: boolean;
|
||||
confetti?: boolean;
|
||||
}
|
||||
|
||||
const useToast = () => {
|
||||
const [toastData, setToastData] = useState<IToast>({
|
||||
show: false,
|
||||
type: 'success',
|
||||
text: '',
|
||||
});
|
||||
// @ts-ignore
|
||||
const { setToast } = useContext(UIContext);
|
||||
|
||||
const hideToast = () => {
|
||||
setToastData((prev: IToast) => ({ ...prev, show: false }));
|
||||
const hideToast = () =>
|
||||
setToast((prev: IToastData) => ({
|
||||
...prev,
|
||||
show: false,
|
||||
}));
|
||||
|
||||
const setToastApiError = (errorText: string, overrides?: IToastOptions) => {
|
||||
setToast({
|
||||
title: 'Something went wrong',
|
||||
text: `We had trouble talking to our API. Here's why: ${errorText}`,
|
||||
type: 'error',
|
||||
show: true,
|
||||
autoHideDuration: 6000,
|
||||
...overrides,
|
||||
});
|
||||
};
|
||||
const toast = (
|
||||
<Toast
|
||||
show={toastData.show}
|
||||
onClose={hideToast}
|
||||
text={toastData.text}
|
||||
type={toastData.type}
|
||||
/>
|
||||
);
|
||||
|
||||
return { toast, setToastData, hideToast };
|
||||
const setToastData = (options: IToastOptions) => {
|
||||
if (options.persist) {
|
||||
setToast({ ...options, show: true });
|
||||
} else {
|
||||
setToast({ ...options, show: true, autoHideDuration: 6000 });
|
||||
}
|
||||
};
|
||||
|
||||
return { setToastData, setToastApiError, hideToast };
|
||||
};
|
||||
|
||||
export default useToast;
|
||||
|
@ -20,6 +20,7 @@ import ScrollToTop from './component/scroll-to-top';
|
||||
import { writeWarning } from './security-logger';
|
||||
import AccessProvider from './component/providers/AccessProvider/AccessProvider';
|
||||
import { getBasePath } from './utils/format-path';
|
||||
import UIProvider from './component/providers/UIProvider/UIProvider';
|
||||
|
||||
let composeEnhancers;
|
||||
|
||||
@ -41,18 +42,20 @@ const unleashStore = createStore(
|
||||
ReactDOM.render(
|
||||
<Provider store={unleashStore}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<AccessProvider store={unleashStore}>
|
||||
<Router basename={`${getBasePath()}`}>
|
||||
<ThemeProvider theme={mainTheme}>
|
||||
<StylesProvider injectFirst>
|
||||
<CssBaseline />
|
||||
<ScrollToTop>
|
||||
<Route path="/" component={App} />
|
||||
</ScrollToTop>
|
||||
</StylesProvider>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
</AccessProvider>
|
||||
<UIProvider>
|
||||
<AccessProvider store={unleashStore}>
|
||||
<Router basename={`${getBasePath()}`}>
|
||||
<ThemeProvider theme={mainTheme}>
|
||||
<StylesProvider injectFirst>
|
||||
<CssBaseline />
|
||||
<ScrollToTop>
|
||||
<Route path="/" component={App} />
|
||||
</ScrollToTop>
|
||||
</StylesProvider>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
</DndProvider>
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
|
@ -26,3 +26,20 @@ export interface IProjectHealthReport extends IProject {
|
||||
activeCount: number;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface IPermission {
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface IProjectRolePermissions {
|
||||
project: IPermission[];
|
||||
environments: IProjectEnvironmentPermissions[];
|
||||
}
|
||||
|
||||
export interface IProjectEnvironmentPermissions {
|
||||
name: string;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
@ -6,4 +6,10 @@ interface IRole {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IProjectRole {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default IRole;
|
||||
|
@ -13,6 +13,7 @@ export interface ISplash {
|
||||
export interface IPermission {
|
||||
permission: string;
|
||||
project: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface IAuthDetails {
|
||||
|
Loading…
Reference in New Issue
Block a user