Skip to content

Commit

Permalink
Task: scope permissions (#2867)
Browse files Browse the repository at this point in the history
  • Loading branch information
thewahome authored Nov 6, 2023
1 parent 4561de9 commit 4a4003f
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 128 deletions.
9 changes: 8 additions & 1 deletion src/app/services/actions/collections-action-creators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AppAction } from '../../../types/action';
import {
COLLECTION_CREATE_SUCCESS,
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, RESOURCEPATHS_UPDATE_SUCCESS
} from '../redux-constants';

export function addResourcePaths(response: object): AppAction {
Expand All @@ -11,6 +11,13 @@ export function addResourcePaths(response: object): AppAction {
};
}

export function updateResourcePaths(response: object): AppAction {
return {
type: RESOURCEPATHS_UPDATE_SUCCESS,
response
};
}

export function createCollection(response: object): AppAction {
return {
type: COLLECTION_CREATE_SUCCESS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { useMemo, useState } from 'react';
import { ReactNode, useMemo, useState } from 'react';

import { CollectionPermission, Method, ResourcePath } from '../../../../types/resources';
import { getVersionsFromPaths } from '../../../views/sidebar/resource-explorer/collection/collection.util';
import {
getScopesFromPaths, getVersionsFromPaths, scopeOptions
} from '../../../views/sidebar/resource-explorer/collection/collection.util';
import { DEVX_API_URL } from '../../graph-constants';
import { CollectionPermissionsContext } from './CollectionPermissionsContext';

const DEVX_API_PERMISSIONS_URL = `${DEVX_API_URL}/api/permissions`;

function getRequestsFromPaths(paths: ResourcePath[], version: string) {
const requests: any[] = [];
interface CollectionRequest {
method: Method;
requestUrl: string;
}

function getRequestsFromPaths(paths: ResourcePath[], version: string, scope: string) {
const requests: CollectionRequest[] = [];
paths.forEach(path => {
const { method, url } = path;
if (version === path.version) {
path.scope = path.scope ?? scopeOptions[0].key;
if (version === path.version && scope === path.scope) {
requests.push({
method: method as Method,
requestUrl: url
Expand All @@ -23,22 +31,39 @@ function getRequestsFromPaths(paths: ResourcePath[], version: string) {

async function getCollectionPermissions(paths: ResourcePath[]): Promise<{ [key: string]: CollectionPermission[] }> {
const versions = getVersionsFromPaths(paths);
const scopes = getScopesFromPaths(paths);
const collectionPermissions: { [key: string]: CollectionPermission[] } = {};
const fetchPromises: Promise<Response>[] = [];

for (const version of versions) {
const response = await fetch(DEVX_API_PERMISSIONS_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(getRequestsFromPaths(paths, version))
});
const perms = await response.json();
collectionPermissions[version] = (perms.results) ? perms.results : [];
for (const scope of scopes) {
const requestPaths = getRequestsFromPaths(paths, version, scope);
if (requestPaths.length === 0) {
continue;
}
const url = `${DEVX_API_PERMISSIONS_URL}?version=${version}&scopeType=${scope}`;
fetchPromises.push(fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestPaths)
}));
}
}

const responses = await Promise.all(fetchPromises);

for (let i = 0; i < responses.length; i++) {
const perms = await responses[i].json();
const key = `${versions[Math.floor(i / scopes.length)]}-${scopes[i % scopes.length]}`;
collectionPermissions[key] = (perms.results) ? perms.results : [];
}

return collectionPermissions;
}

const CollectionPermissionsProvider = ({ children }: any) => {
const CollectionPermissionsProvider = ({ children }: { children: ReactNode }) => {
const [permissions, setPermissions] = useState<{ [key: string]: CollectionPermission[] } | undefined>(undefined);
const [isFetching, setIsFetching] = useState(false);
const [code, setCode] = useState('');
Expand All @@ -62,7 +87,7 @@ const CollectionPermissionsProvider = ({ children }: any) => {

return (
<CollectionPermissionsContext.Provider
value={{ getPermissions, ...valueObject}}>{children}</CollectionPermissionsContext.Provider>
value={{ getPermissions, ...valueObject }}>{children}</CollectionPermissionsContext.Provider>
);
}

Expand Down
26 changes: 19 additions & 7 deletions src/app/services/reducers/collections-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AppAction } from '../../../types/action';
import { Collection, ResourcePath } from '../../../types/resources';
import {
COLLECTION_CREATE_SUCCESS,
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, RESOURCEPATHS_UPDATE_SUCCESS
} from '../redux-constants';
import { getUniquePaths } from './collections-reducer.util';

Expand All @@ -11,22 +11,34 @@ const initialState: Collection[] = [];
export function collections(state: Collection[] = initialState, action: AppAction): Collection[] {
switch (action.type) {

case COLLECTION_CREATE_SUCCESS:
case COLLECTION_CREATE_SUCCESS: {
const items = [...state];
items.push(action.response);
return items;
}

case RESOURCEPATHS_ADD_SUCCESS:
case RESOURCEPATHS_ADD_SUCCESS: {
const index = state.findIndex(k => k.isDefault);
if (index > -1) {
const paths: ResourcePath[] = getUniquePaths(state[index].paths, action.response);
const context = [...state];
context[index].paths = paths;
return context;
}
return state
return state;
}

case RESOURCEPATHS_UPDATE_SUCCESS: {
const collectionIndex = state.findIndex(k => k.isDefault);
if (collectionIndex > -1) {
const context = [...state];
context[collectionIndex].paths = action.response;
return context;
}
return state;
}

case RESOURCEPATHS_DELETE_SUCCESS:
case RESOURCEPATHS_DELETE_SUCCESS: {
const indexOfDefaultCollection = state.findIndex(k => k.isDefault);
if (indexOfDefaultCollection > -1) {
const list: ResourcePath[] = [...state[indexOfDefaultCollection].paths];
Expand All @@ -38,9 +50,9 @@ export function collections(state: Collection[] = initialState, action: AppActio
newState[indexOfDefaultCollection].paths = list;
return newState;
}
return state
}

default:
return state;
}
}
}
1 change: 1 addition & 0 deletions src/app/services/redux-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const GET_POLICY_ERROR = 'GET_POLICY_ERROR';
export const GET_POLICY_PENDING = 'GET_POLICY_PENDING';
export const RESOURCEPATHS_ADD_SUCCESS = 'RESOURCEPATHS_ADD_SUCCESS';
export const RESOURCEPATHS_DELETE_SUCCESS = 'RESOURCEPATHS_DELETE_SUCCESS';
export const RESOURCEPATHS_UPDATE_SUCCESS = 'RESOURCEPATHS_UPDATE_SUCCESS';
export const BULK_ADD_HISTORY_ITEMS_SUCCESS = 'BULK_ADD_HISTORY_ITEMS_SUCCESS';
export const SET_SNIPPET_TAB_SUCCESS = 'SET_SNIPPET_TAB_SUCCESS';
export const GET_ALL_PRINCIPAL_GRANTS_PENDING = 'GET_ALL_PRINCIPAL_GRANTS_PENDING';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { DefaultButton, DetailsList, DialogFooter, Label, PrimaryButton, SelectionMode } from '@fluentui/react';
import React, { useEffect } from 'react';
import { DefaultButton, DetailsList, DialogFooter, IGroup, Label, PrimaryButton, SelectionMode } from '@fluentui/react';
import { FC, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';

import { useAppSelector } from '../../../../../store';
import { componentNames } from '../../../../../telemetry';
import { CollectionPermission } from '../../../../../types/resources';
import { PopupsComponent } from '../../../../services/context/popups-context';
import { useCollectionPermissions } from '../../../../services/hooks/useCollectionPermissions';
import { generateGroupsFromList } from '../../../../utils/generate-groups';
import { translateMessage } from '../../../../utils/translate-messages';
import { downloadToLocal, trackDownload } from '../../../common/download';

const CollectionPermissions: React.FC<PopupsComponent<null>> = (props) => {
const CollectionPermissions: FC<PopupsComponent<null>> = (props) => {
const { getPermissions, permissions, isFetching } = useCollectionPermissions();

const { collections } = useAppSelector(
Expand All @@ -23,10 +24,6 @@ const CollectionPermissions: React.FC<PopupsComponent<null>> = (props) => {
key: 'value', name: translateMessage('Value'), fieldName: 'value',
minWidth: 300,
ariaLabel: translateMessage('Value')
},
{
key: 'scopeType', name: translateMessage('Scope Type'), fieldName: 'scopeType', minWidth: 200,
ariaLabel: translateMessage('Scope Type')
}
];

Expand Down Expand Up @@ -65,17 +62,20 @@ const CollectionPermissions: React.FC<PopupsComponent<null>> = (props) => {
}

const permissionsArray: CollectionPermission[] = [];
let groups: IGroup[] | undefined = [];
if (permissions) {
Object.keys(permissions).forEach(key => {
permissionsArray.push(...permissions[key]);
});
groups = generateGroupsFromList(permissionsArray, 'scopeType')
}

return (
<>
<DetailsList
items={permissionsArray}
columns={columns}
groups={groups}
selectionMode={SelectionMode.none}
/>
{permissions &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import {
ChoiceGroup,
FontSizes, FontWeights, IChoiceGroupOption, Link,
FontSizes, FontWeights,
Link,
PrimaryButton,
Spinner,
Stack,
VerticalDivider, getTheme, mergeStyleSets
} from '@fluentui/react';
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';

import { useAppSelector } from '../../../../../store';
import { componentNames, eventTypes, telemetry } from '../../../../../telemetry';
import { APIManifest } from '../../../../../types/api-manifest';
import { PopupsComponent } from '../../../../services/context/popups-context';
import { API_MANIFEST_SPEC_PAGE, PERMS_SCOPE } from '../../../../services/graph-constants';
import { API_MANIFEST_SPEC_PAGE } from '../../../../services/graph-constants';
import { useCollectionPermissions } from '../../../../services/hooks/useCollectionPermissions';
import { translateMessage } from '../../../../utils/translate-messages';
import { trackedGenericCopy } from '../../../common/copy';
import { downloadToLocal, trackDownload } from '../../../common/download';
import { generateAPIManifest } from './api-manifest.util';


const ManifestDescription: React.FC<PopupsComponent<null>> = () => {
const { permissions, isFetching } = useCollectionPermissions();
const ManifestDescription: FC<PopupsComponent<null>> = () => {
const { permissions, isFetching, getPermissions } = useCollectionPermissions();
const [manifest, setManifest] = useState<APIManifest>();
const [isGeneratingManifest, setIsGeneratingManifest] = useState<boolean>(false);
const [selectedScope, setSelectedScope] = useState<string>('');
const [manifestCopied, setManifestCopied] = useState<boolean>(false);

const manifestStyle = mergeStyleSets(
Expand Down Expand Up @@ -69,38 +66,27 @@ const ManifestDescription: React.FC<PopupsComponent<null>> = () => {
}
);

const options: IChoiceGroupOption[] = [
{
key: `${PERMS_SCOPE.WORK}`,
text: translateMessage('Delegated work'),
disabled: isGeneratingManifest
},
{
key: `${PERMS_SCOPE.APPLICATION}`,
text: translateMessage('Application permissions'),
disabled: isGeneratingManifest
},
{
key: `${PERMS_SCOPE.APPLICATION}_${PERMS_SCOPE.WORK}`,
text: translateMessage('Delegated & application permissions'),
disabled: isGeneratingManifest
}
];

const { collections } = useAppSelector(
(state) => state
);
const paths = collections ? collections.find(k => k.isDefault)!.paths : [];

useEffect(() => {
if (!isFetching && selectedScope !== '') {
const generatedManifest = generateAPIManifest({ paths, permissions, scopeType: selectedScope });
if (paths.length > 0) {
getPermissions(paths);
}
}, [paths]);

useEffect(() => {
if (permissions && paths.length > 0) {
setIsGeneratingManifest(true);
const generatedManifest = generateAPIManifest({ paths, permissions });
if (Object.keys(generatedManifest).length > 0) {
setIsGeneratingManifest(false);
setManifest(generatedManifest);
}
setManifest(generatedManifest);
}
}, [selectedScope, isFetching]);
}, [permissions]);

const downloadManifest = () => {
if (!manifest) { return; }
Expand All @@ -119,13 +105,6 @@ const ManifestDescription: React.FC<PopupsComponent<null>> = () => {
setManifestCopied(false);
}

const onSelectionChange = useCallback((ev: FormEvent<HTMLElement | HTMLInputElement> | undefined,
option: IChoiceGroupOption | undefined) => {
setSelectedScope(option!.key);
setIsGeneratingManifest(true);
setManifestCopied(false);
}, []);

const copyManifestToClipboard = () => {
if (!manifest) { return; }
const base64UrlEncodedManifest = btoa(JSON.stringify(manifest));
Expand Down Expand Up @@ -154,14 +133,6 @@ const ManifestDescription: React.FC<PopupsComponent<null>> = () => {
<br />
<VerticalDivider />

<FormattedMessage id='Permissions choice' />
<ChoiceGroup options={options}
onChange={onSelectionChange} label=''
styles={{ flexContainer: manifestStyle.permissionsButtons }}
/>

<VerticalDivider />

<FormattedMessage id='To generate client' />
<br />
<FormattedMessage id='Use VS Code' />
Expand All @@ -173,11 +144,9 @@ const ManifestDescription: React.FC<PopupsComponent<null>> = () => {
&nbsp;
<FormattedMessage id='VS Code extension' />
<br />
<br />

<VerticalDivider />
<br />
<br />
<FormattedMessage id='Use Kiota CLI' />
<Link
href='https://aka.ms/get/kiota'
Expand All @@ -200,23 +169,25 @@ const ManifestDescription: React.FC<PopupsComponent<null>> = () => {
</div>
<VerticalDivider />
<br />
{isFetching && <>
<Stack horizontal className={manifestStyle.actionButtons}> Fetching permissions<Spinner /></Stack>
<br />
</>}
<Stack horizontal className={manifestStyle.actionButtons}>
<PrimaryButton
onClick={copyManifestToClipboard}
disabled={selectedScope === '' || isGeneratingManifest || isFetching || manifestCopied}
disabled={isGeneratingManifest || isFetching || manifestCopied}
>
<FormattedMessage id='Copy to the clipboard' />
</PrimaryButton>

<PrimaryButton disabled={selectedScope === '' || isGeneratingManifest || isFetching || !manifestCopied}
<PrimaryButton disabled={isGeneratingManifest || isFetching || !manifestCopied}
onClick={openManifestInVisualStudio}>
{isGeneratingManifest && <> Fetching permissions&nbsp;&nbsp; <Spinner /></>}
{!isGeneratingManifest && <FormattedMessage id='Open in VS Code' />}
</PrimaryButton>

<PrimaryButton disabled={selectedScope === '' || isGeneratingManifest || isFetching}
<PrimaryButton disabled={isGeneratingManifest || isFetching}
onClick={downloadManifest}>
{isGeneratingManifest && <> Fetching permissions&nbsp;&nbsp; <Spinner /></>}
{!isGeneratingManifest && <FormattedMessage id='Download API Manifest' />}
</PrimaryButton>
</Stack>
Expand Down
Loading

0 comments on commit 4a4003f

Please sign in to comment.