Skip to content

Commit

Permalink
Fix GraphQL Exception handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh-Matsuoka committed Jul 9, 2024
1 parent 1074c00 commit 8c2e7b2
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 12 deletions.
7 changes: 5 additions & 2 deletions src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { ErrorView } from '@app/ErrorView/ErrorView';
import { authFailMessage, isAuthFail } from '@app/ErrorView/types';
import { authFailMessage, isAuthFail, missingSSLMessage } from '@app/ErrorView/types';
import { LoadingView } from '@app/Shared/Components/LoadingView';
import {
emptyAutomatedAnalysisFilters,
Expand Down Expand Up @@ -45,7 +45,7 @@ import {
AutomatedAnalysisScore,
AnalysisResult,
} from '@app/Shared/Services/api.types';
import { isGraphQLAuthError } from '@app/Shared/Services/api.utils';
import { isGraphQLAuthError, isGraphQLSSLError } from '@app/Shared/Services/api.utils';
import { FeatureLevel } from '@app/Shared/Services/service.types';
import { automatedAnalysisConfigToRecordingAttributes } from '@app/Shared/Services/service.utils';
import { ServiceContext } from '@app/Shared/Services/Services';
Expand Down Expand Up @@ -363,6 +363,9 @@ export const AutomatedAnalysisCard: DashboardCardFC<AutomatedAnalysisCardProps>
if (isGraphQLAuthError(resp)) {
context.target.setAuthFailure();
throw new Error(authFailMessage);
} else if (isGraphQLSSLError(resp)) {
context.target.setSslFailure();
throw new Error(missingSSLMessage);
} else {
throw new Error(resp.errors[0].message);
}
Expand Down
54 changes: 54 additions & 0 deletions src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { useTranslation } from 'react-i18next';
import { interval } from 'rxjs';
import { DashboardCard } from '../../DashboardCard';
import { ChartContext } from '../context';
import { ErrorView } from '@app/ErrorView/ErrorView';

Check warning on line 50 in src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

`@app/ErrorView/ErrorView` import should occur before import of `@app/Settings/types`

Check warning on line 50 in src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

`@app/ErrorView/ErrorView` import should occur before import of `@app/Settings/types`
import { missingSSLMessage, authFailMessage, isAuthFail } from '@app/ErrorView/types';

Check warning on line 51 in src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

`@app/ErrorView/types` import should occur before import of `@app/Settings/types`

Check warning on line 51 in src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

`@app/ErrorView/types` import should occur before import of `@app/Settings/types`

export interface MBeanMetricsChartCardProps extends DashboardCardTypeProps {
themeColor: string;
Expand Down Expand Up @@ -102,6 +104,58 @@ const SimpleChart: React.FC<{
[units, interpolation],
);

const serviceContext = React.useContext(ServiceContext);
const addSubscription = useSubscriptions();
const [errorMessage, setErrorMessage] = React.useState('');

React.useEffect(() => {
addSubscription(
serviceContext.target.sslFailure().subscribe(() => {
setErrorMessage(missingSSLMessage);
}),
);
}, [serviceContext.target, serviceContext.target.sslFailure, setErrorMessage, addSubscription]);

React.useEffect(() => {
addSubscription(
serviceContext.target.authRetry().subscribe(() => {
setErrorMessage(''); // Reset on retry
}),
);
}, [serviceContext.target, serviceContext.target.authRetry, setErrorMessage, addSubscription]);

React.useEffect(() => {
addSubscription(
serviceContext.target.authFailure().subscribe(() => {
setErrorMessage(authFailMessage);
}),
);
}, [serviceContext.target, serviceContext.target.authFailure, setErrorMessage, addSubscription]);

React.useEffect(() => {
addSubscription(
serviceContext.target.target().subscribe(() => {
setErrorMessage(''); // Reset on change
}),
);
}, [serviceContext.target, serviceContext.target.target, setErrorMessage, addSubscription]);

const authRetry = React.useCallback(() => {
serviceContext.target.setAuthRetry();
}, [serviceContext.target]);

const isError = React.useMemo(() => errorMessage != '', [errorMessage]);

if (isError) {
return (
<ErrorView
title={'Error displaying Mbean Metrics'}
message={errorMessage}
retry={isAuthFail(errorMessage) ? authRetry : undefined}
/>
);
}

return (
<Chart
containerComponent={
Expand Down
17 changes: 16 additions & 1 deletion src/app/RecordingMetadata/BulkEditLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import { hashCode, portalRoot } from '@app/utils/utils';
import { Button, Split, SplitItem, Stack, StackItem, Text, Tooltip, ValidatedOptions } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { combineLatest, concatMap, filter, first, forkJoin, map, Observable, of } from 'rxjs';
import { combineLatest, concatMap, filter, first, forkJoin, map, Observable, of, tap } from 'rxjs';
import { RecordingLabelFields } from './RecordingLabelFields';
import { includesLabel } from './utils';
import { isGraphQLAuthError, isGraphQLSSLError } from '@app/Shared/Services/api.utils';

Check warning on line 38 in src/app/RecordingMetadata/BulkEditLabels.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

`@app/Shared/Services/api.utils` import should occur before import of `@app/Shared/Services/Services`

Check warning on line 38 in src/app/RecordingMetadata/BulkEditLabels.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

`@app/Shared/Services/api.utils` import should occur before import of `@app/Shared/Services/Services`
import { authFailMessage, missingSSLMessage } from '@app/ErrorView/types';

Check warning on line 39 in src/app/RecordingMetadata/BulkEditLabels.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

`@app/ErrorView/types` import should occur before import of `@app/RecordingMetadata/LabelCell`

Check warning on line 39 in src/app/RecordingMetadata/BulkEditLabels.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

`@app/ErrorView/types` import should occur before import of `@app/RecordingMetadata/LabelCell`

export interface BulkEditLabelsProps {
isTargetRecording: boolean;
Expand Down Expand Up @@ -213,6 +215,19 @@ export const BulkEditLabels: React.FC<BulkEditLabelsProps> = ({
{ id: target.id! },
),
),
tap((resp) => {
if (resp.data == undefined) {
if (isGraphQLAuthError(resp)) {
context.target.setAuthFailure();
throw new Error(authFailMessage);
} else if (isGraphQLSSLError(resp)) {
context.target.setSslFailure();
throw new Error(missingSSLMessage);
} else {
throw new Error(resp.errors[0].message);
}
}
}),
map((v) => (v.data?.targetNodes[0]?.target?.archivedRecordings?.data as ArchivedRecording[]) ?? []),
first(),
);
Expand Down
35 changes: 32 additions & 3 deletions src/app/Recordings/ArchivedRecordingsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@ import { Tbody, Tr, Td, ExpandableRowContent, TableComposable, SortByDirection }
import * as React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Observable, forkJoin, merge, combineLatest } from 'rxjs';
import { concatMap, filter, first, map } from 'rxjs/operators';
import { concatMap, filter, first, map, tap } from 'rxjs/operators';
import { LabelCell } from '../RecordingMetadata/LabelCell';
import { RecordingActions } from './RecordingActions';
import { RecordingFiltersCategories, filterRecordings, RecordingFilters } from './RecordingFilters';
import { RecordingLabelsPanel } from './RecordingLabelsPanel';
import { ColumnConfig, RecordingsTable } from './RecordingsTable';
import { isGraphQLAuthError, isGraphQLSSLError } from '@app/Shared/Services/api.utils';

Check warning on line 86 in src/app/Recordings/ArchivedRecordingsTable.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

`@app/Shared/Services/api.utils` import should occur before import of `@app/Shared/Services/Services`

Check warning on line 86 in src/app/Recordings/ArchivedRecordingsTable.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

`@app/Shared/Services/api.utils` import should occur before import of `@app/Shared/Services/Services`
import { authFailMessage, missingSSLMessage } from '@app/ErrorView/types';

Check warning on line 87 in src/app/Recordings/ArchivedRecordingsTable.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

`@app/ErrorView/types` import should occur before import of `@app/Modal/DeleteWarningModal`

Check warning on line 87 in src/app/Recordings/ArchivedRecordingsTable.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

`@app/ErrorView/types` import should occur before import of `@app/Modal/DeleteWarningModal`

const tableColumns: TableColumn[] = [
{
Expand Down Expand Up @@ -247,7 +249,21 @@ export const ArchivedRecordingsTable: React.FC<ArchivedRecordingsTableProps> = (
} else if (isUploadsTable) {
addSubscription(
queryUploadedRecordings()
.pipe(map((v) => (v?.data?.archivedRecordings?.data as ArchivedRecording[]) ?? []))
.pipe(
tap((resp) => {
if (resp.data == undefined) {
if (isGraphQLAuthError(resp)) {
context.target.setAuthFailure();
throw new Error(authFailMessage);
} else if (isGraphQLSSLError(resp)) {
context.target.setSslFailure();
throw new Error(missingSSLMessage);
} else {
throw new Error(resp.errors[0].message);
}
}
}),
map((v) => (v?.data?.archivedRecordings?.data as ArchivedRecording[]) ?? []))
.subscribe({
next: handleRecordings,
error: handleError,
Expand All @@ -259,7 +275,20 @@ export const ArchivedRecordingsTable: React.FC<ArchivedRecordingsTableProps> = (
.pipe(
filter((target) => !!target),
first(),
concatMap((target: Target) => queryTargetRecordings(target.id!)),
concatMap((target: Target) => queryTargetRecordings(target.id!).pipe(
tap((resp) => {
if (resp.data == undefined) {
if (isGraphQLAuthError(resp)) {
context.target.setAuthFailure();
throw new Error(authFailMessage);
} else if (isGraphQLSSLError(resp)) {
throw new Error(missingSSLMessage);
} else {
throw new Error(resp.errors[0].message);
}
}
}),
)),
map((v) => (v.data?.targetNodes[0]?.target?.archivedRecordings?.data as ArchivedRecording[]) ?? []),
)
.subscribe({
Expand Down
18 changes: 16 additions & 2 deletions src/app/Shared/Services/Api.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ import {
TargetMetadata,
isTargetMetadata,
} from './api.types';
import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils';
import { isHttpError, includesTarget, isHttpOk, isXMLHttpError, isGraphQLAuthError, isGraphQLSSLError } from './api.utils';
import { LoginService } from './Login.service';
import { NotificationService } from './Notifications.service';
import { SessionState } from './service.types';
import { TargetService } from './Target.service';
import { authFailMessage, missingSSLMessage } from '@app/ErrorView/types';

export class ApiService {
private readonly archiveEnabled = new ReplaySubject<boolean>(1);
Expand Down Expand Up @@ -1278,7 +1279,7 @@ export class ApiService {
}

getTargetMBeanMetrics(target: TargetStub, queries: string[]): Observable<MBeanMetrics> {
return this.graphql<MBeanMetricsResponse>(
return this.graphql<any>(
`
query MBeanMXMetricsForTarget($id: BigInteger!) {
targetNodes(filter: { targetIds: [$id] }) {
Expand All @@ -1291,6 +1292,19 @@ export class ApiService {
}`,
{ id: target.id! },
).pipe(
tap((resp) => {
if (resp.data == undefined) {
if (isGraphQLAuthError(resp)) {
this.target.setAuthFailure();
throw new Error(authFailMessage);
} else if (isGraphQLSSLError(resp)) {
this.target.setSslFailure();
throw new Error(missingSSLMessage);
} else {
throw new Error(resp.errors[0].message);
}
}
}),
map((resp) => {
const nodes = resp.data?.targetNodes ?? [];
if (!nodes || nodes.length === 0) {
Expand Down
14 changes: 14 additions & 0 deletions src/app/Shared/Services/api.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export const isActiveRecording = (toCheck: Recording): toCheck is ActiveRecordin
return (toCheck as ActiveRecording).state !== undefined;
};

// ======================================
// GraphQL Error Handling utils
// ======================================

/* eslint @typescript-eslint/no-explicit-any: 0 */
export const isGraphQLAuthError = (resp: any): boolean => {
if (resp.errors !== undefined) {
Expand All @@ -69,6 +73,16 @@ export const isGraphQLAuthError = (resp: any): boolean => {
return false;
};

/* eslint @typescript-eslint/no-explicit-any: 0 */
export const isGraphQLSSLError = (resp: any): boolean => {
if (resp.errors !== undefined) {
if (resp.errors[0].message.includes('Bad Gateway')) {
return true;
}
}
return false;
};

// ======================================
// Template utils
// ======================================
Expand Down
64 changes: 60 additions & 4 deletions src/app/Topology/Entity/EntityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
* limitations under the License.
*/


import { authFailMessage, isAuthFail, missingSSLMessage } from '@app/ErrorView/types';
import { LinearDotSpinner } from '@app/Shared/Components/LinearDotSpinner';
import { EnvironmentNode, MBeanMetrics, MBeanMetricsResponse, TargetNode } from '@app/Shared/Services/api.types';

Check warning on line 20 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

'MBeanMetricsResponse' is defined but never used

Check warning on line 20 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

'MBeanMetricsResponse' is defined but never used
import { isTargetNode } from '@app/Shared/Services/api.utils';
import { isGraphQLAuthError, isGraphQLSSLError, isTargetNode } from '@app/Shared/Services/api.utils';

Check warning on line 21 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

'isGraphQLAuthError' is defined but never used

Check warning on line 21 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

'isGraphQLAuthError' is defined but never used
import { ServiceContext } from '@app/Shared/Services/Services';
import { ActionDropdown } from '@app/Topology/Actions/NodeActions';
import useDayjs from '@app/utils/hooks/useDayjs';
Expand Down Expand Up @@ -49,7 +51,7 @@ import { ExpandableRowContent, TableComposable, Tbody, Td, Th, Thead, Tr } from
import { GraphElement, NodeStatus } from '@patternfly/react-topology';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { catchError, concatMap, map, of } from 'rxjs';
import { catchError, concatMap, map, of, tap } from 'rxjs';
import { EmptyText } from '../../Shared/Components/EmptyText';
import { NodeAction } from '../Actions/types';
import { actionFactory } from '../Actions/utils';
Expand All @@ -75,6 +77,8 @@ import {
mapSection,
useResources,
} from './utils';
import { ErrorView } from '@app/ErrorView/ErrorView';


export interface EntityDetailsProps {
entity?: GraphElement | ListElement;
Expand Down Expand Up @@ -106,6 +110,58 @@ export const EntityDetails: React.FC<EntityDetailsProps> = ({

const _actions = actionFactory(entity, 'dropdownItem', actionFilter);

const [errorMessage, setErrorMessage] = React.useState('');

Check failure on line 113 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 113 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
const addSubscription = useSubscriptions();

Check failure on line 114 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "useSubscriptions" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 114 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "useSubscriptions" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
const services = React.useContext(ServiceContext);

Check failure on line 115 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useContext" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 115 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useContext" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

React.useEffect(() => {

Check failure on line 117 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 117 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
addSubscription(
services.target.sslFailure().subscribe(() => {
// also triggered if api calls in Custom Recording form fail
setErrorMessage(missingSSLMessage);
}),
);
}, [services.target, setErrorMessage, addSubscription]);

React.useEffect(() => {

Check failure on line 126 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 126 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
addSubscription(
services.target.authRetry().subscribe(() => {
setErrorMessage(''); // Reset on retry
}),
);
}, [services.target, setErrorMessage, addSubscription]);

React.useEffect(() => {

Check failure on line 134 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 134 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
addSubscription(
services.target.authFailure().subscribe(() => {
// also triggered if api calls in Custom Recording form fail
setErrorMessage(authFailMessage);
}),
);
}, [services.target, setErrorMessage, addSubscription]);

React.useEffect(() => {

Check failure on line 143 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 143 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useEffect" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
addSubscription(
services.target.target().subscribe(() => {
setErrorMessage(''); // Reset on change
}),
);
}, [services.target, setErrorMessage, addSubscription]);

const authRetry = React.useCallback(() => {

Check failure on line 151 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

React Hook "React.useCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

Check failure on line 151 in src/app/Topology/Entity/EntityDetails.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

React Hook "React.useCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
services.target.setAuthRetry();
}, [services.target]);

if (errorMessage != '') {
return (
<ErrorView
title={'Error displaying Mbean Metrics'}
message={errorMessage}
retry={isAuthFail(errorMessage) ? authRetry : undefined}
/>
);
}

return (
<>
<EntityDetailHeader
Expand Down Expand Up @@ -267,7 +323,7 @@ const MBeanDetails: React.FC<{
if (isExpanded) {
addSubscription(
context.api
.graphql<MBeanMetricsResponse>(
.graphql<any>(
`
query MBeanMXMetricsForTarget($id: BigInteger!) {
targetNodes(filter: { targetIds: [$id] }) {
Expand Down Expand Up @@ -306,7 +362,7 @@ const MBeanDetails: React.FC<{
.subscribe(setMbeanMetrics),
);
}
}, [isExpanded, addSubscription, targetId, context.api, setMbeanMetrics]);
}, [isExpanded, addSubscription, targetId, context.api, setMbeanMetrics, context.target]);

const _collapsedData = React.useMemo((): DescriptionConfig[] => {
return [
Expand Down

0 comments on commit 8c2e7b2

Please sign in to comment.