Skip to content

Commit e7141d6

Browse files
authored
feat: migrate Modify Permissions component to fluent v9 (#3527)
1 parent d5e0044 commit e7141d6

File tree

6 files changed

+490
-3
lines changed

6 files changed

+490
-3
lines changed

src/app/views/common/lazy-loader/component-registry/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import LazyRequestHeaders from '../../../query-runner/request/headers/RequestHe
1212
import LazyHistory from '../../../sidebar/history/History';
1313
import LazyResourceExplorer from '../../../sidebar/resource-explorer/ResourceExplorer';
1414

15-
export const Permissions = (props?: IPermissionProps) => {
15+
export const Permissions =() => {
1616
return (
17-
<LazySpecificPermissions {...props} />
17+
<LazySpecificPermissions />
1818
)
1919
}
2020

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { makeStyles, tokens } from '@fluentui/react-components';
2+
3+
const permissionStyles = makeStyles({
4+
root: {
5+
padding: '17px'
6+
},
7+
label: {
8+
marginLeft: '12px'
9+
},
10+
errorLabel: {
11+
marginTop: '10px',
12+
paddingLeft: '10px',
13+
paddingRight: '20px',
14+
minHeight: '200px'
15+
},
16+
permissionText: {
17+
marginBottom: '5px',
18+
paddingLeft: '10px'
19+
},
20+
tableWrapper: {
21+
flex: 1,
22+
overflowY: 'auto'
23+
},
24+
adminLabel: {
25+
fontSize: tokens.fontSizeBase300,
26+
padding: tokens.spacingVerticalXS
27+
},
28+
button: {
29+
margin: tokens.spacingVerticalXXS
30+
},
31+
tooltip: {
32+
display: 'block'
33+
},
34+
icon: {
35+
position: 'relative',
36+
top: '4px',
37+
cursor: 'pointer'
38+
},
39+
iconButton: {
40+
position: 'relative',
41+
left: '4px',
42+
top: '2px'
43+
},
44+
headerContainer: {
45+
display: 'flex',
46+
alignItems: 'center',
47+
textAlign: 'left'
48+
},
49+
headerText: {
50+
marginLeft: '8px'
51+
}
52+
});
53+
54+
export default permissionStyles;
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
Button,
3+
Tooltip,
4+
Label
5+
} from '@fluentui/react-components';
6+
import { useAppDispatch, useAppSelector } from '../../../../../store';
7+
import { IPermission, IPermissionGrant } from '../../../../../types/permissions';
8+
import { revokeScopes } from '../../../../services/actions/revoke-scopes.action';
9+
import { REVOKING_PERMISSIONS_REQUIRED_SCOPES } from '../../../../services/graph-constants';
10+
import { consentToScopes } from '../../../../services/slices/auth.slice';
11+
import { getAllPrincipalGrant, getSinglePrincipalGrant } from '../../../../services/slices/permission-grants.slice';
12+
import { translateMessage } from '../../../../utils/translate-messages';
13+
import { PermissionConsentType } from './ConsentType';
14+
import { InfoRegular } from '@fluentui/react-icons';
15+
import permissionStyles from './Permission.stylesV9';
16+
17+
interface PermissionItemProps {
18+
item: IPermission;
19+
index: number;
20+
column: { key: string; fieldName?: string } | undefined;
21+
}
22+
23+
const PermissionItem = (props: PermissionItemProps): JSX.Element | null => {
24+
const dispatch = useAppDispatch();
25+
const scopes = useAppSelector((state) => state.scopes);
26+
const consentedScopes = useAppSelector((state) => state.auth.consentedScopes);
27+
const user = useAppSelector((state) => state.profile.user);
28+
const permissionGrants = useAppSelector((state) => state.permissionGrants);
29+
const { item, column } = props;
30+
const styles = permissionStyles();
31+
32+
const consented = !!item.consented;
33+
34+
const handleConsent = async (permission: IPermission): Promise<void> => {
35+
const consentScopes = [permission.value];
36+
dispatch(consentToScopes(consentScopes));
37+
};
38+
39+
const getAllPrincipalPermissions = (tenantWidePermissionsGrant: IPermissionGrant[]): string[] => {
40+
const allPrincipalPermissions = tenantWidePermissionsGrant.find((permission: IPermissionGrant) =>
41+
permission.consentType.toLowerCase() === 'allprincipals'
42+
);
43+
return allPrincipalPermissions ? allPrincipalPermissions.scope.split(' ') : [];
44+
};
45+
46+
const userHasRequiredPermissions = (): boolean => {
47+
if (permissionGrants && permissionGrants.permissions && permissionGrants.permissions.length > 0) {
48+
const allPrincipalPermissions = getAllPrincipalPermissions(permissionGrants.permissions);
49+
const principalAndAllPrincipalPermissions = [...allPrincipalPermissions, ...consentedScopes];
50+
const requiredPermissions = REVOKING_PERMISSIONS_REQUIRED_SCOPES.split(' ');
51+
return requiredPermissions.every((scope) => principalAndAllPrincipalPermissions.includes(scope));
52+
}
53+
return false;
54+
};
55+
56+
const ConsentTypeProperty = (): JSX.Element | null => {
57+
if (scopes && consented && user?.id) {
58+
const tenantWideGrant: IPermissionGrant[] = permissionGrants.permissions!;
59+
const allPrincipalPermissions = getAllPrincipalGrant(tenantWideGrant);
60+
const singlePrincipalPermissions: string[] = getSinglePrincipalGrant(tenantWideGrant, user?.id);
61+
const tenantGrantFetchPending = permissionGrants.pending;
62+
const consentTypeProperties = {
63+
item,
64+
allPrincipalPermissions,
65+
singlePrincipalPermissions,
66+
tenantGrantFetchPending,
67+
dispatch
68+
};
69+
return <PermissionConsentType {...consentTypeProperties} />;
70+
}
71+
return null;
72+
};
73+
74+
const handleRevoke = async (permission: IPermission): Promise<void> => {
75+
dispatch(revokeScopes(permission.value));
76+
};
77+
78+
const ConsentButton = (): JSX.Element => {
79+
if (consented) {
80+
if (userHasRequiredPermissions()) {
81+
return (
82+
<Button
83+
appearance='primary'
84+
onClick={() => handleRevoke(item)}
85+
className={styles.button}
86+
>
87+
{translateMessage('Revoke')}
88+
</Button>
89+
);
90+
}
91+
return (
92+
<Tooltip content={translateMessage('You require the following permissions to revoke')} relationship='label'>
93+
<Button appearance='primary' disabled className={styles.button}>
94+
{translateMessage('Revoke')}
95+
</Button>
96+
</Tooltip>
97+
);
98+
}
99+
return (
100+
<Button appearance='primary' onClick={() => handleConsent(item)} className={styles.button}>
101+
{translateMessage('Consent')}
102+
</Button>
103+
);
104+
};
105+
106+
if (column) {
107+
const content = item[column.fieldName as keyof IPermission] as string;
108+
switch (column.key) {
109+
case 'value':
110+
return (
111+
<div>
112+
{content}
113+
{props.index === 0 && (
114+
<Tooltip content={translateMessage('Least privileged permission')} relationship='label'>
115+
<span className={styles.icon}><InfoRegular /></span>
116+
</Tooltip>
117+
)}
118+
</div>
119+
);
120+
121+
case 'isAdmin':
122+
return (
123+
<div className={styles.adminLabel}>
124+
<Label>{translateMessage(item.isAdmin ? 'Yes' : 'No')}</Label>
125+
</div>
126+
);
127+
128+
case 'consented':
129+
return <ConsentButton />;
130+
131+
case 'consentDescription':
132+
return (
133+
<Tooltip content={item.consentDescription} relationship='label'>
134+
<span>{item.consentDescription}</span>
135+
</Tooltip>
136+
);
137+
138+
case 'consentType':
139+
return <ConsentTypeProperty />;
140+
141+
default:
142+
return (
143+
<Tooltip content={content} relationship='label'>
144+
<span>{content}</span>
145+
</Tooltip>
146+
);
147+
}
148+
}
149+
return null;
150+
};
151+
152+
export default PermissionItem;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {
2+
Table, TableHeader, TableRow, TableCell, TableBody,
3+
Link, Text
4+
} from '@fluentui/react-components';
5+
import { useContext, useEffect, useState } from 'react';
6+
import { useAppDispatch, useAppSelector } from '../../../../../store';
7+
import { IPermission } from '../../../../../types/permissions';
8+
import { ValidationContext } from '../../../../services/context/validation-context/ValidationContext';
9+
import { usePopups } from '../../../../services/hooks';
10+
import { fetchAllPrincipalGrants } from '../../../../services/slices/permission-grants.slice';
11+
import { fetchScopes } from '../../../../services/slices/scopes.slice';
12+
import { ScopesError } from '../../../../utils/error-utils/ScopesError';
13+
import { translateMessage } from '../../../../utils/translate-messages';
14+
import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment';
15+
import { getColumns } from './columnsV9';
16+
import { setConsentedStatus, sortPermissionsWithPrivilege } from './util';
17+
import permissionStyles from './Permission.stylesV9';
18+
19+
20+
export const Permissions = (): JSX.Element => {
21+
const dispatch = useAppDispatch();
22+
const validation = useContext(ValidationContext);
23+
const sampleQuery = useAppSelector((state) => state.sampleQuery);
24+
const scopes = useAppSelector((state) => state.scopes);
25+
const authToken = useAppSelector((state) => state.auth.authToken);
26+
const consentedScopes = useAppSelector((state) => state.auth.consentedScopes);
27+
const dimensions = useAppSelector((state) => state.dimensions);
28+
const { show: showPermissions } = usePopups('full-permissions', 'panel');
29+
30+
const tokenPresent = !!authToken.token;
31+
const { pending: loading, error } = scopes;
32+
const [permissions, setPermissions] = useState<{ item: IPermission; index: number }[]>([]);
33+
const [permissionsError, setPermissionsError] = useState<ScopesError | null>(error);
34+
35+
const styles = permissionStyles();
36+
const tabHeight = convertVhToPx(dimensions.request.height, 110);
37+
38+
useEffect(() => {
39+
if (error && error?.url.includes('permissions')) {
40+
setPermissionsError(error);
41+
}
42+
}, [error]);
43+
44+
const openPermissionsPanel = () => {
45+
showPermissions({
46+
settings: {
47+
title: translateMessage('Permissions'),
48+
width: 'lg'
49+
}
50+
});
51+
};
52+
53+
const getPermissions = (): void => {
54+
dispatch(fetchScopes('query'));
55+
fetchPermissionGrants();
56+
};
57+
58+
const fetchPermissionGrants = (): void => {
59+
if (tokenPresent) {
60+
dispatch(fetchAllPrincipalGrants());
61+
}
62+
};
63+
64+
useEffect(() => {
65+
if (validation.isValid) {
66+
getPermissions();
67+
}
68+
}, [sampleQuery]);
69+
70+
useEffect(() => {
71+
if (tokenPresent && validation.isValid) {
72+
dispatch(fetchAllPrincipalGrants());
73+
}
74+
}, []);
75+
76+
useEffect(() => {
77+
let updatedPermissions = scopes.data.specificPermissions || [];
78+
updatedPermissions = sortPermissionsWithPrivilege(updatedPermissions);
79+
updatedPermissions = setConsentedStatus(tokenPresent, updatedPermissions, consentedScopes);
80+
setPermissions(updatedPermissions.map((item, index) => ({ item, index })));
81+
}, [scopes.data.specificPermissions, tokenPresent, consentedScopes]);
82+
83+
const columns = getColumns({ source: 'tab', tokenPresent });
84+
85+
if (loading.isSpecificPermissions) {
86+
return <div className={styles.label}><Text>{translateMessage('Fetching permissions')}... </Text></div>;
87+
}
88+
89+
if (!validation.isValid) {
90+
return <div className={styles.label}><Text>{translateMessage('Invalid URL')}!</Text></div>;
91+
}
92+
93+
const displayNoPermissionsFoundMessage = (): JSX.Element => (
94+
<div className={styles.root}>
95+
<Text>
96+
{translateMessage('permissions not found in permissions tab')}
97+
<Link inline onClick={openPermissionsPanel}>
98+
{translateMessage('open permissions panel')}
99+
</Link>
100+
{translateMessage('permissions list')}
101+
</Text>
102+
</div>
103+
);
104+
105+
const displayNotSignedInMessage = (): JSX.Element => (
106+
<div className={styles.root}>
107+
<Text>{translateMessage('sign in to view a list of all permissions')}</Text>
108+
</div>
109+
);
110+
111+
const displayErrorFetchingPermissionsMessage = (): JSX.Element => (
112+
<div className={styles.errorLabel}><Text>{translateMessage('Fetching permissions failing')}</Text></div>
113+
);
114+
115+
if (!tokenPresent && permissions.length === 0) {
116+
return displayNotSignedInMessage();
117+
}
118+
119+
if (permissions.length === 0) {
120+
return permissionsError?.status && (permissionsError?.status === 404 || permissionsError?.status === 400)
121+
? displayNoPermissionsFoundMessage()
122+
: displayErrorFetchingPermissionsMessage();
123+
}
124+
125+
return (
126+
<div>
127+
<div className={styles.permissionText}>
128+
<Text>
129+
{translateMessage(tokenPresent ? 'permissions required to run the query':'sign in to consent to permissions')}
130+
</Text>
131+
</div>
132+
<div className={styles.tableWrapper} style={{ height: tabHeight }}>
133+
<Table aria-label={translateMessage('Permissions Table')}>
134+
<TableHeader>
135+
<TableRow>
136+
{columns.map((column) => (
137+
<TableCell key={column.columnId}>{column.renderHeaderCell()}</TableCell>
138+
))}
139+
</TableRow>
140+
</TableHeader>
141+
<TableBody>
142+
{permissions.map(({ item, index }) => (
143+
<TableRow key={item.value}>
144+
{columns.map((column) => (
145+
<TableCell key={column.columnId}>{column.renderCell({ item, index })}</TableCell>
146+
))}
147+
</TableRow>
148+
))}
149+
</TableBody>
150+
</Table>
151+
</div>
152+
</div>
153+
);
154+
};

0 commit comments

Comments
 (0)