Skip to content

Commit

Permalink
Feature: filter permissions by consented state (#2869)
Browse files Browse the repository at this point in the history
  • Loading branch information
thewahome authored Nov 29, 2023
1 parent bc4fbe8 commit 7e77876
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 55 deletions.
3 changes: 3 additions & 0 deletions src/app/utils/searchbox.styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const searchBoxStyles: any = () => ({
root: {
width: '97%'
},
field: [
{
paddingLeft: 10
Expand Down
182 changes: 129 additions & 53 deletions src/app/views/query-runner/request/permissions/Permissions.Full.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {
Announced, DetailsList, DetailsListLayoutMode, getTheme, GroupHeader, IColumn,
IGroup, Label, SearchBox, SelectionMode, TooltipHost
Announced, DetailsList, DetailsListLayoutMode, getId, getTheme, GroupHeader, IColumn,
IconButton,
IContextualMenuProps,
Label, SearchBox, SelectionMode, Stack, TooltipHost
} from '@fluentui/react';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';

import { AppDispatch, useAppSelector } from '../../../../../store';
import { componentNames, eventTypes, telemetry } from '../../../../../telemetry';
import { SortOrder } from '../../../../../types/enums';
import { IPermission } from '../../../../../types/permissions';
import { fetchAllPrincipalGrants, fetchScopes } from '../../../../services/actions/permissions-action-creator';
Expand All @@ -20,23 +23,25 @@ import { permissionStyles } from './Permission.styles';
import PermissionItem from './PermissionItem';
import { setConsentedStatus } from './util';

type Filter = 'all-permissions' | 'consented-permissions' | 'unconsented-permissions';
interface PermissionListItem extends IPermission {
groupName?: string;
}

const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {
const theme = getTheme();
const dispatch: AppDispatch = useDispatch();
const [filter, setFilter] = useState<Filter>('all-permissions');

const { panelContainer: panelStyles, tooltipStyles, detailsHeaderStyles } = permissionStyles(theme);
const { consentedScopes, scopes, authToken } = useAppSelector((state) => state);
const { fullPermissions } = scopes.data;
const tokenPresent = !!authToken.token;
const loading = scopes.pending.isFullPermissions;

const [permissions, setPermissions] = useState<any[]>([]);
const [groups, setGroups] = useState<IGroup[]>([]);
const [searchStarted, setSearchStarted] = useState(false);
const [permissions, setPermissions] = useState<IPermission[]>([]);
const [searchValue, setSearchValue] = useState<string>('');

const permissionsList: any[] = [];

const getPermissions = (): void => {
dispatch(fetchScopes());
fetchPermissionGrants();
Expand Down Expand Up @@ -78,33 +83,21 @@ const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {
}
}, [scopes.data]);

const shouldGenerateGroups = useRef(true)

useEffect(() => {
if (shouldGenerateGroups.current) {
setGroups(generateGroupsFromList(permissionsList, 'groupName'));
if (groups && groups.length > 0) {
shouldGenerateGroups.current = false;
}
if (permissionsList.length === 0) { return }
}
}, [permissions, searchStarted])


setConsentedStatus(tokenPresent, permissions, consentedScopes);

permissions.forEach((perm: IPermission) => {
const permission: any = { ...perm };
const permissionValue = permission.value;
const groupName = permissionValue.split('.')[0];
permission.groupName = groupName;
permissionsList.push(permission);
});

const searchValueChanged = (event: any, value?: string): void => {
const searchValueChanged = (value?: string): void => {
setSearchValue(value!);
shouldGenerateGroups.current = true;
setSearchStarted((search) => !search);
const searchResults = searchPermissions(value);
const values = filter === 'all-permissions' ? searchResults : searchResults.filter((permission: IPermission) => {
if (filter === 'consented-permissions') {
return permission.consented;
}
return !permission.consented;
});
setPermissions(values);
};

const searchPermissions = (value?: string) => {
let filteredPermissions = scopes.data.fullPermissions;
if (value) {
const keyword = value.toLowerCase();
Expand All @@ -115,9 +108,8 @@ const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {
return name.includes(keyword) || groupName.includes(keyword);
});
}
setPermissions(filteredPermissions);
};

return filteredPermissions;
}

const onRenderGroupHeader = (props: any): JSX.Element | null => {
if (props) {
Expand All @@ -129,7 +121,6 @@ const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {
return null;
};


const groupHeaderStyles = () => {
return {
check: { display: 'none' },
Expand All @@ -146,7 +137,67 @@ const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {

const clearSearchBox = () => {
setSearchValue('');
searchValueChanged({}, '');
searchValueChanged('');
}

const chooseFilter = (chosenFilter: Filter) => {
setFilter(chosenFilter);
switch (chosenFilter) {
case 'all-permissions': {
setPermissions(searchPermissions(searchValue));
break;
}
case 'consented-permissions': {
setPermissions(searchPermissions(searchValue)
.filter((permission: IPermission) => permission.consented));
break;
}
case 'unconsented-permissions': {
setPermissions(searchPermissions(searchValue)
.filter((permission: IPermission) => !permission.consented));
break;
}
}
}

const handleRenderItemColumn = (item?: IPermission, index?: number, column?: IColumn) => {
return <PermissionItem column={column} index={index} item={item!} />;
}

const columns = getColumns({ source: 'panel', tokenPresent });
const permissionsList: PermissionListItem[] = [];
permissions.map((perm: IPermission) => {
const permission: PermissionListItem = { ...perm };
const permissionValue = permission.value;
permission.groupName = permissionValue.split('.')[0];
permissionsList.push(permission);
});
const groups = generateGroupsFromList(permissionsList, 'groupName');

const menuProperties: IContextualMenuProps = {
items: [
{
key: 'all-permissions',
text: translateMessage('All permissions'),
onClick: () => chooseFilter('all-permissions')
},
{
key: 'consented-permissions',
text: translateMessage('Consented permissions'),
onClick: () => chooseFilter('consented-permissions')
},
{
key: 'unconsented-permissions',
text: translateMessage('Unconsented permissions'),
onClick: () => chooseFilter('unconsented-permissions')
}
]
};

const trackFilterButtonClickEvent = () => {
telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, {
ComponentName: componentNames.FILTER_PERMISSIONS_BUTTON
});
}

return (
Expand All @@ -159,24 +210,49 @@ const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {
<FormattedMessage id='Select different permissions' />
</Label>
<hr />
<SearchBox
placeholder={translateMessage('Search permissions')}
onChange={(event?: React.ChangeEvent<HTMLInputElement>, newValue?: string) =>
searchValueChanged(event, newValue)}
styles={searchBoxStyles}
onClear={() => clearSearchBox()}
value={searchValue}
/>
<Announced message={`${permissions.length} search results available.`} />
<Stack horizontal tokens={{ childrenGap: 7 }}>

<TooltipHost
content={
<div style={{ padding: '3px' }}>
{translateMessage('Filter permissions')}
</div>}
id={getId()}
calloutProps={{ gapSpace: 0 }}
styles={tooltipStyles}
>
<IconButton
ariaLabel={translateMessage('Filter permissions')}
role='button'
disabled={loading || fullPermissions.length === 0}
menuIconProps={{ iconName: filter === 'all-permissions' ? 'Filter' : 'FilterSolid' }}
menuProps={menuProperties}
onMenuClick={trackFilterButtonClickEvent}
styles={{
root: {
float: 'left',
width: '100%'
}
}}
/>
</TooltipHost>
<SearchBox
placeholder={translateMessage('Search permissions')}
onChange={(_event?: React.ChangeEvent<HTMLInputElement>, newValue?: string) =>
searchValueChanged(newValue)}
styles={searchBoxStyles}
onClear={() => clearSearchBox()}
value={searchValue}
/>
<Announced message={`${permissions.length} search results available.`} />
</Stack>
<hr />
<DetailsList
onShouldVirtualize={() => false}
items={permissions}
columns={getColumns('panel', tokenPresent)}
columns={columns}
groups={groups}
onRenderItemColumn={(item?: any, index?: number, column?: IColumn) => {
return <PermissionItem column={column} index={index} item={item} />
}}
onRenderItemColumn={handleRenderItemColumn}
selectionMode={SelectionMode.multiple}
layoutMode={DetailsListLayoutMode.justified}
compact={true}
Expand Down Expand Up @@ -205,9 +281,9 @@ const FullPermissions: React.FC<PopupsComponent<null>> = (): JSX.Element => {
<FormattedMessage id='permissions not found' />
</Label> :
!loading && permissions && permissions.length === 0 && scopes.error && scopes.error.error &&
<Label>
<FormattedMessage id='Fetching permissions failing' />
</Label>
<Label>
<FormattedMessage id='Fetching permissions failing' />
</Label>
}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const Permissions = (permissionProps?: IPermissionProps): JSX.Element =>
{ height: tabHeight, overflowX: 'hidden', overflowY: 'auto' }
} : { root: { height: tabHeight, overflowY: 'auto' } }}
items={permissions}
columns={getColumns('tab', tokenPresent)}
columns={getColumns({ source: 'tab', tokenPresent })}
onRenderItemColumn={(item?: any, index?: number, column?: IColumn) => {
return <PermissionItem column={column} index={index} item={item} />
}}
Expand Down
8 changes: 7 additions & 1 deletion src/app/views/query-runner/request/permissions/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { permissionStyles } from './Permission.styles';

type source = 'panel' | 'tab';

interface ColumnProps {
source: source;
tokenPresent?: boolean;
onColumnClicked?: (ev: React.MouseEvent<HTMLElement>, column: IColumn) => void;
}

const trackLinkClickedEvent = (link: string, componentName: string) => {
telemetry.trackLinkClickEvent(link, componentName);
}
Expand All @@ -25,7 +31,7 @@ const openExternalWebsite = (url: string) => {
}
}

const getColumns = (source: source, tokenPresent?: boolean): IColumn[] => {
const getColumns = ({ source, tokenPresent, onColumnClicked }: ColumnProps): IColumn[] => {

const theme = getTheme();
const { columnCellStyles, cellTitleStyles } = permissionStyles(theme);
Expand Down
1 change: 1 addition & 0 deletions src/telemetry/component-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const HELP_BUTTON = 'Help button';
export const SIGN_IN_BUTTON = 'Sign in button';
export const SIGN_IN_WITH_OTHER_ACCOUNT_BUTTON = 'Sign in with other account button';
export const REVOKE_PERMISSION_CONSENT_BUTTON = 'Revoke consent to permissions button'
export const FILTER_PERMISSIONS_BUTTON = 'Filter permissions button';

// List items
export const HISTORY_LIST_ITEM = 'History list item';
Expand Down

0 comments on commit 7e77876

Please sign in to comment.