Skip to content

Commit d239f8f

Browse files
authored
feat: API Permissions and Collections redesign (#3391)
1 parent a5079d6 commit d239f8f

38 files changed

+1281
-469
lines changed

src/app/services/actions/autocomplete-action-creators.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,20 @@ const mockState: ApplicationState = {
103103
permissions: [],
104104
error: null
105105
},
106-
collections: [],
106+
collections: {
107+
collections: [],
108+
saved: false
109+
},
107110
proxyUrl: ''
108111
}
109112

110113
store.getState = () => ({
111114
...mockState,
112115
proxyUrl: '',
113-
collections: [],
116+
collections: {
117+
collections: [],
118+
saved: false
119+
},
114120
graphExplorerMode: Mode.Complete,
115121
queryRunnerStatus: null,
116122
samples: {

src/app/services/actions/permissions-action-creator.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ const mockState: ApplicationState = {
124124
permissions: [],
125125
error: null
126126
},
127-
collections: [],
127+
collections: {
128+
collections: [],
129+
saved: false
130+
},
128131
proxyUrl: ''
129132
}
130133
const currentState = store.getState();

src/app/services/actions/resource-explorer-action-creators.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ const mockState: ApplicationState = {
9999
data: {},
100100
error: null
101101
},
102-
collections: [],
102+
collections: {
103+
collections: [],
104+
saved: false
105+
},
103106
proxyUrl: ''
104107
}
105108

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createContext } from 'react';
2+
3+
import { CollectionPermission, ResourcePath } from '../../../../types/resources';
4+
5+
interface CollectionPermissionsContext {
6+
getPermissions: (paths: ResourcePath[]) => Promise<void>;
7+
permissions?: { [key: string]: CollectionPermission[] };
8+
isFetching?: boolean;
9+
}
10+
11+
// eslint-disable-next-line @typescript-eslint/no-redeclare
12+
export const CollectionPermissionsContext = createContext<CollectionPermissionsContext>(
13+
{} as CollectionPermissionsContext
14+
);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ReactNode, useMemo, useState } from 'react';
2+
3+
import { CollectionPermission, Method, ResourcePath } from '../../../../types/resources';
4+
import {
5+
getScopesFromPaths, getVersionsFromPaths, scopeOptions
6+
} from '../../../views/sidebar/resource-explorer/collection/collection.util';
7+
import { CollectionPermissionsContext } from './CollectionPermissionsContext';
8+
import { useAppSelector } from '../../../../store';
9+
10+
interface CollectionRequest {
11+
method: Method;
12+
requestUrl: string;
13+
}
14+
15+
function getRequestsFromPaths(paths: ResourcePath[], version: string, scope: string) {
16+
const requests: CollectionRequest[] = [];
17+
paths.forEach(path => {
18+
const { method, url } = path;
19+
const pathScope = path.scope ?? scopeOptions[0].key;
20+
if (version === path.version && scope === pathScope) {
21+
requests.push({
22+
method: method as Method,
23+
requestUrl: url
24+
});
25+
}
26+
});
27+
return requests;
28+
}
29+
30+
async function getCollectionPermissions(permissionsUrl: string, paths: ResourcePath[]):
31+
Promise<{ [key: string]: CollectionPermission[] }> {
32+
const versions = getVersionsFromPaths(paths);
33+
const scopes = getScopesFromPaths(paths);
34+
const collectionPermissions: { [key: string]: CollectionPermission[] } = {};
35+
36+
for (const version of versions) {
37+
for (const scope of scopes) {
38+
const requestPaths = getRequestsFromPaths(paths, version, scope);
39+
if (requestPaths.length === 0) {
40+
continue;
41+
}
42+
const url = `${permissionsUrl}?version=${version}&scopeType=${scope}`;
43+
const response = await fetch(url, {
44+
method: 'POST',
45+
headers: {
46+
'Content-Type': 'application/json'
47+
},
48+
body: JSON.stringify(requestPaths)
49+
});
50+
const perms = await response.json();
51+
collectionPermissions[`${version}-${scope}`] = (perms.results) ? perms.results : [];
52+
}
53+
}
54+
return collectionPermissions;
55+
}
56+
57+
const CollectionPermissionsProvider = ({ children }: { children: ReactNode }) => {
58+
const { baseUrl } = useAppSelector((state) => state.devxApi);
59+
const [permissions, setPermissions] = useState<{ [key: string]: CollectionPermission[] } | undefined>(undefined);
60+
const [isFetching, setIsFetching] = useState(false);
61+
const [code, setCode] = useState('');
62+
63+
const getPermissions = async (items: ResourcePath[]): Promise<void> => {
64+
const hashCode = window.btoa(JSON.stringify([...items]));
65+
if (hashCode !== code) {
66+
try {
67+
setIsFetching(true);
68+
const perms = await getCollectionPermissions(`${baseUrl}/permissions`, items);
69+
setPermissions(perms);
70+
setCode(hashCode);
71+
} catch (error) {
72+
setPermissions(undefined);
73+
} finally {
74+
setIsFetching(false);
75+
}
76+
}
77+
};
78+
79+
const contextValue = useMemo(
80+
() => ({ getPermissions, permissions, isFetching }),
81+
[getPermissions, permissions, isFetching]
82+
);
83+
84+
return (
85+
<CollectionPermissionsContext.Provider value={contextValue}>
86+
{children}
87+
</CollectionPermissionsContext.Provider>
88+
);
89+
};
90+
91+
export default CollectionPermissionsProvider;

src/app/services/graph-constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s
3232
// eslint-disable-next-line max-len
3333
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'
3434
export const CURRENT_THEME='CURRENT_THEME';
35-
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas'
35+
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { useContext } from 'react';
2+
3+
import { CollectionPermissionsContext } from '../context/collection-permissions/CollectionPermissionsContext';
4+
5+
export const useCollectionPermissions = () => {
6+
return useContext(CollectionPermissionsContext);
7+
};
Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,66 @@
11
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
22
import { Collection, ResourcePath } from '../../../types/resources';
33

4-
const initialState: Collection[] = [];
4+
interface CollectionsState {
5+
collections: Collection[];
6+
saved: boolean;
7+
}
8+
9+
const initialState: CollectionsState = {
10+
collections: [],
11+
saved: false
12+
};
513

614
const collections = createSlice({
715
name: 'collections',
816
initialState,
917
reducers: {
1018
createCollection: (state, action: PayloadAction<Collection>) => {
11-
state.push(action.payload);
12-
return state
19+
state.collections.push(action.payload);
20+
state.saved = false;
1321
},
14-
addResourcePaths:(state, action: PayloadAction<ResourcePath[]>) => {
15-
const index = state.findIndex(collection => collection.isDefault);
22+
addResourcePaths: (state, action: PayloadAction<ResourcePath[]>) => {
23+
const index = state.collections.findIndex(collection => collection.isDefault);
1624
if (index > -1) {
17-
state[index].paths.push(...action.payload)
25+
state.collections[index].paths.push(...action.payload);
26+
state.saved = false;
27+
}
28+
},
29+
updateResourcePaths: (state, action: PayloadAction<ResourcePath[]>) => {
30+
const collectionIndex = state.collections.findIndex(k => k.isDefault);
31+
if (collectionIndex > -1) {
32+
state.collections[collectionIndex] = {
33+
...state.collections[collectionIndex],
34+
paths: action.payload
35+
};
36+
state.saved = true;
1837
}
1938
},
20-
removeResourcePaths: (state, action: PayloadAction<ResourcePath[]>)=>{
21-
const index = state.findIndex(collection => collection.isDefault);
22-
if(index > -1) {
23-
const defaultResourcePaths = [...state[index].paths];
24-
action.payload.forEach((resourcePath: ResourcePath)=>{
25-
const delIndex = defaultResourcePaths.findIndex(p=>p.key === resourcePath.key)
39+
removeResourcePaths: (state, action: PayloadAction<ResourcePath[]>) => {
40+
const index = state.collections.findIndex(collection => collection.isDefault);
41+
if (index > -1) {
42+
const defaultResourcePaths = [...state.collections[index].paths];
43+
action.payload.forEach((resourcePath: ResourcePath) => {
44+
const delIndex = defaultResourcePaths.findIndex(p => p.key === resourcePath.key);
2645
if (delIndex > -1) {
27-
defaultResourcePaths.splice(delIndex, 1)
46+
defaultResourcePaths.splice(delIndex, 1);
2847
}
29-
})
30-
state[index].paths = defaultResourcePaths;
48+
});
49+
state.collections[index].paths = defaultResourcePaths;
50+
state.saved = false;
3151
}
52+
},
53+
resetSaveState: (state) => {
54+
state.saved = false;
3255
}
3356
}
34-
})
57+
});
3558

36-
export const {createCollection, addResourcePaths, removeResourcePaths} = collections.actions
59+
export const
60+
{ createCollection,
61+
addResourcePaths,
62+
updateResourcePaths,
63+
removeResourcePaths,
64+
resetSaveState } = collections.actions;
3765

38-
export default collections.reducer
66+
export default collections.reducer;

src/app/utils/searchbox.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const searchBoxStyles: any = () => ({
22
root: {
3-
width: '97%'
3+
width: '100%'
44
},
55
field: [
66
{

src/app/views/App.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Mode } from '../../types/enums';
1414
import { IInitMessage, IQuery, IThemeChangedMessage } from '../../types/query-runner';
1515
import { ISharedQueryParams } from '../../types/share-query';
1616
import { ISidebarProps } from '../../types/sidebar';
17+
import CollectionPermissionsProvider from '../services/context/collection-permissions/CollectionPermissionsProvider';
1718
import { PopupsProvider } from '../services/context/popups-context';
1819
import { ValidationProvider } from '../services/context/validation-context/ValidationProvider';
1920
import { GRAPH_URL } from '../services/graph-constants';
@@ -26,12 +27,11 @@ import { changeTheme } from '../services/slices/theme.slice';
2627
import { parseSampleUrl } from '../utils/sample-url-generation';
2728
import { substituteTokens } from '../utils/token-helpers';
2829
import { translateMessage } from '../utils/translate-messages';
29-
import { TermsOfUseMessage } from './app-sections';
30+
import { StatusMessages, TermsOfUseMessage } from './app-sections';
3031
import { headerMessaging } from './app-sections/HeaderMessaging';
3132
import { appStyles } from './App.styles';
3233
import { classNames } from './classnames';
3334
import { KeyboardCopyEvent } from './common/copy-button/KeyboardCopyEvent';
34-
import { StatusMessages } from './common/lazy-loader/component-registry';
3535
import PopupsWrapper from './common/popups/PopupsWrapper';
3636
import { createShareLink } from './common/share';
3737
import { MainHeader } from './main-header/MainHeader';
@@ -492,7 +492,9 @@ class App extends Component<IAppProps, IAppState> {
492492
<TermsOfUseMessage />
493493
</div>
494494
</div>
495-
<PopupsWrapper />
495+
<CollectionPermissionsProvider>
496+
<PopupsWrapper />
497+
</CollectionPermissionsProvider>
496498
</PopupsProvider>
497499
</ThemeContext.Provider>
498500
);

0 commit comments

Comments
 (0)