Skip to content

Commit

Permalink
feat: API Permissions and Collections redesign (#3391)
Browse files Browse the repository at this point in the history
  • Loading branch information
ElinorW authored Dec 2, 2024
1 parent a5079d6 commit d239f8f
Show file tree
Hide file tree
Showing 38 changed files with 1,281 additions and 469 deletions.
10 changes: 8 additions & 2 deletions src/app/services/actions/autocomplete-action-creators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,20 @@ const mockState: ApplicationState = {
permissions: [],
error: null
},
collections: [],
collections: {
collections: [],
saved: false
},
proxyUrl: ''
}

store.getState = () => ({
...mockState,
proxyUrl: '',
collections: [],
collections: {
collections: [],
saved: false
},
graphExplorerMode: Mode.Complete,
queryRunnerStatus: null,
samples: {
Expand Down
5 changes: 4 additions & 1 deletion src/app/services/actions/permissions-action-creator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ const mockState: ApplicationState = {
permissions: [],
error: null
},
collections: [],
collections: {
collections: [],
saved: false
},
proxyUrl: ''
}
const currentState = store.getState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ const mockState: ApplicationState = {
data: {},
error: null
},
collections: [],
collections: {
collections: [],
saved: false
},
proxyUrl: ''
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext } from 'react';

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

interface CollectionPermissionsContext {
getPermissions: (paths: ResourcePath[]) => Promise<void>;
permissions?: { [key: string]: CollectionPermission[] };
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,91 @@
import { ReactNode, useMemo, useState } from 'react';

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

interface CollectionRequest {
method: Method;
requestUrl: string;
}

function getRequestsFromPaths(paths: ResourcePath[], version: string, scope: string) {
const requests: CollectionRequest[] = [];
paths.forEach(path => {
const { method, url } = path;
const pathScope = path.scope ?? scopeOptions[0].key;
if (version === path.version && scope === pathScope) {
requests.push({
method: method as Method,
requestUrl: url
});
}
});
return requests;
}

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

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

const CollectionPermissionsProvider = ({ children }: { children: ReactNode }) => {
const { baseUrl } = useAppSelector((state) => state.devxApi);
const [permissions, setPermissions] = useState<{ [key: string]: CollectionPermission[] } | undefined>(undefined);
const [isFetching, setIsFetching] = useState(false);
const [code, setCode] = useState('');

const getPermissions = async (items: ResourcePath[]): Promise<void> => {
const hashCode = window.btoa(JSON.stringify([...items]));
if (hashCode !== code) {
try {
setIsFetching(true);
const perms = await getCollectionPermissions(`${baseUrl}/permissions`, items);
setPermissions(perms);
setCode(hashCode);
} catch (error) {
setPermissions(undefined);
} finally {
setIsFetching(false);
}
}
};

const contextValue = useMemo(
() => ({ getPermissions, permissions, isFetching }),
[getPermissions, permissions, isFetching]
);

return (
<CollectionPermissionsContext.Provider value={contextValue}>
{children}
</CollectionPermissionsContext.Provider>
);
};

export default CollectionPermissionsProvider;
2 changes: 1 addition & 1 deletion src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s
// 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 CURRENT_THEME='CURRENT_THEME';
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas'
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas';
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);
};
64 changes: 46 additions & 18 deletions src/app/services/slices/collections.slice.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Collection, ResourcePath } from '../../../types/resources';

const initialState: Collection[] = [];
interface CollectionsState {
collections: Collection[];
saved: boolean;
}

const initialState: CollectionsState = {
collections: [],
saved: false
};

const collections = createSlice({
name: 'collections',
initialState,
reducers: {
createCollection: (state, action: PayloadAction<Collection>) => {
state.push(action.payload);
return state
state.collections.push(action.payload);
state.saved = false;
},
addResourcePaths:(state, action: PayloadAction<ResourcePath[]>) => {
const index = state.findIndex(collection => collection.isDefault);
addResourcePaths: (state, action: PayloadAction<ResourcePath[]>) => {
const index = state.collections.findIndex(collection => collection.isDefault);
if (index > -1) {
state[index].paths.push(...action.payload)
state.collections[index].paths.push(...action.payload);
state.saved = false;
}
},
updateResourcePaths: (state, action: PayloadAction<ResourcePath[]>) => {
const collectionIndex = state.collections.findIndex(k => k.isDefault);
if (collectionIndex > -1) {
state.collections[collectionIndex] = {
...state.collections[collectionIndex],
paths: action.payload
};
state.saved = true;
}
},
removeResourcePaths: (state, action: PayloadAction<ResourcePath[]>)=>{
const index = state.findIndex(collection => collection.isDefault);
if(index > -1) {
const defaultResourcePaths = [...state[index].paths];
action.payload.forEach((resourcePath: ResourcePath)=>{
const delIndex = defaultResourcePaths.findIndex(p=>p.key === resourcePath.key)
removeResourcePaths: (state, action: PayloadAction<ResourcePath[]>) => {
const index = state.collections.findIndex(collection => collection.isDefault);
if (index > -1) {
const defaultResourcePaths = [...state.collections[index].paths];
action.payload.forEach((resourcePath: ResourcePath) => {
const delIndex = defaultResourcePaths.findIndex(p => p.key === resourcePath.key);
if (delIndex > -1) {
defaultResourcePaths.splice(delIndex, 1)
defaultResourcePaths.splice(delIndex, 1);
}
})
state[index].paths = defaultResourcePaths;
});
state.collections[index].paths = defaultResourcePaths;
state.saved = false;
}
},
resetSaveState: (state) => {
state.saved = false;
}
}
})
});

export const {createCollection, addResourcePaths, removeResourcePaths} = collections.actions
export const
{ createCollection,
addResourcePaths,
updateResourcePaths,
removeResourcePaths,
resetSaveState } = collections.actions;

export default collections.reducer
export default collections.reducer;
2 changes: 1 addition & 1 deletion src/app/utils/searchbox.styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const searchBoxStyles: any = () => ({
root: {
width: '97%'
width: '100%'
},
field: [
{
Expand Down
8 changes: 5 additions & 3 deletions src/app/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Mode } from '../../types/enums';
import { IInitMessage, IQuery, IThemeChangedMessage } from '../../types/query-runner';
import { ISharedQueryParams } from '../../types/share-query';
import { ISidebarProps } from '../../types/sidebar';
import CollectionPermissionsProvider from '../services/context/collection-permissions/CollectionPermissionsProvider';
import { PopupsProvider } from '../services/context/popups-context';
import { ValidationProvider } from '../services/context/validation-context/ValidationProvider';
import { GRAPH_URL } from '../services/graph-constants';
Expand All @@ -26,12 +27,11 @@ import { changeTheme } from '../services/slices/theme.slice';
import { parseSampleUrl } from '../utils/sample-url-generation';
import { substituteTokens } from '../utils/token-helpers';
import { translateMessage } from '../utils/translate-messages';
import { TermsOfUseMessage } from './app-sections';
import { StatusMessages, TermsOfUseMessage } from './app-sections';
import { headerMessaging } from './app-sections/HeaderMessaging';
import { appStyles } from './App.styles';
import { classNames } from './classnames';
import { KeyboardCopyEvent } from './common/copy-button/KeyboardCopyEvent';
import { StatusMessages } from './common/lazy-loader/component-registry';
import PopupsWrapper from './common/popups/PopupsWrapper';
import { createShareLink } from './common/share';
import { MainHeader } from './main-header/MainHeader';
Expand Down Expand Up @@ -492,7 +492,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
};
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,4 @@ export const ResourceExplorer = (props?: any) => {
return (
<LazyResourceExplorer {...props} />
)
}

}
12 changes: 9 additions & 3 deletions src/app/views/common/lazy-loader/component-registry/popups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import { lazy } from 'react';
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'))],
['full-permissions', lazy(() => import('../../../query-runner/request/permissions/Permissions.Full'))]
['preview-collection', lazy(() => import('../../../sidebar/resource-explorer/collection/APICollection'))],
['full-permissions', lazy(() => import('../../../query-runner/request/permissions/Permissions.Full'))],
['collection-permissions', lazy(() => import('../../../sidebar/resource-explorer/collection/CollectionPermissions'))],
['edit-collection-panel', lazy(() => import('../../../sidebar/resource-explorer/collection/EditCollectionPanel'))],
['edit-scope-panel', lazy(() => import('../../../sidebar/resource-explorer/collection/EditScopePanel'))]
]);

export type PopupItem =
'share-query' |
'theme-chooser' |
'preview-collection' |
'full-permissions';
'full-permissions' |
'collection-permissions' |
'edit-collection-panel' |
'edit-scope-panel'
Loading

0 comments on commit d239f8f

Please sign in to comment.