Skip to content

Commit

Permalink
MGMT-15374: Unties common-to-ocm dependencies (openshift-assisted#2279)
Browse files Browse the repository at this point in the history
* Breaks common-to-ocm dependencies

* Breaks circular dependencies

Moves fileSize and stringToJson to lib/common/utils.ts in order to break circular dependencies
  • Loading branch information
jkilzi authored and rawagner committed Sep 13, 2023
1 parent 760f7f8 commit 506e5eb
Show file tree
Hide file tree
Showing 43 changed files with 743 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { fileSize } from '../../../common';
import { AgentK8sResource } from '../../types';
import { TFunction } from 'i18next';
import { useTranslation } from '../../../common/hooks/use-translation-wrapper';
import { fileSize } from '../../../common/utils';

export const getTotalCompute = (selectedAgents: AgentK8sResource[], t: TFunction) => {
const totals = selectedAgents.reduce(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import AddNodePoolModal from '../modals/AddNodePoolModal';
import { AgentMachineK8sResource, HostedClusterK8sResource, NodePoolK8sResource } from '../types';
import RemoveNodePoolModal from '../modals/RemoveNodePoolModal';
import NodePoolStatus from './NodePoolStatus';
import { fileSize } from '../../../../common';
import { useTranslation } from '../../../../common/hooks/use-translation-wrapper';
import { getNodepoolAgents } from '../utils';

import './NodePoolsTable.css';
import { fileSize } from '../../../../common/utils';

type NodePoolsTableProps = {
nodePools: NodePoolK8sResource[];
Expand Down
56 changes: 56 additions & 0 deletions libs/ui-lib/lib/common/api/axiosClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import axios, { AxiosInstance } from 'axios';
import applyCaseMiddleware from 'axios-case-converter';
import { camelCase } from 'camel-case';

// conforms basePath in swagger.json
export const BASE_PATH = '/api/assisted-install';

// Prevent axios converter to change object keys from '4.7-fc2' to '4_7Fc2'
const axiosCaseConverterOptions = {
caseFunctions: {
camel: (input: string) =>
camelCase(input, {
stripRegexp: /[^A-Z0-9.-]+/gi,
}),
},
};

const getDefaultClient = (withoutConverter = false) => {
const client = axios.create();
client.interceptors.request.use((cfg) => ({
...cfg,
url: `${process.env.AIUI_APP_API_ROOT || ''}${cfg.url || ''}`,
}));
if (withoutConverter) {
return client;
} else {
return applyCaseMiddleware(client, axiosCaseConverterOptions);
}
};

let client: AxiosInstance = getDefaultClient();
let clientWithoutConverter: AxiosInstance = getDefaultClient(true);
let ocmClient: AxiosInstance | null;
let isInOcm = false;

const aiInterceptor = (client: AxiosInstance) => {
client.interceptors.request.use((cfg) => ({
...cfg,
url: `${BASE_PATH}${cfg.url || ''}`,
}));
return client;
};

const getOcmClient = () => ocmClient;

export const setAuthInterceptor = (authInterceptor: (client: AxiosInstance) => AxiosInstance) => {
ocmClient = authInterceptor(axios.create());
isInOcm = true;
client = applyCaseMiddleware(
aiInterceptor(authInterceptor(axios.create())),
axiosCaseConverterOptions,
);
clientWithoutConverter = aiInterceptor(authInterceptor(axios.create()));
};

export { client, getOcmClient, isInOcm, clientWithoutConverter };
11 changes: 11 additions & 0 deletions libs/ui-lib/lib/common/api/axiosExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AxiosError } from 'axios';
import { hasProp, isNonNullObject } from '../types/typescriptExtensions';

// Implementation from Axios.isAxiosError v0.29.2, which is not available in OCM's version 0.17.x
export function isAxiosError<T = unknown, D = unknown>(
payload: unknown,
): payload is AxiosError<T, D> {
return (
isNonNullObject(payload) && hasProp(payload, 'isAxiosError') && payload.isAxiosError === true
);
}
1 change: 1 addition & 0 deletions libs/ui-lib/lib/common/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './axiosClient';
export * from './types';
export * from './utils';
122 changes: 107 additions & 15 deletions libs/ui-lib/lib/common/api/utils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,114 @@
import camelCase from 'lodash-es/camelCase.js';

export const stringToJSON = <T>(jsonString: string | undefined): T | undefined => {
let jsObject: T | undefined;
if (jsonString) {
try {
const camelCased = jsonString.replace(
/"([\w-]+)":/g,
(_match, offset: string) => `"${camelCase(offset)}":`,
);
jsObject = JSON.parse(camelCased) as T;
} catch (e) {
// console.error('Failed to parse api string', e, jsonString);
import Axios, { AxiosError } from 'axios';
import * as Sentry from '@sentry/browser';
import pick from 'lodash-es/pick.js';
import { Error as APIError, InfraError } from './types';
import { getErrorMessage } from '../utils';
import { isAxiosError } from './axiosExtensions';
import { isInOcm } from './axiosClient';

export const FETCH_ABORTED_ERROR_CODE = 'ERR_CANCELED';
export const FETCH_CONNECTIVITY_ERROR_CODE = 'CONNECTIVITY_ERROR';
export const SERVER_ERROR_CODE = 'SERVER_ERROR';
export const PAGE_RELOAD_ERROR = 'ECONNABORTED';

type OnError = (arg0: unknown) => void;

export const handleApiError = (error: unknown, onError?: OnError): void => {
if (Axios.isCancel(error)) {
captureException(error, 'Request canceled', Sentry.Severity.Info);
} else if (isApiError(error)) {
const config = error.config || { url: '', method: '' };
let message = `URL: ${JSON.stringify(config.url, null, 1)}\n`;
message += `Method: ${JSON.stringify(config.method, null, 1)}\n`;
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
message += `Status: ${error.response.status}\n`;
message += `Response: ${JSON.stringify(
pick(error.response.data, ['code', 'message', 'reason']),
null,
1,
)}\n`;
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
message += `Status Code: ${JSON.stringify(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.request.__sentry_xhr__.status_code,
null,
1,
)}`;
}
captureException(error, message);
} else {
// console.info('Empty api string received.');
captureException(error);
}
if (onError) return onError(error);
};

export const getApiErrorMessage = (error: unknown): string => {
if (isApiError(error)) {
return error.response?.data?.message || error.response?.data.reason || error.message;
}
return getErrorMessage(error);
};

return jsObject;
export const isUnknownServerError = (error: Error): boolean => {
return getApiErrorCode(error) === SERVER_ERROR_CODE;
};

export const getApiErrorCode = (error: Error | AxiosError): string | number => {
if (!isAxiosError(error)) {
return FETCH_CONNECTIVITY_ERROR_CODE;
}
// Aborted request
if (error.code === FETCH_ABORTED_ERROR_CODE) {
return FETCH_ABORTED_ERROR_CODE;
}

// Page is reloading and some request has been interrupted
if (error.code === PAGE_RELOAD_ERROR) {
return '';
}

const responseStatus = error.response?.status || 0;

// Error status
if (responseStatus >= 500 && responseStatus < 600) {
return SERVER_ERROR_CODE;
}
if (responseStatus >= 400 && responseStatus < 500) {
return responseStatus;
}
// A generic connectivity issue
return FETCH_CONNECTIVITY_ERROR_CODE;
};

export type APIErrorMixin = InfraError & APIError;
export type AIAxiosErrorType = AxiosError<APIErrorMixin, APIErrorMixin>;

export const isApiError = (error: unknown): error is AIAxiosErrorType => {
if (!isAxiosError<AIAxiosErrorType, AIAxiosErrorType>(error)) {
return false;
}
return typeof error.response?.data === 'object';
};

export const captureException = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
message?: string,
severity: Sentry.Severity = Sentry.Severity.Error,
) => {
if (isInOcm) {
message && Sentry.captureMessage(message, severity);
Sentry.captureException(error);
} else {
// severity === Sentry.Severity.Error
// ? console.error(message, error)
// : console.warn(message, error);
}
};

export const removeProtocolFromURL = (url = '') => url.replace(/^(http|https):\/\//, '');
Loading

0 comments on commit 506e5eb

Please sign in to comment.