Skip to content

Commit

Permalink
Merge branch 'feature/download-api-manifest' into task/show-upload-co…
Browse files Browse the repository at this point in the history
…llections-button
  • Loading branch information
Onokaev committed Jul 10, 2023
2 parents e476d7a + 737178c commit b825d0d
Show file tree
Hide file tree
Showing 18 changed files with 527 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext } from 'react';

import { ResourcePath } from '../../../../types/resources';

interface CollectionPermissionsContext {
getPermissions: (paths: ResourcePath[]) => Promise<void>;
permissions: any[];
isFetching?: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-redeclare
export const CollectionPermissionsContext = createContext<CollectionPermissionsContext>(
{} as CollectionPermissionsContext
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useState } from 'react';

import { CollectionPermission, Method, ResourcePath } from '../../../../types/resources';
import { CollectionPermissionsContext } from './CollectionPermissionsContext';

const DEVX_API_URL = 'https://graphexplorerapi-staging.azurewebsites.net/api/permissions';

function getRequestsFromPaths(paths: ResourcePath[]) {
const requests: any[] = [];
paths.forEach(path => {
const { method, url } = path;
requests.push({
method: method as Method,
requestUrl: url
});
});
return requests;
}

async function getCollectionPermissions(paths: ResourcePath[]) {
const response = await fetch(DEVX_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(getRequestsFromPaths(paths))
});
const perms = await response.json();
if (!perms.results) {
throw new Error('No permissions found');
}
return perms;
}

const CollectionPermissionsProvider = ({ children }: any) => {
const [permissions, setPermissions] = useState<CollectionPermission[]>([]);
const [isFetching, setIsFetching] = useState(false);
const [code, setCode] = useState('');

async function getPermissions(items: ResourcePath[]): Promise<void> {
const hashCode = window.btoa(JSON.stringify([...items]));
if (hashCode !== code) {
try {
setIsFetching(true);
const perms = await getCollectionPermissions(items);
setPermissions(perms.results);
setIsFetching(false);
setCode(hashCode);
} catch (error) {
setIsFetching(false);
setPermissions([]);
}
}
}

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

export default CollectionPermissionsProvider
6 changes: 6 additions & 0 deletions src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ export const REVOKING_PERMISSIONS_REQUIRED_SCOPES = 'DelegatedPermissionGrant.Re
export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/security-authorization#:~:text=If%20you%27re%20calling%20the%20Microsoft%20Graph%20Security%20API%20from%20Graph%20Explorer'
// eslint-disable-next-line max-len
export const CONSENT_TYPE_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#:~:text=(eq%20only).-,consentType,-String'
export const GRAPH_BETA_DESCRIPTION_URL =
'https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/beta/openapi.yaml';
export const GRAPH_V1_DESCRIPTION_URL =
'https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml';
export const API_MANIFEST_SPEC_PAGE =
'https://darrelmiller.github.io/api-manifest/draft-miller-api-manifest.html#name-publisher-object';
7 changes: 7 additions & 0 deletions src/app/services/hooks/useCollectionPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useContext } from 'react';

import { CollectionPermissionsContext } from '../context/collection-permissions/CollectionPermissionsContext';

export const useCollectionPermissions = () => {
return useContext(CollectionPermissionsContext);
};
5 changes: 4 additions & 1 deletion src/app/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { QueryResponse } from './query-response';
import { QueryRunner } from './query-runner';
import { parse } from './query-runner/util/iframe-message-parser';
import { Sidebar } from './sidebar/Sidebar';
import CollectionPermissionsProvider from '../services/context/collection-permissions/CollectionPermissionsProvider';

export interface IAppProps {
theme?: ITheme;
Expand Down Expand Up @@ -484,7 +485,9 @@ class App extends Component<IAppProps, IAppState> {
<TermsOfUseMessage />
</div>
</div>
<PopupsWrapper />
<CollectionPermissionsProvider>
<PopupsWrapper />
</CollectionPermissionsProvider>
</PopupsProvider>
</ThemeContext.Provider>
);
Expand Down
14 changes: 9 additions & 5 deletions src/app/views/common/download.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { telemetry, eventTypes, componentNames } from '../../../telemetry';
import { telemetry, eventTypes } from '../../../telemetry';

export function downloadToLocal(content: any, filename: string) {
function downloadToLocal(content: any, filename: string) {
const blob = new Blob([JSON.stringify(content, null, 4)], {
type: 'text/json'
});
download(blob, filename);
trackDownload(filename);
}

function download(blob: Blob, filename: string) {
Expand All @@ -17,9 +16,14 @@ function download(blob: Blob, filename: string) {
document.body.removeChild(elem);
}

function trackDownload(filename: string) {
function trackDownload(filename: string, componentName: string) {
telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, {
ComponentName: componentNames.DOWNLOAD_POSTMAN_COLLECTION_BUTTON,
componentName,
filename
});
}

export {
downloadToLocal,
trackDownload
};
22 changes: 18 additions & 4 deletions src/app/views/common/popups/PanelWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getTheme, IOverlayProps, Panel, PanelType, Spinner } from '@fluentui/react';
import { getTheme, IconButton, IOverlayProps, Panel, PanelType, Spinner } from '@fluentui/react';
import { Suspense } from 'react';

import { useAppSelector } from '../../../../store';
import { translateMessage } from '../../../utils/translate-messages';
import { WrapperProps } from './popups.types';

export function PanelWrapper(props: WrapperProps) {
Expand Down Expand Up @@ -42,22 +43,35 @@ export function PanelWrapper(props: WrapperProps) {
const panelType = getPanelType();

const onRenderFooterContent = (): JSX.Element | null => {
return renderFooter? renderFooter() : null;
return renderFooter ? renderFooter() : null;
}

return (
<div>
<Panel
isOpen={isOpen}
onDismiss={() => dismissPopup()}
hasCloseButton={true}
hasCloseButton={false}
type={panelType}
headerText={headerText.toString()}
isFooterAtBottom={true}
isBlocking={true}
isLightDismiss={false}
closeButtonAriaLabel='Close'
overlayProps={panelOverlayProps}
onRenderFooterContent={onRenderFooterContent}
>
<IconButton
styles={{
root: {
float: 'right',
zIndex: 1,
marginTop: -30
}
}}
iconProps={{ iconName: 'Cancel' }}
ariaLabel={translateMessage('Close')}
onClick={() => dismissPopup()}
/>
<div>
{
<Suspense fallback={<Spinner />}>
Expand Down
7 changes: 6 additions & 1 deletion src/app/views/common/registry/popups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ export const popups = new Map<string, any>([
['share-query', lazy(() => import('../../query-runner/query-input/share-query/ShareQuery'))],
['theme-chooser', lazy(() => import('../../main-header/settings/ThemeChooser'))],
['preview-collection', lazy(() => import('../../sidebar/resource-explorer/collection/PreviewCollection'))],
['manifest-description', lazy(() => import('../../sidebar/resource-explorer/collection/ManifestDescription'))],
['collection-permissions', lazy(() => import('../../sidebar/resource-explorer/collection/CollectionPermissions'))],
['full-permissions', lazy(() => import('../../query-runner/request/permissions/Permissions.Full'))]
]);

export type PopupItem =
'share-query' |
'theme-chooser' |
'preview-collection' |
'full-permissions';
'full-permissions' |
'collection-permissions' |
'manifest-description'
;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { DefaultButton, DetailsList, DialogFooter, Label, PrimaryButton, SelectionMode } from '@fluentui/react';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';

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

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

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

const columns = [
{
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')
}
];

function downloadPermissions(): void {
const filename = 'collection-permissions.json';
downloadToLocal(permissions, filename);
trackDownload(filename, componentNames.DOWNLOAD_COLLECTION_PERMISSIONS_BUTTON);
}

useEffect(() => {
if (paths.length > 0) {
getPermissions(paths)
}
}, [paths]);

if (!isFetching && permissions.length === 0) {
return (
<Label style={{
display: 'flex',
width: '100%',
minHeight: '200px',
justifyContent: 'center',
alignItems: 'center'
}}>
<FormattedMessage id='permissions not found' />
</Label>
)
}

if (isFetching) {
return (
<Label>
<FormattedMessage id={'Fetching permissions'} />...
</Label>
)
}

return (
<>
<DetailsList
items={permissions}
columns={columns}
selectionMode={SelectionMode.none}
/>
{permissions.length > 0 &&
<DialogFooter styles={{ actionsRight: { justifyContent: 'start' } }}>
<PrimaryButton onClick={downloadPermissions}>
<FormattedMessage id='Download permissions' />
</PrimaryButton>
<DefaultButton onClick={() => props.dismissPopup()}>
<FormattedMessage id='Close' />
</DefaultButton>
</DialogFooter>}
</>
)
}

export default CollectionPermissions
Loading

0 comments on commit b825d0d

Please sign in to comment.