Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: list all permissions #477

Merged
merged 25 commits into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d0d1839
change samples endpoint
thewahome Apr 14, 2020
07adce7
creates a new setting to handle permissions
thewahome Apr 14, 2020
2deec61
changes permission to allow panel view and tab view
thewahome Apr 14, 2020
f5edc61
allow consent of multiple items at a go
thewahome Apr 15, 2020
a00a1d0
enhance action creator to take state's url
thewahome Apr 15, 2020
47cc05e
enhance permissions hooks to enable dispatch fetchScopes
thewahome Apr 15, 2020
dd8535c
perform search when sample url changes
thewahome Apr 15, 2020
7db0860
change snippets to use state devX api url
thewahome Apr 15, 2020
4c6a3ab
fix failing test
thewahome Apr 15, 2020
9de0bea
fix liniting errors
thewahome Apr 15, 2020
9664111
subscribe permissions to selected verb
thewahome Apr 16, 2020
0a15130
move consent function to action creator for reusability
thewahome Apr 16, 2020
02afbbc
displays the permissions in order when in the panel
thewahome Apr 16, 2020
cb6446e
adds sorting tests
thewahome Apr 17, 2020
a7f980b
remove bulk permissions consent
thewahome Apr 17, 2020
165b478
create groups to categorise permissions
thewahome Apr 17, 2020
2602b66
convert to class component :-(
thewahome Apr 17, 2020
f63da01
select multiiple permissions for consent
thewahome Apr 17, 2020
ff47246
accessibility edits
thewahome Apr 17, 2020
e05270b
Improve messaging
thewahome Apr 20, 2020
3f27fc2
display message of empty permissions
thewahome Apr 23, 2020
a3e27a5
use object destructuring
thewahome Apr 28, 2020
409905e
Merge branch 'dev' into feature/list-all-permissions
thewahome Apr 28, 2020
926c0f2
change settings class component to hooks to fix close/reopen bug
thewahome Apr 29, 2020
7f44b27
Merge branch 'feature/list-all-permissions' of https://github.com/mic…
thewahome Apr 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 51 additions & 34 deletions src/app/services/actions/permissions-action-creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { IAction } from '../../../types/action';
import { IQuery } from '../../../types/query-runner';
import { IRequestOptions } from '../../../types/request';
import { FETCH_SCOPES_ERROR, FETCH_SCOPES_SUCCESS } from '../redux-constants';
import { parseSampleUrl } from '../../utils/sample-url-generation';
import { acquireNewAccessToken } from '../graph-client/msal-service';
import { FETCH_SCOPES_ERROR, FETCH_SCOPES_PENDING, FETCH_SCOPES_SUCCESS } from '../redux-constants';
import { getAuthTokenSuccess, getConsentedScopesSuccess } from './auth-action-creators';

export function fetchScopesSuccess(response: object): IAction {
return {
Expand All @@ -9,47 +13,60 @@ export function fetchScopesSuccess(response: object): IAction {
};
}

export function fetchScopesPending(): any {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any [](start = 38, length = 3)

i know you don't like creating an interface for something this simple but it really does help keep the code more readable and maintainable.

return {
type: FETCH_SCOPES_PENDING,
};
}

export function fetchScopesError(response: object): IAction {
return {
type: FETCH_SCOPES_ERROR,
response,
};
}

export function fetchScopes(): Function {
export function fetchScopes(query?: IQuery): Function {
return async (dispatch: Function, getState: Function) => {
const { sampleQuery: { sampleUrl, selectedVerb } } = getState();
const urlObject: URL = new URL(sampleUrl);
const createdAt = new Date().toISOString();
// remove the prefix i.e. beta or v1.0 and any possible extra '/' character at the end
const requestUrl = urlObject.pathname.substr(5).replace(/\/$/, '');
const permissionsUrl = 'https://graphexplorerapi.azurewebsites.net/api/GraphExplorerPermissions?requesturl=' +
requestUrl + '&method=' + selectedVerb;

const headers = {
'Content-Type': 'application/json',
};

const options: IRequestOptions = { headers };

return fetch(permissionsUrl, options)
.then(res => res.json())
.then(res => {
if (res.error) {
throw (res.error);
try {
const { devxApi } = getState();
thewahome marked this conversation as resolved.
Show resolved Hide resolved
let permissionsUrl = `${devxApi}/permissions`;

if (query) {
const { requestUrl, sampleUrl } = parseSampleUrl(query.sampleUrl);

if (!sampleUrl) {
throw new Error('url is invalid');
}
dispatch(fetchScopesSuccess(res));
})
.catch(() => {
const duration = (new Date()).getTime() - new Date(createdAt).getTime();
const response = {
/* Return 'Forbidden' regardless of error, as this was a
permission-centric operation with regards to user context */
statusText: 'Forbidden',
status: '403',
duration
};
return dispatch(fetchScopesError(response));
});

permissionsUrl = `${permissionsUrl}?requesturl=/${requestUrl}&method=${query.selectedVerb}`;
}

const headers = {
'Content-Type': 'application/json',
};
const options: IRequestOptions = { headers };

dispatch(fetchScopesPending());

const response = await fetch(permissionsUrl, options);
if (response.ok) {
const scopes = await response.json();
return dispatch(fetchScopesSuccess(scopes));
}
throw (response);
} catch (error) {
return dispatch(fetchScopesError(error));
}
};
}

export function consentToScopes(scopes: string[]): Function {
return async (dispatch: Function) => {
const authResponse = await acquireNewAccessToken(scopes);
if (authResponse && authResponse.accessToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have an authResponse that doesn't have an accessToken?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jobala I want to say there is :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not repeat ourselves

dispatch(getAuthTokenSuccess(authResponse.accessToken));
dispatch(getConsentedScopesSuccess(authResponse.scopes));
}
};
}
2 changes: 1 addition & 1 deletion src/app/services/actions/samples-action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function fetchSamplesPending(): any {
export function fetchSamples(): Function {
return async (dispatch: Function, getState: Function) => {
const devxApi = getState().devxApi;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Use object destructuring
const { devxApi } = getState()

const samplesUrl = `${devxApi}/api/GraphExplorerSamples`;
const samplesUrl = `${devxApi}/samples`;

const headers = {
'Content-Type': 'application/json',
Expand Down
74 changes: 47 additions & 27 deletions src/app/services/actions/snippet-action-creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IAction } from '../../../types/action';
import { IQuery } from '../../../types/query-runner';
import { GET_SNIPPET_SUCCESS } from '../redux-constants';
import { parseSampleUrl } from '../../utils/sample-url-generation';
import { GET_SNIPPET_ERROR, GET_SNIPPET_PENDING, GET_SNIPPET_SUCCESS } from '../redux-constants';

export function getSnippetSuccess(response: string): IAction {
return {
Expand All @@ -9,33 +9,53 @@ export function getSnippetSuccess(response: string): IAction {
};
}

export function getSnippet(language: string, sampleQuery: IQuery, dispatch: Function) {
const sample = { ...sampleQuery };
export function getSnippetError(response: string): IAction {
return {
type: GET_SNIPPET_ERROR,
response,
};
}

if (sample.sampleUrl) {
const urlObject: URL = new URL(sample.sampleUrl);
sample.sampleUrl = urlObject.pathname + urlObject.search;
}
export function getSnippetPending(): any {
return {
type: GET_SNIPPET_PENDING
};
}

let url = 'https://graphexplorerapi.azurewebsites.net/api/graphexplorersnippets';
export function getSnippet(language: string): Function {
return async (dispatch: Function, getState: Function) => {
try {
const { devxApi, sampleQuery } = getState();
let snippetsUrl = `${devxApi}/api/graphexplorersnippets`;

if (language !== 'csharp') {
url += `?lang=${language}`;
}
const { requestUrl, sampleUrl, queryVersion } = parseSampleUrl(sampleQuery.sampleUrl);
if (!sampleUrl) {
throw new Error('url is invalid');
}
if (language !== 'csharp') {
snippetsUrl += `?lang=${language}`;
}

// tslint:disable-next-line: max-line-length
const body = `${sample.selectedVerb} ${sample.sampleUrl} HTTP/1.1\r\nHost: graph.microsoft.com\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(sample.sampleBody)}`;
const obj: any = {};
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/http'
},
body
}).then(resp => resp.text())
// tslint:disable-next-line
.then((result) => {
obj[language] = result;
dispatch(getSnippetSuccess(obj));
});
dispatch(getSnippetPending());

// tslint:disable-next-line: max-line-length
const body = `${sampleQuery.selectedVerb} /${queryVersion}/${requestUrl} HTTP/1.1\r\nHost: graph.microsoft.com\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(sampleQuery.sampleBody)}`;
const obj: any = {};
const response = await fetch(snippetsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/http'
},
body
});
if (response.ok) {
const result = await response.text();
obj[language] = result;
return dispatch(getSnippetSuccess(obj));
}
throw (response);
} catch (error) {
return dispatch(getSnippetError(error));
}
};
}
16 changes: 11 additions & 5 deletions src/app/services/reducers/permissions-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IAction } from '../../../types/action';
import { FETCH_SCOPES_ERROR, FETCH_SCOPES_SUCCESS } from '../redux-constants';
import { FETCH_SCOPES_ERROR, FETCH_SCOPES_PENDING, FETCH_SCOPES_SUCCESS } from '../redux-constants';

const initialState = {
pending: false,
Expand All @@ -11,15 +11,21 @@ export function scopes(state = initialState, action: IAction): any {
switch (action.type) {
case FETCH_SCOPES_SUCCESS:
return {
...state,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...state [](start = 8, length = 8)

wouldn't this still be good to have in case the state object ever adds new items.

pending: false,
data: action.response
data: action.response,
error: null
};
case FETCH_SCOPES_ERROR:
return {
...state,
pending: false,
error: action.response
error: action.response,
data: []
};
case FETCH_SCOPES_PENDING:
return {
pending: true,
data: [],
error: null
};
default:
return state;
Expand Down
4 changes: 1 addition & 3 deletions src/app/services/reducers/query-runner-status-reducers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { IAction } from '../../../types/action';
import { CLEAR_QUERY_STATUS, FETCH_SCOPES_ERROR,
import { CLEAR_QUERY_STATUS,
GET_CONSENT_ERROR, QUERY_GRAPH_RUNNING, QUERY_GRAPH_STATUS,
VIEW_HISTORY_ITEM_SUCCESS } from '../redux-constants';

export function queryRunnerStatus(state = {}, action: IAction): any {
switch (action.type) {
case QUERY_GRAPH_STATUS:
return action.response;
case FETCH_SCOPES_ERROR:
return action.response;
case GET_CONSENT_ERROR:
return action.response;
case QUERY_GRAPH_RUNNING:
Expand Down
27 changes: 24 additions & 3 deletions src/app/services/reducers/snippet-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { IAction } from '../../../types/action';
import { GET_SNIPPET_SUCCESS } from '../redux-constants';
import { GET_SNIPPET_ERROR, GET_SNIPPET_PENDING, GET_SNIPPET_SUCCESS } from '../redux-constants';

export function snippets(state = {}, action: IAction): any {
const initialState = {
pending: false,
data: {},
error: null
};
export function snippets(state = initialState, action: IAction): any {
Copy link

@ddyett ddyett Apr 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any [](start = 65, length = 3)

great place to use typescript to ensure the type coming out is the state type. #Resolved

Copy link
Collaborator Author

@thewahome thewahome Apr 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can create a base type for all the initial states that involve fetching data from APIs other than graph can inherit from so that it can be consistent,

This is some chunk of work which will make this PR a headache to review. I can add this to the backlog as technical debt... #Resolved

switch (action.type) {
case GET_SNIPPET_SUCCESS:
return { ...state, ...action.response as object };
return {
pending: false,
data: action.response as object,
error: null
};
case GET_SNIPPET_ERROR:
return {
pending: false,
data: null,
error: action.response as object
};
case GET_SNIPPET_PENDING:
return {
pending: true,
data: null,
error: null
};
default:
return state;
}
Expand Down
3 changes: 3 additions & 0 deletions src/app/services/redux-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const PROFILE_REQUEST_SUCCESS = 'PROFILE_REQUEST_SUCCESS';
export const PROFILE_IMAGE_REQUEST_SUCCESS = 'PROFILE_IMAGE_REQUEST_SUCCESS';
export const PROFILE_REQUEST_ERROR = 'PROFILE_REQUEST_ERROR';
export const GET_SNIPPET_SUCCESS = 'GET_SNIPPET_SUCCESS';
export const GET_SNIPPET_ERROR = 'GET_SNIPPET_ERROR';
export const GET_SNIPPET_PENDING = 'GET_SNIPPET_PENDING';
export const REMOVE_HISTORY_ITEM_SUCCESS = 'REMOVE_HISTORY_ITEM_SUCCESS';
export const ADD_HISTORY_ITEM_SUCCESS = 'ADD_HISTORY_ITEM_SUCCESS';
export const GET_HISTORY_ITEMS_SUCCESS = 'GET_HISTORY_ITEMS_SUCCESS';
Expand All @@ -28,6 +30,7 @@ export const FETCH_ADAPTIVE_CARD_ERROR = 'FETCH_ADAPTIVE_CARD_ERROR';
export const CLEAR_TERMS_OF_USE = 'CLEAR_TERMS_OF_USE';
export const FETCH_SCOPES_SUCCESS = 'SCOPES_FETCH_SUCCESS';
export const FETCH_SCOPES_ERROR = 'SCOPES_FETCH_ERROR';
export const FETCH_SCOPES_PENDING = 'FETCH_SCOPES_PENDING';
export const GET_CONSENT_ERROR = 'GET_CONSENT_ERROR';
export const GET_CONSENTED_SCOPES_SUCCESS = 'GET_CONSENTED_SCOPES_SUCCESS';
export const SET_DEVX_API_URL_SUCCESS = 'SET_DEVX_API_URL_SUCCESS';
Expand Down
17 changes: 17 additions & 0 deletions src/app/utils/dynamic-sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SortOrder } from '../../types/enums';
/**
* Sorts a given array by the passed in property in the direction specified
* @param {string} property the property to sort the array with
* @param {SortOrder} sortOrder the direction to follow Ascending / Descending
* You pass this helper to the array sort function
*/
export function dynamicSort(property: string, sortOrder: SortOrder) {
let order = 1;
if (sortOrder === SortOrder.DESC) {
order = -1;
}
return (first: any, second: any) => {
const result = (first[property] < second[property]) ? -1 : (first[property] > second[property]) ? 1 : 0;
return result * order;
};
}
45 changes: 31 additions & 14 deletions src/app/views/query-response/snippets/snippets-helper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IconButton, PivotItem } from 'office-ui-fabric-react';
import React, { useEffect, useState } from 'react';
import { IconButton, Label, PivotItem } from 'office-ui-fabric-react';
import React, { useEffect } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';

import { FormattedMessage } from 'react-intl';
import { getSnippet } from '../../../services/actions/snippet-action-creator';
import { Monaco } from '../../common';
import { genericCopy } from '../../common/copy';
Expand Down Expand Up @@ -33,8 +34,9 @@ function Snippet(props: ISnippetProps) {


const sampleQuery = useSelector((state: any) => state.sampleQuery, shallowEqual);
const snippet = useSelector((state: any) => (state.snippets)[language]);
const [ loadingState, setLoadingState ] = useState(false);
const snippets = useSelector((state: any) => (state.snippets));
const { data, pending: loadingState } = snippets;
const snippet = (!loadingState && data) ? data[language] : null;

const dispatch = useDispatch();

Expand All @@ -43,20 +45,35 @@ function Snippet(props: ISnippetProps) {
};

useEffect(() => {
setLoadingState(true);

getSnippet(language, sampleQuery, dispatch)
.then(() => setLoadingState(false));
dispatch(getSnippet(language));
}, [sampleQuery.sampleUrl]);

return (
<div style={{ display: 'block' }}>
<IconButton style={{ float: 'right', zIndex: 1}} iconProps={copyIcon} onClick={async () => genericCopy(snippet)}/>
<Monaco
body={loadingState ? 'Fetching code snippet...' : snippet}
language={language}
readOnly={true}
/>
{loadingState &&
<Label style={{ padding: 10 }}>
<FormattedMessage id ='Fetching code snippet' />...
</Label>
}
{!loadingState && snippet &&
<>
<IconButton
style={{ float: 'right', zIndex: 1}}
iconProps={copyIcon}
onClick={async () => genericCopy(snippet)}
/>
<Monaco
body={snippet}
language={language}
readOnly={true}
/>
</>
}
{!loadingState && !snippet &&
<Label style={{ padding: 10 }}>
<FormattedMessage id ='Snippet not available' />
</Label>
}
</div>
);
}
Loading