mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
feat: add usage info to project role deletion dialog (#4464)
## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> Adds projects user and group -usage information to the dialog shown when user wants to delete a project role <img width="670" alt="Skjermbilde 2023-08-10 kl 08 28 40" src="https://github.com/Unleash/unleash/assets/707867/a1df961b-2d0f-419d-b9bf-fedef896a84e"> --------- Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
parent
da7829daca
commit
76d3cc59cf
@ -1,21 +1,7 @@
|
|||||||
import { Alert, styled } from '@mui/material';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
|
||||||
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
|
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
|
||||||
import { IRole } from 'interfaces/role';
|
import { IRole } from 'interfaces/role';
|
||||||
import { RoleDeleteDialogUsers } from './RoleDeleteDialogUsers/RoleDeleteDialogUsers';
|
import { RoleDeleteDialogRootRole } from './RoleDeleteDialogRootRole/RoleDeleteDialogRootRole';
|
||||||
import { RoleDeleteDialogServiceAccounts } from './RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts';
|
import { RoleDeleteDialogProjectRole } from './RoleDeleteDialogProjectRole/RoleDeleteDialogProjectRole';
|
||||||
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
import { CUSTOM_PROJECT_ROLE_TYPE } from 'constants/roles';
|
||||||
import { RoleDeleteDialogGroups } from './RoleDeleteDialogGroups/RoleDeleteDialogGroups';
|
|
||||||
|
|
||||||
const StyledTableContainer = styled('div')(({ theme }) => ({
|
|
||||||
marginTop: theme.spacing(1.5),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledLabel = styled('p')(({ theme }) => ({
|
|
||||||
marginTop: theme.spacing(3),
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IRoleDeleteDialogProps {
|
interface IRoleDeleteDialogProps {
|
||||||
role?: IRole;
|
role?: IRole;
|
||||||
@ -30,98 +16,23 @@ export const RoleDeleteDialog = ({
|
|||||||
setOpen,
|
setOpen,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: IRoleDeleteDialogProps) => {
|
}: IRoleDeleteDialogProps) => {
|
||||||
const { users } = useUsers();
|
if (role?.type === CUSTOM_PROJECT_ROLE_TYPE) {
|
||||||
const { serviceAccounts } = useServiceAccounts();
|
return (
|
||||||
const { groups } = useGroups();
|
<RoleDeleteDialogProjectRole
|
||||||
|
role={role}
|
||||||
const roleUsers = users.filter(({ rootRole }) => rootRole === role?.id);
|
open={open}
|
||||||
const roleServiceAccounts = serviceAccounts.filter(
|
setOpen={setOpen}
|
||||||
({ rootRole }) => rootRole === role?.id
|
onConfirm={onConfirm}
|
||||||
);
|
/>
|
||||||
const roleGroups = groups?.filter(({ rootRole }) => rootRole === role?.id);
|
|
||||||
|
|
||||||
const entitiesWithRole = Boolean(
|
|
||||||
roleUsers.length || roleServiceAccounts.length || roleGroups?.length
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<RoleDeleteDialogRootRole
|
||||||
title="Delete role?"
|
role={role}
|
||||||
open={open}
|
open={open}
|
||||||
primaryButtonText="Delete role"
|
setOpen={setOpen}
|
||||||
secondaryButtonText="Cancel"
|
onConfirm={onConfirm}
|
||||||
disabledPrimaryButton={entitiesWithRole}
|
|
||||||
onClick={() => onConfirm(role!)}
|
|
||||||
onClose={() => {
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={entitiesWithRole}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<Alert severity="error">
|
|
||||||
You are not allowed to delete a role that is
|
|
||||||
currently in use. Please change the role of the
|
|
||||||
following entities first:
|
|
||||||
</Alert>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(roleUsers.length)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<StyledLabel>
|
|
||||||
Users ({roleUsers.length}):
|
|
||||||
</StyledLabel>
|
|
||||||
<StyledTableContainer>
|
|
||||||
<RoleDeleteDialogUsers
|
|
||||||
users={roleUsers}
|
|
||||||
/>
|
/>
|
||||||
</StyledTableContainer>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(roleServiceAccounts.length)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<StyledLabel>
|
|
||||||
Service accounts (
|
|
||||||
{roleServiceAccounts.length}):
|
|
||||||
</StyledLabel>
|
|
||||||
<StyledTableContainer>
|
|
||||||
<RoleDeleteDialogServiceAccounts
|
|
||||||
serviceAccounts={
|
|
||||||
roleServiceAccounts
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledTableContainer>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(roleGroups?.length)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<StyledLabel>
|
|
||||||
Groups ({roleGroups?.length}):
|
|
||||||
</StyledLabel>
|
|
||||||
<StyledTableContainer>
|
|
||||||
<RoleDeleteDialogGroups
|
|
||||||
groups={roleGroups!}
|
|
||||||
/>
|
|
||||||
</StyledTableContainer>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<p>
|
|
||||||
You are about to delete role:{' '}
|
|
||||||
<strong>{role?.name}</strong>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Dialogue>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
import { Alert, styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import { IRole } from 'interfaces/role';
|
||||||
|
import { useProjectRoleAccessUsage } from 'hooks/api/getters/useProjectRoleAccessUsage/useProjectRoleAccessUsage';
|
||||||
|
import { RoleDeleteDialogProjectRoleTable } from './RoleDeleteDialogProjectRoleTable';
|
||||||
|
|
||||||
|
const StyledTableContainer = styled('div')(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledLabel = styled('p')(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IRoleDeleteDialogProps {
|
||||||
|
role?: IRole;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
onConfirm: (role: IRole) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoleDeleteDialogProjectRole = ({
|
||||||
|
role,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm,
|
||||||
|
}: IRoleDeleteDialogProps) => {
|
||||||
|
const { projects } = useProjectRoleAccessUsage(role?.id);
|
||||||
|
|
||||||
|
const entitiesWithRole = Boolean(projects?.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
title="Delete project role?"
|
||||||
|
open={open}
|
||||||
|
primaryButtonText="Delete role"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
disabledPrimaryButton={entitiesWithRole}
|
||||||
|
onClick={() => onConfirm(role!)}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
maxWidth="md"
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={entitiesWithRole}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Alert severity="error">
|
||||||
|
You are not allowed to delete a role that is
|
||||||
|
currently in use. Please change the role of the
|
||||||
|
following entities first:
|
||||||
|
</Alert>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(projects?.length)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledLabel>
|
||||||
|
Role assigned in {projects?.length}{' '}
|
||||||
|
projects:
|
||||||
|
</StyledLabel>
|
||||||
|
<StyledTableContainer>
|
||||||
|
<RoleDeleteDialogProjectRoleTable
|
||||||
|
projects={projects}
|
||||||
|
/>
|
||||||
|
</StyledTableContainer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<p>
|
||||||
|
You are about to delete role:{' '}
|
||||||
|
<strong>{role?.name}</strong>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
import { VirtualizedTable } from 'component/common/Table';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useTable, useSortBy, useFlexLayout, Column } from 'react-table';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { IProjectRoleUsageCount } from 'interfaces/project';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
|
||||||
|
interface IRoleDeleteDialogProjectRoleTableProps {
|
||||||
|
projects: IProjectRoleUsageCount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoleDeleteDialogProjectRoleTable = ({
|
||||||
|
projects,
|
||||||
|
}: IRoleDeleteDialogProjectRoleTableProps) => {
|
||||||
|
const [initialState] = useState(() => ({
|
||||||
|
sortBy: [{ id: 'name' }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
Header: 'Project name',
|
||||||
|
accessor: (row: any) => row.project || '',
|
||||||
|
minWidth: 200,
|
||||||
|
Cell: ({ row: { original: item } }: any) => (
|
||||||
|
<LinkCell
|
||||||
|
title={item.project}
|
||||||
|
to={`/projects/${item.project}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'users',
|
||||||
|
Header: 'Assigned users',
|
||||||
|
accessor: (row: any) =>
|
||||||
|
row.userCount === 1
|
||||||
|
? '1 user'
|
||||||
|
: `${row.userCount} users`,
|
||||||
|
Cell: TextCell,
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'serviceAccounts',
|
||||||
|
Header: 'Service accounts',
|
||||||
|
accessor: (row: any) =>
|
||||||
|
row.serviceAccountCount === 1
|
||||||
|
? '1 account'
|
||||||
|
: `${row.serviceAccountCount} accounts`,
|
||||||
|
Cell: TextCell,
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'groups',
|
||||||
|
Header: 'Assigned groups',
|
||||||
|
accessor: (row: any) =>
|
||||||
|
row.groupCount === 1
|
||||||
|
? '1 group'
|
||||||
|
: `${row.groupCount} groups`,
|
||||||
|
Cell: TextCell,
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
] as Column<IProjectRoleUsageCount>[],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { headerGroups, rows, prepareRow } = useTable(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
data: projects,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetHiddenColumns: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
disableMultiSort: true,
|
||||||
|
},
|
||||||
|
useSortBy,
|
||||||
|
useFlexLayout
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualizedTable
|
||||||
|
rows={rows}
|
||||||
|
headerGroups={headerGroups}
|
||||||
|
prepareRow={prepareRow}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,127 @@
|
|||||||
|
import { Alert, styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
|
||||||
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
|
import { IRole } from 'interfaces/role';
|
||||||
|
import { RoleDeleteDialogUsers } from './RoleDeleteDialogUsers';
|
||||||
|
import { RoleDeleteDialogServiceAccounts } from './RoleDeleteDialogServiceAccounts';
|
||||||
|
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
||||||
|
import { RoleDeleteDialogGroups } from './RoleDeleteDialogGroups';
|
||||||
|
|
||||||
|
const StyledTableContainer = styled('div')(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledLabel = styled('p')(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IRoleDeleteDialogRootRoleProps {
|
||||||
|
role?: IRole;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
onConfirm: (role: IRole) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoleDeleteDialogRootRole = ({
|
||||||
|
role,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm,
|
||||||
|
}: IRoleDeleteDialogRootRoleProps) => {
|
||||||
|
const { users } = useUsers();
|
||||||
|
const { serviceAccounts } = useServiceAccounts();
|
||||||
|
const { groups } = useGroups();
|
||||||
|
|
||||||
|
const roleUsers = users.filter(({ rootRole }) => rootRole === role?.id);
|
||||||
|
const roleServiceAccounts = serviceAccounts.filter(
|
||||||
|
({ rootRole }) => rootRole === role?.id
|
||||||
|
);
|
||||||
|
const roleGroups = groups?.filter(({ rootRole }) => rootRole === role?.id);
|
||||||
|
|
||||||
|
const entitiesWithRole = Boolean(
|
||||||
|
roleUsers.length || roleServiceAccounts.length || roleGroups?.length
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
title="Delete root role?"
|
||||||
|
open={open}
|
||||||
|
primaryButtonText="Delete role"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
disabledPrimaryButton={entitiesWithRole}
|
||||||
|
onClick={() => onConfirm(role!)}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={entitiesWithRole}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Alert severity="error">
|
||||||
|
You are not allowed to delete a role that is
|
||||||
|
currently in use. Please change the role of the
|
||||||
|
following entities first:
|
||||||
|
</Alert>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(roleUsers.length)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledLabel>
|
||||||
|
Users ({roleUsers.length}):
|
||||||
|
</StyledLabel>
|
||||||
|
<StyledTableContainer>
|
||||||
|
<RoleDeleteDialogUsers
|
||||||
|
users={roleUsers}
|
||||||
|
/>
|
||||||
|
</StyledTableContainer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(roleServiceAccounts.length)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledLabel>
|
||||||
|
Service accounts (
|
||||||
|
{roleServiceAccounts.length}):
|
||||||
|
</StyledLabel>
|
||||||
|
<StyledTableContainer>
|
||||||
|
<RoleDeleteDialogServiceAccounts
|
||||||
|
serviceAccounts={
|
||||||
|
roleServiceAccounts
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledTableContainer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(roleGroups?.length)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledLabel>
|
||||||
|
Groups ({roleGroups?.length}):
|
||||||
|
</StyledLabel>
|
||||||
|
<StyledTableContainer>
|
||||||
|
<RoleDeleteDialogGroups
|
||||||
|
groups={roleGroups!}
|
||||||
|
/>
|
||||||
|
</StyledTableContainer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<p>
|
||||||
|
You are about to delete role:{' '}
|
||||||
|
<strong>{role?.name}</strong>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
2
frontend/src/constants/roles.ts
Normal file
2
frontend/src/constants/roles.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const CUSTOM_ROOT_ROLE_TYPE = 'root-custom';
|
||||||
|
export const CUSTOM_PROJECT_ROLE_TYPE = 'custom';
|
@ -0,0 +1,33 @@
|
|||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
|
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||||
|
import { IProjectRoleUsageCount } from 'interfaces/project';
|
||||||
|
|
||||||
|
export const useProjectRoleAccessUsage = (roleId?: number) => {
|
||||||
|
const { isEnterprise } = useUiConfig();
|
||||||
|
|
||||||
|
const { data, error, mutate } = useConditionalSWR(
|
||||||
|
isEnterprise() && roleId,
|
||||||
|
{ projects: [] },
|
||||||
|
formatApiPath(`api/admin/projects/roles/${roleId}/access`),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
projects: (data?.projects ?? []) as IProjectRoleUsageCount[],
|
||||||
|
loading: !error && !data,
|
||||||
|
refetch: () => mutate(),
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[data, error, mutate]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('Project role usage'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
@ -35,3 +35,11 @@ export interface IProjectHealthReport extends IProject {
|
|||||||
activeCount: number;
|
activeCount: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProjectRoleUsageCount {
|
||||||
|
project: string;
|
||||||
|
role: number;
|
||||||
|
userCount: number;
|
||||||
|
groupCount: number;
|
||||||
|
serviceAccountCount: number;
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { Logger } from '../logger';
|
|||||||
import {
|
import {
|
||||||
IAccessInfo,
|
IAccessInfo,
|
||||||
IAccessStore,
|
IAccessStore,
|
||||||
|
IProjectRoleUsage,
|
||||||
IRole,
|
IRole,
|
||||||
IRoleWithProject,
|
IRoleWithProject,
|
||||||
IUserPermission,
|
IUserPermission,
|
||||||
@ -304,6 +305,59 @@ export class AccessStore implements IAccessStore {
|
|||||||
return rows.map((r) => r.group_id);
|
return rows.map((r) => r.group_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectUserAndGroupCountsForRole(
|
||||||
|
roleId: number,
|
||||||
|
): Promise<IProjectRoleUsage[]> {
|
||||||
|
const query = await this.db.raw(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
uq.project,
|
||||||
|
sum(uq.user_count) AS user_count,
|
||||||
|
sum(uq.svc_account_count) AS svc_account_count,
|
||||||
|
sum(uq.group_count) AS group_count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
project,
|
||||||
|
0 AS user_count,
|
||||||
|
0 AS svc_account_count,
|
||||||
|
count(project) AS group_count
|
||||||
|
FROM group_role
|
||||||
|
WHERE role_id = ?
|
||||||
|
GROUP BY project
|
||||||
|
|
||||||
|
UNION SELECT
|
||||||
|
project,
|
||||||
|
count(us.id) AS user_count,
|
||||||
|
count(svc.id) AS svc_account_count,
|
||||||
|
0 AS group_count
|
||||||
|
FROM role_user AS usr_r
|
||||||
|
LEFT OUTER JOIN public.users AS us ON us.id = usr_r.user_id AND us.is_service = 'false'
|
||||||
|
LEFT OUTER JOIN public.users AS svc ON svc.id = usr_r.user_id AND svc.is_service = 'true'
|
||||||
|
WHERE usr_r.role_id = ?
|
||||||
|
GROUP BY usr_r.project
|
||||||
|
) AS uq
|
||||||
|
GROUP BY uq.project
|
||||||
|
`,
|
||||||
|
[roleId, roleId],
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
const rows2 = await this.db(T.ROLE_USER)
|
||||||
|
.select('project', this.db.raw('count(project) as user_count'))
|
||||||
|
.where('role_id', roleId)
|
||||||
|
.groupBy('project');
|
||||||
|
*/
|
||||||
|
return query.rows.map((r) => {
|
||||||
|
return {
|
||||||
|
project: r.project,
|
||||||
|
role: roleId,
|
||||||
|
userCount: Number(r.user_count),
|
||||||
|
groupCount: Number(r.group_count),
|
||||||
|
serviceAccountCount: Number(r.svc_account_count),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async addUserToRole(
|
async addUserToRole(
|
||||||
userId: number,
|
userId: number,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
|
@ -3,6 +3,7 @@ import User, { IProjectUser, IUser } from '../types/user';
|
|||||||
import {
|
import {
|
||||||
IAccessInfo,
|
IAccessInfo,
|
||||||
IAccessStore,
|
IAccessStore,
|
||||||
|
IProjectRoleUsage,
|
||||||
IRole,
|
IRole,
|
||||||
IRoleWithPermissions,
|
IRoleWithPermissions,
|
||||||
IRoleWithProject,
|
IRoleWithProject,
|
||||||
@ -453,6 +454,10 @@ export class AccessService {
|
|||||||
return [roles, users.flat(), groups];
|
return [roles, users.flat(), groups];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectRoleUsage(roleId: number): Promise<IProjectRoleUsage[]> {
|
||||||
|
return this.store.getProjectUserAndGroupCountsForRole(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
async createDefaultProjectRoles(
|
async createDefaultProjectRoles(
|
||||||
owner: IUser,
|
owner: IUser,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
RoleName,
|
RoleName,
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
ProjectAccessAddedEvent,
|
ProjectAccessAddedEvent,
|
||||||
|
IProjectRoleUsage,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||||
import {
|
import {
|
||||||
@ -697,6 +698,10 @@ export default class ProjectService {
|
|||||||
return this.store.getProjectsByUser(userId);
|
return this.store.getProjectsByUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectRoleUsage(roleId: number): Promise<IProjectRoleUsage[]> {
|
||||||
|
return this.accessService.getProjectRoleUsage(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
async statusJob(): Promise<void> {
|
async statusJob(): Promise<void> {
|
||||||
const projects = await this.store.getAll();
|
const projects = await this.store.getAll();
|
||||||
|
|
||||||
|
@ -1 +1,9 @@
|
|||||||
export const DEFAULT_PROJECT = 'default';
|
export const DEFAULT_PROJECT = 'default';
|
||||||
|
|
||||||
|
export interface IProjectRoleUsage {
|
||||||
|
project: string;
|
||||||
|
role: number;
|
||||||
|
userCount: number;
|
||||||
|
groupCount: number;
|
||||||
|
serviceAccountCount: number;
|
||||||
|
}
|
||||||
|
@ -14,6 +14,14 @@ export interface IRole {
|
|||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProjectRoleUsage {
|
||||||
|
project: string;
|
||||||
|
role: number;
|
||||||
|
userCount: number;
|
||||||
|
groupCount: number;
|
||||||
|
serviceAccountCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRoleWithProject extends IRole {
|
export interface IRoleWithProject extends IRole {
|
||||||
project: string;
|
project: string;
|
||||||
}
|
}
|
||||||
@ -70,6 +78,10 @@ export interface IAccessStore extends Store<IRole, number> {
|
|||||||
|
|
||||||
getGroupIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
getGroupIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
||||||
|
|
||||||
|
getProjectUserAndGroupCountsForRole(
|
||||||
|
roleId: number,
|
||||||
|
): Promise<IProjectRoleUsage[]>;
|
||||||
|
|
||||||
wipePermissionsFromRole(role_id: number): Promise<void>;
|
wipePermissionsFromRole(role_id: number): Promise<void>;
|
||||||
|
|
||||||
addEnvironmentPermissionsToRole(
|
addEnvironmentPermissionsToRole(
|
||||||
|
7
src/test/fixtures/fake-access-store.ts
vendored
7
src/test/fixtures/fake-access-store.ts
vendored
@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
IAccessInfo,
|
IAccessInfo,
|
||||||
IAccessStore,
|
IAccessStore,
|
||||||
|
IProjectRoleUsage,
|
||||||
IRole,
|
IRole,
|
||||||
IRoleWithProject,
|
IRoleWithProject,
|
||||||
IUserPermission,
|
IUserPermission,
|
||||||
@ -20,6 +21,12 @@ class AccessStoreMock implements IAccessStore {
|
|||||||
this.fakeRolesStore = roleStore ?? new FakeRoleStore();
|
this.fakeRolesStore = roleStore ?? new FakeRoleStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProjectUserAndGroupCountsForRole(
|
||||||
|
roleId: number,
|
||||||
|
): Promise<IProjectRoleUsage[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
addAccessToProject(
|
addAccessToProject(
|
||||||
users: IAccessInfo[],
|
users: IAccessInfo[],
|
||||||
groups: IAccessInfo[],
|
groups: IAccessInfo[],
|
||||||
|
Loading…
Reference in New Issue
Block a user