Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate Modify Permissions component to fluent v9 #3527

Merged
merged 5 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import LazyRequestHeaders from '../../../query-runner/request/headers/RequestHe
import LazyHistory from '../../../sidebar/history/History';
import LazyResourceExplorer from '../../../sidebar/resource-explorer/ResourceExplorer';

export const Permissions = (props?: IPermissionProps) => {
export const Permissions =() => {
return (
<LazySpecificPermissions {...props} />
<LazySpecificPermissions />
)
}

Expand Down
168 changes: 168 additions & 0 deletions src/app/views/query-runner/request/permissions/PermissionItemV9.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
Button,
Tooltip,
Label,
makeStyles,
tokens
} from '@fluentui/react-components';
import { useAppDispatch, useAppSelector } from '../../../../../store';
import { IPermission, IPermissionGrant } from '../../../../../types/permissions';
import { revokeScopes } from '../../../../services/actions/revoke-scopes.action';
import { REVOKING_PERMISSIONS_REQUIRED_SCOPES } from '../../../../services/graph-constants';
import { consentToScopes } from '../../../../services/slices/auth.slice';
import { getAllPrincipalGrant, getSinglePrincipalGrant } from '../../../../services/slices/permission-grants.slice';
import { translateMessage } from '../../../../utils/translate-messages';
import { PermissionConsentType } from './ConsentType';
import { InfoRegular } from '@fluentui/react-icons';

interface PermissionItemProps {
item: IPermission;
index: number;
column: { key: string; fieldName?: string } | undefined;
}

const useStyles = makeStyles({
adminLabel: {
fontSize: tokens.fontSizeBase300,
padding: tokens.spacingVerticalXS
},
button: {
margin: tokens.spacingVerticalXXS
},
tooltip: {
display: 'block'
},
icon: {
position: 'relative',
top: '4px',
cursor: 'pointer'
}
});

const PermissionItem = (props: PermissionItemProps): JSX.Element | null => {
const dispatch = useAppDispatch();
const { scopes, auth: { consentedScopes }, profile: { user }, permissionGrants } = useAppSelector((state) => state);
ElinorW marked this conversation as resolved.
Show resolved Hide resolved
const { item, column } = props;
const styles = useStyles();

const consented = !!item.consented;

const handleConsent = async (permission: IPermission): Promise<void> => {
const consentScopes = [permission.value];
dispatch(consentToScopes(consentScopes));
};

const getAllPrincipalPermissions = (tenantWidePermissionsGrant: IPermissionGrant[]): string[] => {
const allPrincipalPermissions = tenantWidePermissionsGrant.find((permission: IPermissionGrant) =>
permission.consentType.toLowerCase() === 'allprincipals'
);
return allPrincipalPermissions ? allPrincipalPermissions.scope.split(' ') : [];
};

const userHasRequiredPermissions = (): boolean => {
if (permissionGrants && permissionGrants.permissions && permissionGrants.permissions.length > 0) {
const allPrincipalPermissions = getAllPrincipalPermissions(permissionGrants.permissions);
const principalAndAllPrincipalPermissions = [...allPrincipalPermissions, ...consentedScopes];
const requiredPermissions = REVOKING_PERMISSIONS_REQUIRED_SCOPES.split(' ');
return requiredPermissions.every((scope) => principalAndAllPrincipalPermissions.includes(scope));
}
return false;
};

const ConsentTypeProperty = (): JSX.Element | null => {
if (scopes && consented && user?.id) {
const tenantWideGrant: IPermissionGrant[] = permissionGrants.permissions!;
const allPrincipalPermissions = getAllPrincipalGrant(tenantWideGrant);
const singlePrincipalPermissions: string[] = getSinglePrincipalGrant(tenantWideGrant, user?.id);
const tenantGrantFetchPending = permissionGrants.pending;
const consentTypeProperties = {
item,
allPrincipalPermissions,
singlePrincipalPermissions,
tenantGrantFetchPending,
dispatch
};
return <PermissionConsentType {...consentTypeProperties} />;
}
return null;
};

const handleRevoke = async (permission: IPermission): Promise<void> => {
dispatch(revokeScopes(permission.value));
};

const ConsentButton = (): JSX.Element => {
if (consented) {
if (userHasRequiredPermissions()) {
return (
<Button
appearance='primary'
onClick={() => handleRevoke(item)}
className={styles.button}
>
{translateMessage('Revoke')}
</Button>
);
}
return (
<Tooltip content={translateMessage('You require the following permissions to revoke')} relationship='label'>
<Button appearance='primary' disabled className={styles.button}>
{translateMessage('Revoke')}
</Button>
</Tooltip>
);
}
return (
<Button appearance='primary' onClick={() => handleConsent(item)} className={styles.button}>
{translateMessage('Consent')}
</Button>
);
};

if (column) {
const content = item[column.fieldName as keyof IPermission] as string;
switch (column.key) {
case 'value':
return (
<div>
{content}
{props.index === 0 && (
<Tooltip content={translateMessage('Least privileged permission')} relationship='label'>
<span className={styles.icon}><InfoRegular /></span>
</Tooltip>
)}
</div>
);

case 'isAdmin':
return (
<div className={styles.adminLabel}>
<Label>{translateMessage(item.isAdmin ? 'Yes' : 'No')}</Label>
</div>
);

case 'consented':
return <ConsentButton />;

case 'consentDescription':
return (
<Tooltip content={item.consentDescription} relationship='label'>
<span>{item.consentDescription}</span>
</Tooltip>
);

case 'consentType':
return <ConsentTypeProperty />;

default:
return (
<Tooltip content={content} relationship='label'>
<span>{content}</span>
</Tooltip>
);
}
}
return null;
};

export default PermissionItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
Table, TableHeader, TableRow, TableCell, TableBody,
Label, makeStyles
} from '@fluentui/react-components';
import { useContext, useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../../../../store';
import { IPermission } from '../../../../../types/permissions';
import { ValidationContext } from '../../../../services/context/validation-context/ValidationContext';
import { usePopups } from '../../../../services/hooks';
import { fetchAllPrincipalGrants } from '../../../../services/slices/permission-grants.slice';
import { fetchScopes } from '../../../../services/slices/scopes.slice';
import { ScopesError } from '../../../../utils/error-utils/ScopesError';
import { translateMessage } from '../../../../utils/translate-messages';
import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment';
import { getColumns } from './columnsV9';
import { setConsentedStatus, sortPermissionsWithPrivilege } from './util';
import { FontSizes, Link } from '@fluentui/react';
ElinorW marked this conversation as resolved.
Show resolved Hide resolved

const useStyles = makeStyles({
root: {
padding: '17px'
},
label: {
marginLeft: '12px'
},
errorLabel: {
marginTop: '10px',
paddingLeft: '10px',
paddingRight: '20px',
minHeight: '200px'
},
permissionText: {
fontSize: FontSizes.small,
ElinorW marked this conversation as resolved.
Show resolved Hide resolved
marginBottom: '5px',
paddingLeft: '10px'
},
tableWrapper: {
flex: 1,
overflowY: 'auto'
}
});

export const Permissions = (): JSX.Element => {
const dispatch = useAppDispatch();
const validation = useContext(ValidationContext);
const { sampleQuery, scopes, auth: { authToken, consentedScopes }, dimensions } =
ElinorW marked this conversation as resolved.
Show resolved Hide resolved
useAppSelector((state) => state);
const { show: showPermissions } = usePopups('full-permissions', 'panel');

const tokenPresent = !!authToken.token;
const { pending: loading, error } = scopes;
const [permissions, setPermissions] = useState<{ item: IPermission; index: number }[]>([]);
const [permissionsError, setPermissionsError] = useState<ScopesError | null>(error);

const styles = useStyles();
const tabHeight = convertVhToPx(dimensions.request.height, 110);

useEffect(() => {
if (error && error?.url.includes('permissions')) {
setPermissionsError(error);
}
}, [error]);

const openPermissionsPanel = () => {
showPermissions({
settings: {
title: translateMessage('Permissions'),
width: 'lg'
}
});
};

const getPermissions = (): void => {
dispatch(fetchScopes('query'));
fetchPermissionGrants();
};

const fetchPermissionGrants = (): void => {
if (tokenPresent) {
dispatch(fetchAllPrincipalGrants());
}
};

useEffect(() => {
if (validation.isValid) {
getPermissions();
}
}, [sampleQuery]);

useEffect(() => {
if (tokenPresent && validation.isValid) {
dispatch(fetchAllPrincipalGrants());
}
}, []);

useEffect(() => {
let updatedPermissions = scopes.data.specificPermissions || [];
updatedPermissions = sortPermissionsWithPrivilege(updatedPermissions);
updatedPermissions = setConsentedStatus(tokenPresent, updatedPermissions, consentedScopes);
setPermissions(updatedPermissions.map((item, index) => ({ item, index })));
}, [scopes.data.specificPermissions, tokenPresent, consentedScopes]);

const columns = getColumns({ source: 'tab', tokenPresent });

if (loading.isSpecificPermissions) {
return <Label className={styles.label}>{translateMessage('Fetching permissions')}...</Label>;
}

if (!validation.isValid) {
return <Label className={styles.label}>{translateMessage('Invalid URL')}!</Label>;
}

const displayNoPermissionsFoundMessage = (): JSX.Element => (
<Label className={styles.root}>
{translateMessage('permissions not found in permissions tab')}
<Link underline onClick={openPermissionsPanel}>
{translateMessage('open permissions panel')}
</Link>
{translateMessage('permissions list')}
</Label>
);

const displayNotSignedInMessage = (): JSX.Element => (
<Label className={styles.root}>
{translateMessage('sign in to view a list of all permissions')}
</Label>
);

const displayErrorFetchingPermissionsMessage = (): JSX.Element => (
<Label className={styles.errorLabel}>{translateMessage('Fetching permissions failing')}</Label>
);

if (!tokenPresent && permissions.length === 0) {
return displayNotSignedInMessage();
}

if (permissions.length === 0) {
return permissionsError?.status && (permissionsError?.status === 404 || permissionsError?.status === 400)
? displayNoPermissionsFoundMessage()
: displayErrorFetchingPermissionsMessage();
}

return (
<div>
<Label className={styles.permissionText}>
{translateMessage(tokenPresent ? 'permissions required to run the query' : 'sign in to consent to permissions')}
</Label>
<div className={styles.tableWrapper} style={{ height: tabHeight }}>
<Table aria-label={translateMessage('Permissions Table')}>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableCell key={column.columnId}>{column.renderHeaderCell()}</TableCell>
))}
</TableRow>
</TableHeader>
<TableBody>
{permissions.map(({ item, index }) => (
<TableRow key={item.value}>
{columns.map((column) => (
<TableCell key={column.columnId}>{column.renderCell({ item, index })}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
Loading
Loading