1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: RBAC environment role list (#558)

* fix: move admin to components and add ProjectRoles route

* feat: fetch project roles and create project roles list

* fix: add pagination and update tests

* update projectRoles folder name

Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2021-12-14 10:36:19 +01:00 committed by GitHub
parent 76c1363aaa
commit 5de56256e1
47 changed files with 349 additions and 66 deletions

View File

@ -0,0 +1,10 @@
import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles(theme => ({
rolesListBody: {
paddingBottom: '4rem',
minHeight: '50vh',
position: 'relative',
},
}));

View File

@ -0,0 +1,63 @@
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 { useStyles } from './ProjectRoles.styles';
import RolesList from './RolesList';
const ProjectRoles = () => {
const { hasAccess } = useContext(AccessContext);
const styles = useStyles();
const history = useHistory();
return (
<div>
<AdminMenu history={history} />
<PageContent
bodyClass={styles.rolesListBody}
headerContent={
<HeaderTitle
title="Project Roles"
actions={
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={
<Button
variant="contained"
color="primary"
onClick={() => console.log('hi')}
>
New Project role
</Button>
}
elseShow={
<small>
PS! Only admins can add/remove roles.
</small>
}
/>
}
/>
}
>
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={<RolesList location={location} />}
elseShow={
<Alert severity="error">
You need instance admin to access this section.
</Alert>
}
/>
</PageContent>
</div>
);
};
export default ProjectRoles;

View File

@ -0,0 +1,63 @@
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;

View File

@ -0,0 +1,13 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
tableRow: {
'&:hover': {
backgroundColor: theme.palette.grey[200],
},
},
leftTableCell:{
textAlign: 'left',
maxWidth: '300px'
}
}));

View File

@ -0,0 +1,61 @@
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;

View File

@ -19,6 +19,8 @@ const activeNavLinkStyle = {
};
function AdminMenu({ history }) {
const SHOW_PROJECT_ROLES = false;
const { location } = history;
const { pathname } = location;
return (
@ -36,6 +38,21 @@ function AdminMenu({ history }) {
</NavLink>
}
></Tab>
{SHOW_PROJECT_ROLES && (
<Tab
value="/admin/roles"
label={
<NavLink
to="/admin/roles"
activeStyle={activeNavLinkStyle}
style={navLinkStyle}
>
<span>PROJECT ROLES</span>
</NavLink>
}
></Tab>
)}
<Tab
value="/admin/api"
label={
@ -56,7 +73,7 @@ function AdminMenu({ history }) {
activeStyle={activeNavLinkStyle}
style={navLinkStyle}
>
Single Sign-On
Single Sign-On
</NavLink>
}
></Tab>

View File

@ -1,14 +1,14 @@
import { TextField } from '@material-ui/core';
import classNames from 'classnames';
import React, { useState, useEffect } from 'react';
import { styles as commonStyles } from '../../../component/common';
import { IApiTokenCreate } from '../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import ConditionallyRender from '../../common/ConditionallyRender';
import Dialogue from '../../common/Dialogue';
import GeneralSelect from '../../common/GeneralSelect/GeneralSelect';
import { styles as commonStyles } from '../../../common';
import { IApiTokenCreate } from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useEnvironments from '../../../../hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from '../../../../hooks/api/getters/useProjects/useProjects';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import ConditionallyRender from '../../../common/ConditionallyRender';
import Dialogue from '../../../common/Dialogue';
import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect';
import { useStyles } from './styles';

View File

@ -9,29 +9,29 @@ import {
TableHead,
TableRow,
} from '@material-ui/core';
import AccessContext from '../../../contexts/AccessContext';
import useToast from '../../../hooks/useToast';
import useLoading from '../../../hooks/useLoading';
import useApiTokens from '../../../hooks/api/getters/useApiTokens/useApiTokens';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import AccessContext from '../../../../contexts/AccessContext';
import useToast from '../../../../hooks/useToast';
import useLoading from '../../../../hooks/useLoading';
import useApiTokens from '../../../../hooks/api/getters/useApiTokens/useApiTokens';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useApiTokensApi, {
IApiTokenCreate,
} from '../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import ApiError from '../../common/ApiError/ApiError';
import PageContent from '../../common/PageContent';
import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender';
} from '../../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import ApiError from '../../../common/ApiError/ApiError';
import PageContent from '../../../common/PageContent';
import HeaderTitle from '../../../common/HeaderTitle';
import ConditionallyRender from '../../../common/ConditionallyRender';
import {
CREATE_API_TOKEN,
DELETE_API_TOKEN,
} from '../../providers/AccessProvider/permissions';
} from '../../../providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenList.styles';
import { formatDateWithLocale } from '../../common/util';
import { formatDateWithLocale } from '../../../common/util';
import Secret from './secret';
import { Delete, FileCopy } from '@material-ui/icons';
import ApiTokenCreate from '../ApiTokenCreate/ApiTokenCreate';
import Dialogue from '../../common/Dialogue';
import { CREATE_API_TOKEN_BUTTON } from '../../../testIds';
import Dialogue from '../../../common/Dialogue';
import { CREATE_API_TOKEN_BUTTON } from '../../../../testIds';
import { Alert } from '@material-ui/lab';
import copy from 'copy-to-clipboard';

View File

@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import ApiTokenList from '../../../component/api-token/ApiTokenList/ApiTokenList';
import ApiTokenList from '../api-token/ApiTokenList/ApiTokenList';
import AdminMenu from '../admin-menu';
import usePermissions from '../../../hooks/usePermissions';
import ConditionallyRender from '../../../component/common/ConditionallyRender';
import ConditionallyRender from '../../common/ConditionallyRender';
const ApiPage = ({ history, location }) => {
const { isAdmin } = usePermissions();

View File

@ -5,9 +5,9 @@ import { Alert } from '@material-ui/lab';
import GoogleAuth from './google-auth-container';
import SamlAuth from './saml-auth-container';
import OidcAuth from './oidc-auth-container';
import TabNav from '../../../component/common/TabNav/TabNav';
import PageContent from '../../../component/common/PageContent/PageContent';
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
import TabNav from '../../common/TabNav/TabNav';
import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
function AdminAuthPage({ authenticationType, history }) {
const tabs = [

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import GoogleAuth from './google-auth';
import { getGoogleConfig, updateGoogleConfig } from './../../../store/e-admin-auth/actions';
import { getGoogleConfig, updateGoogleConfig } from '../../../store/e-admin-auth/actions';
const mapStateToProps = state => ({
config: state.authAdmin.get('google'),

View File

@ -8,9 +8,9 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import { ADMIN } from '../../providers/AccessProvider/permissions';
const initialState = {
enabled: false,

View File

@ -8,9 +8,9 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
const initialState = {

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import SamlAuth from './saml-auth';
import { getSamlConfig, updateSamlConfig } from './../../../store/e-admin-auth/actions';
import { getSamlConfig, updateSamlConfig } from '../../../store/e-admin-auth/actions';
const mapStateToProps = state => ({
config: state.authAdmin.get('saml'),

View File

@ -8,9 +8,9 @@ import {
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
const initialState = {

View File

@ -2,8 +2,8 @@ import { useContext } from 'react';
import PropTypes from 'prop-types';
import InvoiceList from './invoice-container';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import ConditionallyRender from '../../../component/common/ConditionallyRender';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab';
const InvoiceAdminPage = ({ history }) => {

View File

@ -9,10 +9,10 @@ import {
Button,
} from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { formatDateWithLocale } from '../../../component/common/util';
import PageContent from '../../../component/common/PageContent';
import HeaderTitle from '../../../component/common/HeaderTitle';
import ConditionallyRender from '../../../component/common/ConditionallyRender';
import { formatDateWithLocale } from '../../common/util';
import PageContent from '../../common/PageContent';
import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender';
import { formatApiPath } from '../../../utils/format-path';
const PORTAL_URL = formatApiPath('api/admin/invoices/portal');

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import Dialogue from '../../../../component/common/Dialogue';
import Dialogue from '../../../common/Dialogue';
import { IUserApiErrors } from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import IRole from '../../../../interfaces/role';

View File

@ -11,9 +11,9 @@ import {
Typography,
} from '@material-ui/core';
import { trim } from '../../../../../component/common/util';
import { trim } from '../../../../common/util';
import { useCommonStyles } from '../../../../../common.styles';
import ConditionallyRender from '../../../../../component/common/ConditionallyRender';
import ConditionallyRender from '../../../../common/ConditionallyRender';
import { useStyles } from './AddUserForm.styles';
import useLoading from '../../../../../hooks/useLoading';
import {

View File

@ -1,5 +1,5 @@
import { Typography } from '@material-ui/core';
import Dialogue from '../../../../../component/common/Dialogue';
import Dialogue from '../../../../common/Dialogue';
import { ReactComponent as EmailIcon } from '../../../../../assets/icons/email.svg';
import { useStyles } from './ConfirmUserEmail.styles';

View File

@ -1,7 +1,7 @@
import { Typography } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { useCommonStyles } from '../../../../../common.styles';
import Dialogue from '../../../../../component/common/Dialogue';
import Dialogue from '../../../../common/Dialogue';
import UserInviteLink from './UserInviteLink/UserInviteLink';
interface IConfirmUserLink {

View File

@ -7,9 +7,9 @@ import {
} from '@material-ui/core';
import { Edit, Lock, Delete } from '@material-ui/icons';
import { SyntheticEvent, useContext } from 'react';
import { ADMIN } from '../../../../../component/providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../../component/common/ConditionallyRender';
import { formatDateWithLocale } from '../../../../../component/common/util';
import { ADMIN } from '../../../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../common/ConditionallyRender';
import { formatDateWithLocale } from '../../../../common/util';
import AccessContext from '../../../../../contexts/AccessContext';
import { IUser } from '../../../../../interfaces/user';
import { useStyles } from './UserListItem.styles';

View File

@ -12,9 +12,9 @@ import AddUser from '../AddUser/AddUser';
import ChangePassword from '../change-password-component';
import UpdateUser from '../update-user-component';
import DelUser from '../del-user-component';
import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../../component/providers/AccessProvider/permissions';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
@ -22,7 +22,7 @@ import UserListItem from './UserListItem/UserListItem';
import loadingData from './loadingData';
import useLoading from '../../../../hooks/useLoading';
import usePagination from '../../../../hooks/usePagination';
import PaginateUI from '../../../../component/common/PaginateUI/PaginateUI';
import PaginateUI from '../../../common/PaginateUI/PaginateUI';
function UsersList({ location, closeDialog, showDialog }) {
const { users, roles, refetch, loading } = useUsers();

View File

@ -2,13 +2,13 @@ import { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { TextField, Typography, Avatar } from '@material-ui/core';
import { trim } from '../../../component/common/util';
import { trim } from '../../common/util';
import { modalStyles } from './util';
import Dialogue from '../../../component/common/Dialogue/Dialogue';
import PasswordChecker from '../../../component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import Dialogue from '../../common/Dialogue/Dialogue';
import PasswordChecker from '../../user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import { useCommonStyles } from '../../../common.styles';
import PasswordMatcher from '../../../component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import ConditionallyRender from '../../../component/common/ConditionallyRender';
import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab';
function ChangePassword({

View File

@ -1,6 +1,6 @@
import React from 'react';
import Dialogue from '../../../component/common/Dialogue/Dialogue';
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
import Dialogue from '../../common/Dialogue/Dialogue';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import propTypes from 'prop-types';
import { REMOVE_USER_ERROR } from '../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import { Alert } from '@material-ui/lab';

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Dialogue from '../../../component/common/Dialogue';
import Dialogue from '../../common/Dialogue';
import UserForm from './AddUser/AddUserForm/AddUserForm';
function AddUser({

View File

@ -376,6 +376,17 @@ Array [
"title": "Single Sign-On",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"menu": Object {
"adminSettings": true,
},
"parent": "/admin",
"path": "/admin/roles",
"title": "Project Roles",
"type": "protected",
},
Object {
"component": [Function],
"hidden": false,

View File

@ -23,11 +23,11 @@ import CreateTag from '../../page/tags/create';
import Addons from '../../page/addons';
import AddonsCreate from '../../page/addons/create';
import AddonsEdit from '../../page/addons/edit';
import Admin from '../../page/admin';
import AdminApi from '../../page/admin/api';
import AdminUsers from '../../page/admin/users';
import AdminInvoice from '../../page/admin/invoice';
import AdminAuth from '../../page/admin/auth';
import Admin from '../admin';
import AdminApi from '../admin/api';
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 NewUser from '../user/NewUser';
@ -41,6 +41,7 @@ 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';
export const routes = [
// Project
@ -418,6 +419,15 @@ export const routes = [
layout: 'main',
menu: { adminSettings: true },
},
{
path: '/admin/roles',
parent: '/admin',
title: 'Project Roles',
component: ProjectRoles,
type: 'protected',
layout: 'main',
menu: { adminSettings: true },
},
{
path: '/admin',
title: 'Admin',

View File

@ -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 useProjectRoles = (options: SWRConfiguration = {}) => {
const fetcher = () => {
const path = formatApiPath(`api/admin/roles`);
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('project roles'))
.then(res => res.json());
};
const { data, error } = useSWR(`api/admin/roles`, fetcher, options);
const [loading, setLoading] = useState(!error && !data);
const refetch = () => {
mutate(`api/admin/roles`);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return {
roles: data?.roles || [],
error,
loading,
refetch,
};
};
export default useProjectRoles;