* bubbling a custom exception and using it to drive a custom exception layout

* shared service api errors

* ExceptionLayout in more places

* exception handling in create form

* error handling in Airlock

* Delete + disable popups now showing error messages rather than hanging on a spinner
This commit is contained in:
David Moore 2022-09-08 14:47:26 +01:00 коммит произвёл GitHub
Родитель b660f2db5f
Коммит 6fd2e33f33
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 440 добавлений и 313 удалений

Просмотреть файл

@ -1,4 +1,4 @@
import { MessageBar, MessageBarType, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { Spinner, SpinnerSize, Stack } from '@fluentui/react';
import React, { useEffect, useState } from 'react';
import { Route, Routes } from 'react-router-dom';
import { Admin } from '../../App';
@ -12,21 +12,24 @@ import { SharedServices } from '../shared/SharedServices';
import { SharedServiceItem } from '../shared/SharedServiceItem';
import { SecuredByRole } from '../shared/SecuredByRole';
import { RoleName } from '../../models/roleNames';
import { APIError } from '../../models/exceptions';
import { ExceptionLayout } from '../shared/ExceptionLayout';
export const RootLayout: React.FunctionComponent = () => {
const [workspaces, setWorkspaces] = useState([] as Array<Workspace>);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const [apiError, setApiError] = useState({} as APIError);
const apiCall = useAuthApiCall();
useEffect(() => {
const getWorkspaces = async () => {
try {
const r = await apiCall(ApiEndpoint.Workspaces, HttpMethod.Get, undefined, undefined, ResultType.JSON, (roles: Array<string>) => {
setLoadingState(roles && roles.length > 0 ? LoadingState.Ok : LoadingState.AccessDenied);
});
const r = await apiCall(ApiEndpoint.Workspaces, HttpMethod.Get, undefined, undefined, ResultType.JSON);
setLoadingState(LoadingState.Ok);
r && r.workspaces && setWorkspaces(r.workspaces);
} catch {
} catch (e:any) {
e.userMessage = 'Error retrieving resources';
setApiError(e);
setLoadingState(LoadingState.Error);
}
@ -81,27 +84,9 @@ export const RootLayout: React.FunctionComponent = () => {
</Stack.Item>
</Stack>
);
case LoadingState.AccessDenied:
return (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={true}
>
<h3>Access Denied</h3>
<p>
You do not have access to this application. If you feel you should have access, please speak to your TRE Administrator. <br />
If you have recently been given access, you may need to clear you browser local storage and refresh.</p>
</MessageBar>
);
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving workspaces</h3>
<p>We were unable to fetch the workspace list. Please see browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
);
default:
return (

Просмотреть файл

@ -5,6 +5,9 @@ import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCa
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { OperationsContext } from '../../contexts/OperationsContext';
import { ResourceType } from '../../models/resourceType';
import { APIError } from '../../models/exceptions';
import { LoadingState } from '../../models/loadingState';
import { ExceptionLayout } from './ExceptionLayout';
interface ConfirmDeleteProps {
resource: Resource,
@ -14,7 +17,8 @@ interface ConfirmDeleteProps {
// show a 'are you sure' modal, and then send a patch if the user confirms
export const ConfirmDeleteResource: React.FunctionComponent<ConfirmDeleteProps> = (props: ConfirmDeleteProps) => {
const apiCall = useAuthApiCall();
const [isSending, setIsSending] = useState(false);
const [apiError, setApiError] = useState({} as APIError);
const [loading, setLoading] = useState(LoadingState.Ok);
const workspaceCtx = useContext(WorkspaceContext);
const opsCtx = useContext(OperationsContext);
@ -36,11 +40,16 @@ export const ConfirmDeleteResource: React.FunctionComponent<ConfirmDeleteProps>
const wsAuth = (props.resource.resourceType === ResourceType.WorkspaceService || props.resource.resourceType === ResourceType.UserResource);
const deleteCall = async () => {
setIsSending(true);
let op = await apiCall(props.resource.resourcePath, HttpMethod.Delete, wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined, undefined, ResultType.JSON);
opsCtx.addOperations([op.operation]);
setIsSending(false);
props.onDismiss();
setLoading(LoadingState.Loading);
try {
let op = await apiCall(props.resource.resourcePath, HttpMethod.Delete, wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined, undefined, ResultType.JSON);
opsCtx.addOperations([op.operation]);
props.onDismiss();
} catch (err: any) {
err.userMessage = 'Failed to delete resource';
setApiError(err);
setLoading(LoadingState.Error);
}
}
return (<>
@ -50,15 +59,21 @@ export const ConfirmDeleteResource: React.FunctionComponent<ConfirmDeleteProps>
dialogContentProps={deleteProps}
modalProps={modalProps}
>
{!isSending ?
{
loading === LoadingState.Ok &&
<DialogFooter>
<PrimaryButton text="Delete" onClick={() => deleteCall()} />
<DefaultButton text="Cancel" onClick={() => props.onDismiss()} />
</DialogFooter>
:
}
{
loading === LoadingState.Loading &&
<Spinner label="Sending request..." ariaLive="assertive" labelPosition="right" />
}
{
loading === LoadingState.Error &&
<ExceptionLayout e={apiError} />
}
</Dialog>
</>);
};

Просмотреть файл

@ -5,6 +5,9 @@ import { HttpMethod, ResultType, useAuthApiCall } from '../../hooks/useAuthApiCa
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import { OperationsContext } from '../../contexts/OperationsContext';
import { ResourceType } from '../../models/resourceType';
import { LoadingState } from '../../models/loadingState';
import { APIError } from '../../models/exceptions';
import { ExceptionLayout } from './ExceptionLayout';
interface ConfirmDisableEnableResourceProps {
resource: Resource,
@ -15,7 +18,8 @@ interface ConfirmDisableEnableResourceProps {
// show a 'are you sure' modal, and then send a patch if the user confirms
export const ConfirmDisableEnableResource: React.FunctionComponent<ConfirmDisableEnableResourceProps> = (props: ConfirmDisableEnableResourceProps) => {
const apiCall = useAuthApiCall();
const [isSending, setIsSending] = useState(false);
const [loading, setLoading] = useState(LoadingState.Ok);
const [apiError, setApiError] = useState({} as APIError);
const workspaceCtx = useContext(WorkspaceContext);
const opsCtx = useContext(OperationsContext);
@ -44,35 +48,46 @@ export const ConfirmDisableEnableResource: React.FunctionComponent<ConfirmDisabl
const wsAuth = (props.resource.resourceType === ResourceType.WorkspaceService || props.resource.resourceType === ResourceType.UserResource);
const toggleDisableCall = async () => {
setIsSending(true);
let body = { isEnabled: props.isEnabled }
let op = await apiCall(props.resource.resourcePath, HttpMethod.Patch, wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined, body, ResultType.JSON, undefined, undefined, props.resource._etag);
opsCtx.addOperations([op.operation]);
setIsSending(false);
props.onDismiss();
setLoading(LoadingState.Loading);
try {
let body = { isEnabled: props.isEnabled }
let op = await apiCall(props.resource.resourcePath, HttpMethod.Patch, wsAuth ? workspaceCtx.workspaceApplicationIdURI : undefined, body, ResultType.JSON, undefined, undefined, props.resource._etag);
opsCtx.addOperations([op.operation]);
props.onDismiss();
} catch (err: any) {
err.userMessage = 'Failed to enable/disable resource';
setApiError(err);
setLoading(LoadingState.Error);
}
}
return (
<>
<Dialog
hidden={false}
onDismiss={() => props.onDismiss()}
dialogContentProps={props.isEnabled ? enableProps : disableProps}
modalProps={modalProps}
>
{!isSending ?
<DialogFooter>
{props.isEnabled ?
<PrimaryButton text="Enable" onClick={() => toggleDisableCall()} />
:
<PrimaryButton text="Disable" onClick={() => toggleDisableCall()} />
}
<DefaultButton text="Cancel" onClick={() => props.onDismiss()} />
</DialogFooter>
:
<Spinner label="Sending request..." ariaLive="assertive" labelPosition="right" />
}
</Dialog>
</>);
<>
<Dialog
hidden={false}
onDismiss={() => props.onDismiss()}
dialogContentProps={props.isEnabled ? enableProps : disableProps}
modalProps={modalProps}
>
{
loading === LoadingState.Ok &&
<DialogFooter>
{props.isEnabled ?
<PrimaryButton text="Enable" onClick={() => toggleDisableCall()} />
:
<PrimaryButton text="Disable" onClick={() => toggleDisableCall()} />
}
<DefaultButton text="Cancel" onClick={() => props.onDismiss()} />
</DialogFooter>
}
{
loading === LoadingState.Loading &&
<Spinner label="Sending request..." ariaLive="assertive" labelPosition="right" />
}
{
loading === LoadingState.Error &&
<ExceptionLayout e={apiError} />
}
</Dialog>
</>);
};

Просмотреть файл

@ -0,0 +1,70 @@
import { MessageBar, MessageBarType, Link as FluentLink, Icon, } from '@fluentui/react';
import React, { useState } from 'react';
import { APIError } from '../../models/exceptions';
interface ExceptionLayoutProps {
e: APIError
}
export const ExceptionLayout: React.FunctionComponent<ExceptionLayoutProps> = (props: ExceptionLayoutProps) => {
const [showDetails, setShowDetails] = useState(false);
switch (props.e.status) {
case 403:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Access Denied</h3>
<h4>{props.e.userMessage}</h4>
<p>{props.e.message}</p>
<p>Attempted resource: {props.e.endpoint}</p>
</MessageBar>
);
default:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>{props.e.userMessage}</h3>
<p>{props.e.message}</p><br />
<FluentLink title={showDetails ? 'Show less' : 'Show more'} href="#" onClick={() => { setShowDetails(!showDetails) }} style={{ position: 'relative', top: '2px', paddingLeft: 0 }}>
{
showDetails ?
<><Icon iconName='ChevronUp' aria-label='Expand Details' /> {'Hide Details'}</> :
<><Icon iconName='ChevronDown' aria-label='Collapse Details' /> {'Show Details'} </>
}
</FluentLink>
{
showDetails &&
<>
<table style={{ border: '1px solid #666', width: '100%', padding: 10, marginTop: 15 }}>
<tbody>
<tr>
<td><b>Endpoint</b></td>
<td>{props.e.endpoint}</td>
</tr>
<tr>
<td><b>Status Code</b></td>
<td>{props.e.status || '(none)'}</td>
</tr>
<tr>
<td><b>Stack Trace</b></td>
<td>{props.e.stack}</td>
</tr>
<tr>
<td><b>Exception</b></td>
<td>{props.e.exception}</td>
</tr>
</tbody>
</table>
</>
}
</MessageBar>
)
}
};

Просмотреть файл

@ -16,6 +16,7 @@ import { CreateUpdateResourceContext } from '../../contexts/CreateUpdateResource
import { Workspace } from '../../models/workspace';
import { WorkspaceService } from '../../models/workspaceService';
import { actionsDisabledStates } from '../../models/operation';
import { AppRolesContext } from '../../contexts/AppRolesContext';
interface ResourceContextMenuProps {
resource: Resource,
@ -32,6 +33,10 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
const createFormCtx = useContext(CreateUpdateResourceContext);
const opsWriteContext = useRef(useContext(OperationsContext)); // useRef to avoid re-running a hook on context write
const [parentResource, setParentResource] = useState({} as WorkspaceService | Workspace);
const [roles, setRoles] = useState([] as Array<string>);
const [wsAuth, setWsAuth] = useState(false);
const appRoles = useContext(AppRolesContext); // the user is in these roles which apply across the app
// get the resource template
useEffect(() => {
@ -57,11 +62,37 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
default:
throw Error('Unsupported resource type.');
}
const template = await apiCall(`${templatesPath}/${props.resource.templateName}`, HttpMethod.Get);
setResourceTemplate(template);
let r = [] as Array<string>;
let wsAuth = false;
switch (props.resource.resourceType) {
case ResourceType.SharedService:
r = [RoleName.TREAdmin, WorkspaceRoleName.WorkspaceOwner];
break;
case ResourceType.WorkspaceService:
r = [WorkspaceRoleName.WorkspaceOwner]
wsAuth = true;
break;
case ResourceType.UserResource:
r = [WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher];
wsAuth = true;
break;
case ResourceType.Workspace:
r = [RoleName.TREAdmin];
break;
}
setWsAuth(wsAuth);
setRoles(r);
// should we bother getting the template? if the user isn't in the right role they won't see the menu at all.
const userRoles = wsAuth ? workspaceCtx.roles : appRoles.roles;
if (userRoles && r.filter(x => userRoles.includes(x)).length > 0) {
const template = await apiCall(`${templatesPath}/${props.resource.templateName}`, HttpMethod.Get);
setResourceTemplate(template);
}
};
getTemplate();
}, [apiCall, props.resource, workspaceCtx.workspace.id, workspaceCtx.workspaceApplicationIdURI]);
}, [apiCall, props.resource, workspaceCtx.workspace.id, workspaceCtx.workspaceApplicationIdURI, appRoles.roles, workspaceCtx.roles]);
const doAction = async (actionName: string) => {
const action = await apiCall(`${props.resource.resourcePath}/${ApiEndpoint.InvokeAction}?action=${actionName}`, HttpMethod.Post, workspaceCtx.workspaceApplicationIdURI);
@ -70,8 +101,6 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
// context menu
let menuItems: Array<any> = [];
let roles: Array<string> = [];
let wsAuth = false;
menuItems = [
{
@ -153,18 +182,6 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
});
}
switch (props.resource.resourceType) {
case ResourceType.Workspace:
case ResourceType.SharedService:
roles = [RoleName.TREAdmin];
break;
case ResourceType.WorkspaceService:
case ResourceType.UserResource:
wsAuth = true;
roles = [WorkspaceRoleName.WorkspaceOwner];
break;
}
const menuProps: IContextualMenuProps = {
shouldFocusOnMount: true,
items: menuItems
@ -177,7 +194,7 @@ export const ResourceContextMenu: React.FunctionComponent<ResourceContextMenuPro
<CommandBar
items={menuItems}
ariaLabel="Resource actions"
/>
/>
:
<IconButton iconProps={{ iconName: 'More' }} menuProps={menuProps} className="tre-hide-chevron" disabled={props.componentAction === ComponentAction.Lock} />
} />

Просмотреть файл

@ -1,4 +1,4 @@
import { IStackStyles, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { IStackStyles, Spinner, SpinnerSize, Stack } from "@fluentui/react";
import React, { useEffect, useContext, useState } from 'react';
import { useParams } from 'react-router-dom';
import { HttpMethod, useAuthApiCall } from '../../hooks/useAuthApiCall';
@ -9,6 +9,9 @@ import { ResourceOperationListItem } from './ResourceOperationListItem';
import { WorkspaceContext } from '../../contexts/WorkspaceContext';
import config from '../../config.json';
import moment from "moment";
import { APIError } from "../../models/exceptions";
import { LoadingState } from "../../models/loadingState";
import { ExceptionLayout } from "./ExceptionLayout";
interface ResourceOperationsListProps {
@ -17,6 +20,7 @@ interface ResourceOperationsListProps {
export const ResourceOperationsList: React.FunctionComponent<ResourceOperationsListProps> = (props: ResourceOperationsListProps) => {
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const workspaceCtx = useContext(WorkspaceContext);
const { resourceId } = useParams();
const [resourceOperations, setResourceOperations] = useState([] as Array<Operation>)
@ -29,9 +33,11 @@ export const ResourceOperationsList: React.FunctionComponent<ResourceOperationsL
const ops = await apiCall(`${props.resource.resourcePath}/${ApiEndpoint.Operations}`, HttpMethod.Get, workspaceCtx.workspaceApplicationIdURI);
config.debug && console.log(`Got resource operations, for resource:${props.resource.id}: ${ops.operations}`);
setResourceOperations(ops.operations.reverse());
setLoadingState(ops && ops.operations.length > 0 ? 'ok' : 'error');
} catch {
setLoadingState('error');
setLoadingState(ops && ops.operations.length > 0 ? LoadingState.Ok : LoadingState.Error);
} catch (err: any) {
err.userMessage = "Error retrieving resource operations"
setApiError(err);
setLoadingState(LoadingState.Error);
}
};
getOperations();
@ -46,7 +52,7 @@ export const ResourceOperationsList: React.FunctionComponent<ResourceOperationsL
};
switch (loadingState) {
case 'ok':
case LoadingState.Ok:
return (
<>
{
@ -70,15 +76,9 @@ export const ResourceOperationsList: React.FunctionComponent<ResourceOperationsL
}
</>
);
case 'error':
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving resource operations</h3>
<p>There was an error retrieving this resource operations. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
)
default:
return (

Просмотреть файл

@ -2,13 +2,15 @@ import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ApiEndpoint } from '../../models/apiEndpoints';
import { useAuthApiCall, HttpMethod } from '../../hooks/useAuthApiCall';
import { MessageBar, MessageBarType, Spinner, SpinnerSize } from '@fluentui/react';
import { Spinner, SpinnerSize } from '@fluentui/react';
import { LoadingState } from '../../models/loadingState';
import { SharedService } from '../../models/sharedService';
import { ResourceHeader } from './ResourceHeader';
import { useComponentManager } from '../../hooks/useComponentManager';
import { Resource } from '../../models/resource';
import { ResourceBody } from './ResourceBody';
import { APIError } from '../../models/exceptions';
import { ExceptionLayout } from './ExceptionLayout';
interface SharedServiceItemProps {
readonly?: boolean
@ -19,19 +21,26 @@ export const SharedServiceItem: React.FunctionComponent<SharedServiceItemProps>
const [sharedService, setSharedService] = useState({} as SharedService);
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const navigate = useNavigate();
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const latestUpdate = useComponentManager(
sharedService,
(r: Resource) => setSharedService(r as SharedService),
(r: Resource) => navigate(`/${ApiEndpoint.SharedServices}`)
);
const apiCall = useAuthApiCall();
useEffect(() => {
const getData = async () => {
let ss = await apiCall(`${ApiEndpoint.SharedServices}/${sharedServiceId}`, HttpMethod.Get);
setSharedService(ss.sharedService);
setLoadingState(LoadingState.Ok);
try {
let ss = await apiCall(`${ApiEndpoint.SharedServices}/${sharedServiceId}`, HttpMethod.Get);
setSharedService(ss.sharedService);
setLoadingState(LoadingState.Ok);
} catch (err:any) {
err.userMessage = "Error retrieving shared service";
setApiError(err);
setLoadingState(LoadingState.Error)
}
};
getData();
}, [apiCall, sharedServiceId]);
@ -46,13 +55,7 @@ export const SharedServiceItem: React.FunctionComponent<SharedServiceItemProps>
);
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving shared service</h3>
<p>There was an error retrieving this shared service. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
);
default:
return (

Просмотреть файл

@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { CommandBarButton, DetailsList, getTheme, IColumn, MessageBar, MessageBarType, Persona, PersonaSize, SelectionMode, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { CommandBarButton, DetailsList, getTheme, IColumn, Persona, PersonaSize, SelectionMode, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { HttpMethod, useAuthApiCall } from '../../../hooks/useAuthApiCall';
import { ApiEndpoint } from '../../../models/apiEndpoints';
import { WorkspaceContext } from '../../../contexts/WorkspaceContext';
@ -8,6 +8,8 @@ import moment from 'moment';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { AirlockViewRequest } from './AirlockViewRequest';
import { LoadingState } from '../../../models/loadingState';
import { APIError } from '../../../models/exceptions';
import { ExceptionLayout } from '../ExceptionLayout';
interface AirlockProps {
}
@ -18,6 +20,7 @@ export const Airlock: React.FunctionComponent<AirlockProps> = (props: AirlockPro
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const workspaceCtx = useContext(WorkspaceContext);
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const theme = getTheme();
const navigate = useNavigate();
@ -41,7 +44,9 @@ export const Airlock: React.FunctionComponent<AirlockProps> = (props: AirlockPro
requests.sort((a, b) => a.updatedWhen < b.updatedWhen ? 1 : -1);
setAirlockRequests(requests);
setLoadingState(LoadingState.Ok);
} catch (error) {
} catch (err: any) {
err.userMessage = 'Error fetching airlock requests';
setApiError(err);
setLoadingState(LoadingState.Error);
}
}
@ -173,13 +178,7 @@ export const Airlock: React.FunctionComponent<AirlockProps> = (props: AirlockPro
break;
case LoadingState.Error:
requestsList = (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error fetching airlock requests</h3>
<p>There was an error fetching the airlock requests. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
); break;
default:
requestsList = (

Просмотреть файл

@ -1,4 +1,4 @@
import { DefaultButton, Dialog, DialogFooter, IStackItemStyles, IStackStyles, MessageBar, MessageBarType, Panel, PanelType, Persona, PersonaSize, PrimaryButton, Spinner, SpinnerSize, Stack, TextField, useTheme } from "@fluentui/react";
import { DefaultButton, Dialog, DialogFooter, IStackItemStyles, IStackStyles, MessageBar, Panel, PanelType, Persona, PersonaSize, PrimaryButton, Spinner, SpinnerSize, Stack, TextField, useTheme } from "@fluentui/react";
import moment from "moment";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
@ -6,6 +6,8 @@ import { WorkspaceContext } from "../../../contexts/WorkspaceContext";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
import { AirlockRequest, AirlockRequestStatus } from "../../../models/airlock";
import { ApiEndpoint } from "../../../models/apiEndpoints";
import { APIError } from "../../../models/exceptions";
import { ExceptionLayout } from "../ExceptionLayout";
interface AirlockViewRequestProps {
requests: AirlockRequest[];
@ -40,6 +42,9 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
const [hideCancelDialog, setHideCancelDialog] = useState(true);
const workspaceCtx = useContext(WorkspaceContext);
const apiCall = useAuthApiCall();
const [apiFilesLinkError, setApiFilesLinkError] = useState({} as APIError);
const [apiSubmitError, setApiSubmitError] = useState({} as APIError);
const [apiCancelError, setApiCancelError] = useState({} as APIError);
const navigate = useNavigate();
const theme = useTheme();
@ -68,7 +73,9 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
workspaceCtx.workspaceApplicationIdURI
);
setFilesLink(linkObject.containerUrl);
} catch (error) {
} catch (err: any) {
err.userMessage = 'Error retrieving storage link';
setApiFilesLinkError(err);
setFilesLinkError(true);
}
}
@ -89,7 +96,9 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
);
props.updateRequest(response.airlockRequest);
setHideSubmitDialog(true);
} catch (error) {
} catch (err: any) {
err.userMessage = 'Error submitting airlock request';
setApiSubmitError(err);
setSubmitError(true);
}
setSubmitting(false);
@ -109,8 +118,11 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
);
props.updateRequest(response.airlockRequest);
setHideCancelDialog(true);
} catch (error) {
} catch (err: any) {
err.userMessage = 'Error cancelling airlock request';
setApiCancelError(err);
setCancelError(true);
}
setCancelling(false);
}
@ -129,10 +141,10 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
}
<div style={{textAlign: 'end'}}>
{
request.status !== AirlockRequestStatus.Cancelled && <DefaultButton onClick={() => setHideCancelDialog(false)} styles={cancelButtonStyles}>Cancel Request</DefaultButton>
request.status !== AirlockRequestStatus.Cancelled && <DefaultButton onClick={() => {setCancelError(false); setHideCancelDialog(false)}} styles={cancelButtonStyles}>Cancel Request</DefaultButton>
}
{
request.status === AirlockRequestStatus.Draft && <PrimaryButton onClick={() => setHideSubmitDialog(false)}>Submit</PrimaryButton>
request.status === AirlockRequestStatus.Draft && <PrimaryButton onClick={() => {setSubmitError(false); setHideSubmitDialog(false)}}>Submit</PrimaryButton>
}
</div>
</>
@ -236,14 +248,12 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
iconProps={{iconName: 'copy'}}
styles={{root: {minWidth: '40px'}}}
onClick={() => {navigator.clipboard.writeText(filesLink)}}
/> : <PrimaryButton onClick={generateFilesLink}>Generate</PrimaryButton>
/> : <PrimaryButton onClick={() => {setFilesLinkError(false); generateFilesLink()}}>Generate</PrimaryButton>
}
</Stack>
</Stack.Item>
{
filesLinkError && <MessageBar messageBarType={MessageBarType.error}>
Error retrieving storage link. Check console.
</MessageBar>
filesLinkError && <ExceptionLayout e={apiFilesLinkError} />
}
</Stack>
</>
@ -260,7 +270,7 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
}}
>
{
submitError && <MessageBar messageBarType={MessageBarType.error}>Error submitting request. Check the console for details.</MessageBar>
submitError && <ExceptionLayout e={apiSubmitError} />
}
{
submitting
@ -281,7 +291,7 @@ export const AirlockViewRequest: React.FunctionComponent<AirlockViewRequestProps
}}
>
{
cancelError && <MessageBar messageBarType={MessageBarType.error}>Error cancelling request. Check the console for details.</MessageBar>
cancelError && <ExceptionLayout e={apiCancelError} />
}
{
cancelling

Просмотреть файл

@ -1,4 +1,4 @@
import { MessageBar, MessageBarType, Spinner, SpinnerSize } from "@fluentui/react";
import { Spinner, SpinnerSize } from "@fluentui/react";
import { useEffect, useState } from "react";
import { LoadingState } from "../../../models/loadingState";
import { HttpMethod, ResultType, useAuthApiCall } from "../../../hooks/useAuthApiCall";
@ -6,6 +6,8 @@ import Form from "@rjsf/fluent-ui";
import { Operation } from "../../../models/operation";
import { Resource } from "../../../models/resource";
import { ResourceType } from "../../../models/resourceType";
import { APIError } from "../../../models/exceptions";
import { ExceptionLayout } from "../ExceptionLayout";
interface ResourceFormProps {
templateName: string,
@ -20,9 +22,9 @@ export const ResourceForm: React.FunctionComponent<ResourceFormProps> = (props:
const [template, setTemplate] = useState<any | null>(null);
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
const [deployError, setDeployError] = useState(false);
const [sendingData, setSendingData] = useState(false);
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
useEffect(() => {
const getFullTemplate = async () => {
@ -41,7 +43,9 @@ export const ResourceForm: React.FunctionComponent<ResourceFormProps> = (props:
setTemplate(templateResponse);
setLoading(LoadingState.Ok);
} catch {
} catch (err: any){
err.userMessage = "Error retrieving resource template";
setApiError(err);
setLoading(LoadingState.Error);
}
};
@ -53,30 +57,32 @@ export const ResourceForm: React.FunctionComponent<ResourceFormProps> = (props:
}, [apiCall, props.templatePath, template, props.updateResource]);
const createUpdateResource = async (formData: any) => {
setDeployError(false);
setSendingData(true);
let response;
if (props.updateResource) {
// only send the properties we're allowed to send
let d: any = {}
for (let prop in template.properties) {
if (!template.properties[prop].readOnly) d[prop] = formData[prop];
try
{
if (props.updateResource) {
// only send the properties we're allowed to send
let d: any = {}
for (let prop in template.properties) {
if (!template.properties[prop].readOnly) d[prop] = formData[prop];
}
console.log("patching resource", d);
let wsAuth = props.updateResource.resourceType === ResourceType.WorkspaceService || props.updateResource.resourceType === ResourceType.UserResource;
response = await apiCall(props.updateResource.resourcePath, HttpMethod.Patch, wsAuth ? props.workspaceApplicationIdURI : undefined, { properties: d }, ResultType.JSON, undefined, undefined, props.updateResource._etag);
} else {
const resource = { templateName: props.templateName, properties: formData };
console.log(resource);
response = await apiCall(props.resourcePath, HttpMethod.Post, props.workspaceApplicationIdURI, resource, ResultType.JSON);
}
console.log("patching resource", d);
let wsAuth = props.updateResource.resourceType === ResourceType.WorkspaceService || props.updateResource.resourceType === ResourceType.UserResource;
response = await apiCall(props.updateResource.resourcePath, HttpMethod.Patch, wsAuth ? props.workspaceApplicationIdURI : undefined, { properties: d }, ResultType.JSON, undefined, undefined, props.updateResource._etag);
} else {
const resource = { templateName: props.templateName, properties: formData };
console.log(resource);
response = await apiCall(props.resourcePath, HttpMethod.Post, props.workspaceApplicationIdURI, resource, ResultType.JSON);
}
setSendingData(false);
if (response) {
props.onCreateResource(response.operation);
} else {
setDeployError(true);
} catch (err: any) {
err.userMessage = 'Error sending create / update request';
setApiError(err);
setLoading(LoadingState.Error);
}
setSendingData(false);
}
// use the supplied uiSchema or create a blank one, and set the overview field to textarea manually.
@ -106,23 +112,11 @@ export const ResourceForm: React.FunctionComponent<ResourceFormProps> = (props:
:
<Form schema={template} formData={formData} uiSchema={uiSchema} onSubmit={(e: any) => createUpdateResource(e.formData)} />
}
{
deployError &&
<MessageBar messageBarType={MessageBarType.error}>
<p>The API returned an error. Check the console for details or retry.</p>
</MessageBar>
}
</div>
)
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving template</h3>
<p>There was an error retrieving the full resource template. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
);
default:
return (

Просмотреть файл

@ -2,6 +2,8 @@ import { DefaultButton, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack
import { useEffect, useState } from "react";
import { LoadingState } from "../../../models/loadingState";
import { HttpMethod, useAuthApiCall } from "../../../hooks/useAuthApiCall";
import { APIError } from "../../../models/exceptions";
import { ExceptionLayout } from "../ExceptionLayout";
interface SelectTemplateProps {
templatesPath: string,
@ -12,6 +14,7 @@ export const SelectTemplate: React.FunctionComponent<SelectTemplateProps> = (pro
const [templates, setTemplates] = useState<any[] | null>(null);
const [loading, setLoading] = useState(LoadingState.Loading as LoadingState);
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
useEffect(() => {
const getTemplates = async () => {
@ -20,7 +23,9 @@ export const SelectTemplate: React.FunctionComponent<SelectTemplateProps> = (pro
const templatesResponse = await apiCall(props.templatesPath, HttpMethod.Get);
setTemplates(templatesResponse.templates);
setLoading(LoadingState.Ok);
} catch {
} catch (err: any){
err.userMessage = 'Error retrieving templates';
setApiError(err);
setLoading(LoadingState.Error);
}
};
@ -29,7 +34,7 @@ export const SelectTemplate: React.FunctionComponent<SelectTemplateProps> = (pro
if (!templates) {
getTemplates();
}
});
}, [apiCall, props.templatesPath, templates]);
switch (loading) {
case LoadingState.Ok:
@ -56,13 +61,7 @@ export const SelectTemplate: React.FunctionComponent<SelectTemplateProps> = (pro
)
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving templates</h3>
<p>There was an error retrieving resource templates. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
);
default:
return (

Просмотреть файл

@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { Icon, ProgressIndicator, Link as FluentLink, Stack, DefaultPalette, Shimmer, ShimmerElementType, MessageBar, MessageBarType } from '@fluentui/react';
import { Icon, ProgressIndicator, Link as FluentLink, Stack, DefaultPalette, Shimmer, ShimmerElementType } from '@fluentui/react';
import { TRENotification } from '../../../models/treNotification';
import { awaitingStates, completedStates, failedStates, inProgressStates, Operation, OperationStep } from '../../../models/operation';
import { Link } from 'react-router-dom';
@ -10,6 +10,8 @@ import { ApiEndpoint } from '../../../models/apiEndpoints';
import { getResourceFromResult, Resource } from '../../../models/resource';
import { NotificationPoller } from './NotificationPoller';
import { OperationsContext } from '../../../contexts/OperationsContext';
import { APIError } from '../../../models/exceptions';
import { ExceptionLayout } from '../ExceptionLayout';
interface NotificationItemProps {
operation: Operation,
@ -25,6 +27,7 @@ export const NotificationItem: React.FunctionComponent<NotificationItemProps> =
const opsCtx = useContext(OperationsContext);
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const getRelativeTime = (createdWhen: number) => {
return (moment.utc(moment.unix(createdWhen))).from(now);
@ -60,6 +63,8 @@ export const NotificationItem: React.FunctionComponent<NotificationItemProps> =
}
setNotification({ operation: op, resource: resource, workspace: ws });
} catch (err: any) {
err.userMessage = `Error retrieving operation details for ${props.operation.id}`
setApiError(err);
setErrorNotification(true);
}
setLoadingNotification(false);
@ -104,13 +109,7 @@ export const NotificationItem: React.FunctionComponent<NotificationItemProps> =
:
errorNotification ?
<li>
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving operation details</h3>
<p>We were unable to get more information about the operation {props.operation.id}. This might be because the associated resource has been deleted. Please investigate with your administrators to get the data cleaned up.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
</li>
:
<li className="tre-notification-item">

Просмотреть файл

@ -1,4 +1,4 @@
import { MessageBar, MessageBarType, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { Spinner, SpinnerSize, Stack } from '@fluentui/react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Route, Routes, useParams } from 'react-router-dom';
import { ApiEndpoint } from '../../models/apiEndpoints';
@ -16,6 +16,9 @@ import { SharedService } from '../../models/sharedService';
import { SharedServices } from '../shared/SharedServices';
import { SharedServiceItem } from '../shared/SharedServiceItem';
import { Airlock } from '../shared/airlock/Airlock';
import { APIError } from '../../models/exceptions';
import { LoadingState } from '../../models/loadingState';
import { ExceptionLayout } from '../shared/ExceptionLayout';
export const WorkspaceProvider: React.FunctionComponent = () => {
const apiCall = useAuthApiCall();
@ -23,9 +26,11 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
const [workspaceServices, setWorkspaceServices] = useState([] as Array<WorkspaceService>)
const [sharedServices, setSharedServices] = useState([] as Array<SharedService>)
const workspaceCtx = useRef(useContext(WorkspaceContext));
const [loadingState, setLoadingState] = useState('loading');
const [loadingState, setLoadingState] = useState(LoadingState.Loading);
const [ apiError, setApiError ] = useState({} as APIError);
const { workspaceId } = useParams();
// set workspace context from url
useEffect(() => {
const getWorkspace = async () => {
@ -46,14 +51,16 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
// get workspace services to pass to nav + ws services page
const workspaceServices = await apiCall(`${ApiEndpoint.Workspaces}/${ws.id}/${ApiEndpoint.WorkspaceServices}`, HttpMethod.Get, ws_application_id_uri);
setWorkspaceServices(workspaceServices.workspaceServices);
setLoadingState(wsRoles && wsRoles.length > 0 ? 'ok' : 'denied');
setLoadingState(wsRoles && wsRoles.length > 0 ? LoadingState.Ok : LoadingState.AccessDenied);
// get shared services to pass to nav shared services pages
const sharedServices = await apiCall(ApiEndpoint.SharedServices, HttpMethod.Get);
setSharedServices(sharedServices.sharedServices);
} catch {
setLoadingState('error');
} catch (e: any){
e.userMessage = 'Error retrieving workspace';
setApiError(e);
setLoadingState(LoadingState.Error);
}
};
getWorkspace();
@ -89,7 +96,7 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
}
switch (loadingState) {
case 'ok':
case LoadingState.Ok:
return (
<>
<WorkspaceHeader />
@ -142,27 +149,9 @@ export const WorkspaceProvider: React.FunctionComponent = () => {
</Stack>
</>
);
case 'denied':
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={true}
>
<h3>Access Denied</h3>
<p>
You do not have access to this Workspace. If you feel you should have access, please speak to your TRE Administrator. <br />
If you have recently been given access, you may need to clear you browser local storage and refresh.</p>
</MessageBar>
);
case 'error':
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving workspace</h3>
<p>There was an error retrieving this workspace. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
)
default:
return (

Просмотреть файл

@ -4,7 +4,7 @@ import { ApiEndpoint } from '../../models/apiEndpoints';
import { useAuthApiCall, HttpMethod } from '../../hooks/useAuthApiCall';
import { UserResource } from '../../models/userResource';
import { WorkspaceService } from '../../models/workspaceService';
import { MessageBar, MessageBarType, PrimaryButton, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { PrimaryButton, Spinner, SpinnerSize, Stack } from '@fluentui/react';
import { ComponentAction, Resource } from '../../models/resource';
import { ResourceCardList } from '../shared/ResourceCardList';
import { LoadingState } from '../../models/loadingState';
@ -18,6 +18,8 @@ import { UserResourceItem } from './UserResourceItem';
import { ResourceBody } from '../shared/ResourceBody';
import { SecuredByRole } from '../shared/SecuredByRole';
import { WorkspaceRoleName } from '../../models/roleNames';
import { APIError } from '../../models/exceptions';
import { ExceptionLayout } from '../shared/ExceptionLayout';
interface WorkspaceServiceItemProps {
workspaceService?: WorkspaceService,
@ -36,6 +38,8 @@ export const WorkspaceServiceItem: React.FunctionComponent<WorkspaceServiceItemP
const createFormCtx = useContext(CreateUpdateResourceContext);
const navigate = useNavigate();
const apiCall = useAuthApiCall();
const [apiError, setApiError] = useState({} as APIError);
const latestUpdate = useComponentManager(
workspaceService,
(r: Resource) => { props.updateWorkspaceService(r as WorkspaceService); setWorkspaceService(r as WorkspaceService) },
@ -64,7 +68,9 @@ export const WorkspaceServiceItem: React.FunctionComponent<WorkspaceServiceItemP
setHasUserResourceTemplates(ut && ut.templates && ut.templates.length > 0);
setUserResources(u.userResources);
setLoadingState(LoadingState.Ok);
} catch {
} catch (err: any) {
err.userMessage = "Error retrieving resources";
setApiError(err);
setLoadingState(LoadingState.Error);
}
};
@ -100,15 +106,16 @@ export const WorkspaceServiceItem: React.FunctionComponent<WorkspaceServiceItemP
<>
<ResourceHeader resource={workspaceService} latestUpdate={latestUpdate} />
<ResourceBody resource={workspaceService} />
{
hasUserResourceTemplates &&
<Stack className="tre-panel">
<Stack.Item>
<Stack horizontal horizontalAlign="space-between">
<h1>User Resources</h1>
<h1>Resources</h1>
<SecuredByRole allowedRoles={[WorkspaceRoleName.WorkspaceOwner, WorkspaceRoleName.WorkspaceResearcher]} workspaceAuth={true} element={
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new" disabled={!workspaceService.isEnabled || latestUpdate.componentAction === ComponentAction.Lock || successStates.indexOf(workspaceService.deploymentStatus) === -1} title={!workspaceService.isEnabled ? 'Service must be enabled first' : 'Create a User Resource'}
<PrimaryButton iconProps={{ iconName: 'Add' }} text="Create new"
disabled={!workspaceService.isEnabled || latestUpdate.componentAction === ComponentAction.Lock || successStates.indexOf(workspaceService.deploymentStatus) === -1}
title={(!workspaceService.isEnabled || latestUpdate.componentAction === ComponentAction.Lock || successStates.indexOf(workspaceService.deploymentStatus) === -1) ? 'Service must be enabled, successfully deployed, and not locked' : 'Create a User Resource'}
onClick={() => {
createFormCtx.openCreateForm({
resourceType: ResourceType.UserResource,
@ -148,13 +155,7 @@ export const WorkspaceServiceItem: React.FunctionComponent<WorkspaceServiceItemP
);
case LoadingState.Error:
return (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
>
<h3>Error retrieving workspace</h3>
<p>There was an error retrieving this workspace. Please see the browser console for details.</p>
</MessageBar>
<ExceptionLayout e={apiError} />
);
default:
return (

Просмотреть файл

@ -1,127 +1,144 @@
import { AuthenticationResult, InteractionRequiredAuthError } from "@azure/msal-browser";
import { useMsal, useAccount } from "@azure/msal-react";
import { useCallback } from "react";
import { APIError } from "../models/exceptions";
import config from "../config.json";
export enum ResultType {
JSON = "JSON",
Text = "Text",
None = "None"
JSON = "JSON",
Text = "Text",
None = "None"
}
export enum HttpMethod {
Get = "GET",
Post = "POST",
Patch = "PATCH",
Delete = "DELETE"
Get = "GET",
Post = "POST",
Patch = "PATCH",
Delete = "DELETE"
}
export const useAuthApiCall = () => {
const { instance, accounts } = useMsal();
const account = useAccount(accounts[0] || {});
const { instance, accounts } = useMsal();
const account = useAccount(accounts[0] || {});
const parseJwt = (token: string) => {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const parseJwt = (token: string) => {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
return JSON.parse(jsonPayload);
}
return useCallback(async (
endpoint: string,
method: HttpMethod,
workspaceApplicationIdURI?: string,
body?: any,
resultType?: ResultType,
setRoles?: (roles: Array<string>) => void,
tokenOnly?: boolean,
etag?: string) => {
if (!account) {
console.error("No account object found, please refresh.");
return;
}
return useCallback(async (
endpoint: string,
method: HttpMethod,
workspaceApplicationIdURI?: string,
body?: any,
resultType?: ResultType,
setRoles?: (roles: Array<string>) => void,
tokenOnly?: boolean,
etag?: string) => {
const applicationIdURI = workspaceApplicationIdURI || config.treApplicationId;
let tokenResponse = {} as AuthenticationResult;
let tokenRequest = {
scopes: [`${applicationIdURI}/user_impersonation`],
account: account
}
if (!account) {
console.error("No account object found, please refresh.");
return;
}
// try and get a token silently. at times this might throw an InteractionRequiredAuthError - if so give the user a popup to click
try {
tokenResponse = await instance.acquireTokenSilent(tokenRequest);
} catch (err) {
console.warn("Unable to get a token silently", err);
if (err instanceof InteractionRequiredAuthError) {
tokenResponse = await instance.acquireTokenPopup(tokenRequest);
}
}
const applicationIdURI = workspaceApplicationIdURI || config.treApplicationId;
let tokenResponse = {} as AuthenticationResult;
let tokenRequest = {
scopes: [`${applicationIdURI}/user_impersonation`],
account: account
}
config.debug && console.log("Token Response", tokenResponse);
// try and get a token silently. at times this might throw an InteractionRequiredAuthError - if so give the user a popup to click
try {
tokenResponse = await instance.acquireTokenSilent(tokenRequest);
} catch (err) {
console.warn("Unable to get a token silently", err);
if (err instanceof InteractionRequiredAuthError) {
tokenResponse = await instance.acquireTokenPopup(tokenRequest);
}
}
if (!tokenResponse) {
console.error("Token could not be retrieved, please refresh.");
return;
}
config.debug && console.log("Token Response", tokenResponse);
// caller can pass a function to allow us to set the roles to use for RBAC
if (setRoles) {
let decodedToken = parseJwt(tokenResponse.accessToken);
config.debug && console.log("Decoded token", decodedToken);
setRoles(decodedToken.roles);
}
if (!tokenResponse) {
console.error("Token could not be retrieved, please refresh.");
return;
}
// we might just want the token to get the roles.
if (tokenOnly) return;
// caller can pass a function to allow us to set the roles to use for RBAC
if (setRoles) {
let decodedToken = parseJwt(tokenResponse.accessToken);
config.debug && console.log("Decoded token", decodedToken);
setRoles(decodedToken.roles);
}
// trim first slash if we're given one
if (endpoint[0] === "/") endpoint = endpoint.substring(1);
// we might just want the token to get the roles.
if (tokenOnly) return;
// default to JSON unless otherwise told
resultType = resultType || ResultType.JSON;
config.debug && console.log(`Calling ${method} on authenticated api: ${endpoint}`);
// default to JSON unless otherwise told
resultType = resultType || ResultType.JSON;
config.debug && console.log(`Calling ${method} on authenticated api: ${endpoint}`);
// set the headers for auth + http method
const opts: RequestInit = {
mode: "cors",
headers: {
Authorization: `Bearer ${tokenResponse.accessToken}`,
'Content-Type': 'application/json',
'etag': etag ? etag : ""
},
method: method
}
// set the headers for auth + http method
const opts: RequestInit = {
mode: "cors",
headers: {
Authorization: `Bearer ${tokenResponse.accessToken}`,
'Content-Type': 'application/json',
'etag': etag ? etag : ""
},
method: method
}
// add a body if we're given one
if (body) opts.body = JSON.stringify(body);
// add a body if we're given one
if (body) opts.body = JSON.stringify(body);
let resp;
try {
resp = await fetch(`${config.treUrl}/${endpoint}`, opts);
} catch (err: any) {
console.error(err);
let e = err as APIError;
e.name = 'API call failure';
e.message = 'Unable to make call to API Backend';
e.endpoint = `${config.treUrl}/${endpoint}`;
throw e;
}
try {
let resp = await fetch(`${config.treUrl}/${endpoint}`, opts);
if (!resp.ok) {
let e = new APIError();
e.message = await resp.text();
e.status = resp.status;
e.endpoint = endpoint;
throw e;
}
if (!resp.ok) {
let message = `Error calling ${endpoint}: ${resp.status} - ${resp.statusText}`
throw(message);
}
try {
switch (resultType) {
case ResultType.Text:
let text = await resp.text();
config.debug && console.log(text);
return text;
case ResultType.JSON:
let json = await resp.json();
config.debug && console.log(json);
return json
case ResultType.None:
return;
}
} catch (err: any) {
let e = err as APIError;
e.name = "Error with response data";
throw e;
}
switch (resultType) {
case ResultType.Text:
let text = await resp.text();
config.debug && console.log(text);
return text;
case ResultType.JSON:
let json = await resp.json();
config.debug && console.log(json);
return json
case ResultType.None:
return;
}
} catch (err: any) {
// TODO: this is currently hiding errors, we should either rethrow to be handled in components
// or hook this up to user-facing alerts
console.error("Error calling API", err);
throw err;
}
}, [account, instance]);
}, [account, instance]);
}

Просмотреть файл

@ -0,0 +1,14 @@
class TREError extends Error {
constructor() {
super();
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class APIError extends TREError {
status?: number;
exception?: any;
userMessage?: string;
endpoint?: string;
}