1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

Fix pro project role descriptions (#2612)

https://linear.app/unleash/issue/2-485/bug-pro-access-page-the-tooltip-on-the-roles-is-not-working

Fixes an issue where we would not show anything for project role
permissions since we're not serving that specific endpoint below
Enterprise level. This way we fallback to the access role descriptions,
which are also nice and informative:


![image](https://user-images.githubusercontent.com/14320932/205987327-def46ef2-4010-47dd-ba7d-5a264f0b547d.png)


PR also adds support for SWR options in ConditionalSWR and
EnterpriseSWR.
This commit is contained in:
Nuno Góis 2022-12-07 10:22:42 +00:00 committed by GitHub
parent c66daa0ba2
commit 9accbcfa8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 162 additions and 82 deletions

View File

@ -392,7 +392,12 @@ export const ProjectAccessAssign = ({
</StyledAutocompleteWrapper> </StyledAutocompleteWrapper>
<ConditionallyRender <ConditionallyRender
condition={Boolean(role?.id)} condition={Boolean(role?.id)}
show={<ProjectRoleDescription roleId={role?.id!} />} show={
<ProjectRoleDescription
roleId={role?.id!}
projectId={projectId}
/>
}
/> />
</div> </div>

View File

@ -2,6 +2,9 @@ import { styled, SxProps, Theme } from '@mui/material';
import { ForwardedRef, forwardRef, useMemo, VFC } from 'react'; import { ForwardedRef, forwardRef, useMemo, VFC } from 'react';
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole'; import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useProjectAccess from 'hooks/api/getters/useProjectAccess/useProjectAccess';
import { ProjectRoleDescriptionProjectPermissions } from './ProjectRoleDescriptionProjectPermissions/ProjectRoleDescriptionProjectPermissions';
import { ProjectRoleDescriptionEnvironmentPermissions } from './ProjectRoleDescriptionEnvironmentPermissions/ProjectRoleDescriptionEnvironmentPermissions';
const StyledDescription = styled('div', { const StyledDescription = styled('div', {
shouldForwardProp: prop => shouldForwardProp: prop =>
@ -46,15 +49,24 @@ interface IProjectRoleDescriptionStyleProps {
interface IProjectRoleDescriptionProps interface IProjectRoleDescriptionProps
extends IProjectRoleDescriptionStyleProps { extends IProjectRoleDescriptionStyleProps {
roleId: number; roleId: number;
projectId: string;
} }
export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> =
forwardRef( forwardRef(
( (
{ roleId, className, sx, ...props }: IProjectRoleDescriptionProps, {
roleId,
projectId,
className,
sx,
...props
}: IProjectRoleDescriptionProps,
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
) => { ) => {
const { role } = useProjectRole(roleId.toString()); const { role } = useProjectRole(roleId.toString());
const { access } = useProjectAccess(projectId);
const accessRole = access?.roles.find(role => role.id === roleId);
const environments = useMemo(() => { const environments = useMemo(() => {
const environments = new Set<string>(); const environments = new Set<string>();
@ -80,62 +92,65 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> =
ref={ref} ref={ref}
> >
<ConditionallyRender <ConditionallyRender
condition={Boolean(projectPermissions?.length)} condition={role.permissions?.length > 0}
show={ show={
<> <>
<StyledDescriptionHeader> <ConditionallyRender
Project permissions condition={Boolean(
</StyledDescriptionHeader> projectPermissions?.length
<StyledDescriptionBlock> )}
{role.permissions show={
?.filter( <>
(permission: any) => <StyledDescriptionHeader>
!permission.environment Project permissions
) </StyledDescriptionHeader>
.map( <StyledDescriptionBlock>
(permission: any) => <ProjectRoleDescriptionProjectPermissions
permission.displayName permissions={
) role.permissions
.sort() }
.map((permission: any) => ( />
<p key={permission}>{permission}</p> </StyledDescriptionBlock>
))} </>
</StyledDescriptionBlock> }
/>
<ConditionallyRender
condition={Boolean(environments.length)}
show={
<>
<StyledDescriptionHeader>
Environment permissions
</StyledDescriptionHeader>
{environments.map(environment => (
<div key={environment}>
<StyledDescriptionSubHeader>
{environment}
</StyledDescriptionSubHeader>
<StyledDescriptionBlock>
<ProjectRoleDescriptionEnvironmentPermissions
environment={
environment
}
permissions={
role.permissions
}
/>
</StyledDescriptionBlock>
</div>
))}
</>
}
/>
</> </>
} }
/> elseShow={
<ConditionallyRender
condition={Boolean(environments.length)}
show={
<> <>
<StyledDescriptionHeader> <StyledDescriptionSubHeader>
Environment permissions {accessRole?.name}
</StyledDescriptionHeader> </StyledDescriptionSubHeader>
{environments.map((environment: any) => ( <StyledDescriptionBlock>
<div key={environment}> {accessRole?.description}
<StyledDescriptionSubHeader> </StyledDescriptionBlock>
{environment}
</StyledDescriptionSubHeader>
<StyledDescriptionBlock>
{role.permissions
.filter(
(permission: any) =>
permission.environment ===
environment
)
.map(
(permission: any) =>
permission.displayName
)
.sort()
.map((permission: any) => (
<p key={permission}>
{permission}
</p>
))}
</StyledDescriptionBlock>
</div>
))}
</> </>
} }
/> />

View File

@ -0,0 +1,19 @@
interface IProjectRoleDescriptionEnvironmentPermissionsProps {
environment: string;
permissions: any[];
}
export const ProjectRoleDescriptionEnvironmentPermissions = ({
environment,
permissions,
}: IProjectRoleDescriptionEnvironmentPermissionsProps) => (
<>
{permissions
.filter((permission: any) => permission.environment === environment)
.map((permission: any) => permission.displayName)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</>
);

View File

@ -0,0 +1,17 @@
interface IProjectRoleDescriptionProjectPermissionsProps {
permissions: any[];
}
export const ProjectRoleDescriptionProjectPermissions = ({
permissions,
}: IProjectRoleDescriptionProjectPermissionsProps) => (
<>
{permissions
?.filter((permission: any) => !permission.environment)
.map((permission: any) => permission.displayName)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</>
);

View File

@ -17,12 +17,14 @@ const StyledPopover = styled(Popover)(() => ({
interface IProjectAccessRoleCellProps { interface IProjectAccessRoleCellProps {
roleId: number; roleId: number;
projectId: string;
value?: string; value?: string;
emptyText?: string; emptyText?: string;
} }
export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({ export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
roleId, roleId,
projectId,
value, value,
emptyText, emptyText,
}) => { }) => {
@ -63,7 +65,11 @@ export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
horizontal: 'left', horizontal: 'left',
}} }}
> >
<ProjectRoleDescription roleId={roleId} popover /> <ProjectRoleDescription
roleId={roleId}
projectId={projectId}
popover
/>
</StyledPopover> </StyledPopover>
</> </>
); );

View File

@ -170,6 +170,7 @@ export const ProjectAccessTable: VFC = () => {
Cell: ({ value, row: { original: row } }: any) => ( Cell: ({ value, row: { original: row } }: any) => (
<ProjectAccessRoleCell <ProjectAccessRoleCell
roleId={row.entity.roleId} roleId={row.entity.roleId}
projectId={projectId}
value={value} value={value}
/> />
), ),

View File

@ -7,9 +7,9 @@ export const useChangeRequestConfig = (projectId: string) => {
const { data, error, mutate } = useEnterpriseSWR< const { data, error, mutate } = useEnterpriseSWR<
IChangeRequestEnvironmentConfig[] IChangeRequestEnvironmentConfig[]
>( >(
[],
formatApiPath(`api/admin/projects/${projectId}/change-requests/config`), formatApiPath(`api/admin/projects/${projectId}/change-requests/config`),
fetcher, fetcher
[]
); );
return { return {

View File

@ -0,0 +1,23 @@
import useSWR, { BareFetcher, Key, SWRConfiguration, SWRResponse } from 'swr';
import { useEffect } from 'react';
export const useConditionalSWR = <Data = any, Error = any, T = boolean>(
condition: T,
fallback: Data,
key: Key,
fetcher: BareFetcher<Data>,
options: SWRConfiguration = {}
): SWRResponse<Data, Error> => {
const result = useSWR(
key,
(path: string) =>
condition ? fetcher(path) : Promise.resolve(fallback),
options
);
useEffect(() => {
result.mutate();
}, [condition]);
return result;
};

View File

@ -1,33 +1,21 @@
import useSWR, { BareFetcher, Key, SWRResponse } from 'swr'; import { BareFetcher, Key, SWRConfiguration } from 'swr';
import { useEffect } from 'react'; import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import useUiConfig from '../useUiConfig/useUiConfig'; import useUiConfig from '../useUiConfig/useUiConfig';
export const useConditionalSWR = <Data = any, Error = any, T = boolean>(
key: Key,
fetcher: BareFetcher<Data>,
condition: T
): SWRResponse<Data, Error> => {
const result = useSWR(key, fetcher);
useEffect(() => {
result.mutate();
}, [condition]);
return result;
};
export const useEnterpriseSWR = <Data = any, Error = any>( export const useEnterpriseSWR = <Data = any, Error = any>(
fallback: Data,
key: Key, key: Key,
fetcher: BareFetcher<Data>, fetcher: BareFetcher<Data>,
fallback: Data options: SWRConfiguration = {}
) => { ) => {
const { isEnterprise } = useUiConfig(); const { isEnterprise } = useUiConfig();
const result = useConditionalSWR( const result = useConditionalSWR(
isEnterprise(),
fallback,
key, key,
(path: string) => fetcher,
isEnterprise() ? fetcher(path) : Promise.resolve(fallback), options
isEnterprise()
); );
return result; return result;

View File

@ -11,9 +11,9 @@ const fetcher = (path: string) => {
export const usePendingChangeRequests = (project: string) => { export const usePendingChangeRequests = (project: string) => {
const { data, error, mutate } = useEnterpriseSWR<IChangeRequest[]>( const { data, error, mutate } = useEnterpriseSWR<IChangeRequest[]>(
[],
formatApiPath(`api/admin/projects/${project}/change-requests/pending`), formatApiPath(`api/admin/projects/${project}/change-requests/pending`),
fetcher, fetcher
[]
); );
return { return {

View File

@ -14,11 +14,11 @@ export const usePendingChangeRequestsForFeature = (
featureName: string featureName: string
) => { ) => {
const { data, error, mutate } = useEnterpriseSWR<IChangeRequest[]>( const { data, error, mutate } = useEnterpriseSWR<IChangeRequest[]>(
[],
formatApiPath( formatApiPath(
`api/admin/projects/${project}/change-requests/pending/${featureName}` `api/admin/projects/${project}/change-requests/pending/${featureName}`
), ),
fetcher, fetcher
[]
); );
return { return {

View File

@ -1,7 +1,8 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR';
const useProjectRole = (id: string, options: SWRConfiguration = {}) => { const useProjectRole = (id: string, options: SWRConfiguration = {}) => {
const fetcher = () => { const fetcher = () => {
@ -13,7 +14,12 @@ const useProjectRole = (id: string, options: SWRConfiguration = {}) => {
.then(res => res.json()); .then(res => res.json());
}; };
const { data, error } = useSWR(`api/admin/roles/${id}`, fetcher, options); const { data, error } = useEnterpriseSWR(
{},
`api/admin/roles/${id}`,
fetcher,
options
);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {