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

Feat: excel export #430

Merged
merged 2 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 51 additions & 52 deletions src/api/generated/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import axios from 'axios';
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
import FormData from 'form-data';

import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
Expand Down Expand Up @@ -38,6 +42,10 @@ export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};

export const isSuccess = (status: number): boolean => {
return status >= 200 && status < 300;
};

export const base64 = (str: string): string => {
try {
return btoa(str);
Expand Down Expand Up @@ -136,22 +144,24 @@ export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Reso
return resolver;
};

export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const additionalHeaders = await resolve(options, config.HEADERS);
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}

const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formHeaders,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);

if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
Expand All @@ -174,75 +184,63 @@ export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptio
}
}

return new Headers(headers);
return headers;
};

export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
if (options.body) {
return options.body;
}
return undefined;
};

export const sendRequest = async (
export const sendRequest = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();

const request: RequestInit = {
headers: Record<string, string>,
onCancel: OnCancel,
axiosClient: AxiosInstance
): Promise<AxiosResponse<T>> => {
const source = axios.CancelToken.source();

const requestConfig: AxiosRequestConfig = {
url,
headers,
body: body ?? formData,
data: body ?? formData,
method: options.method,
signal: controller.signal,
withCredentials: config.WITH_CREDENTIALS,
cancelToken: source.token,
};

if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}

onCancel(() => controller.abort());
onCancel(() => source.cancel('The user aborted a request.'));

return await fetch(url, request);
try {
return await axiosClient.request(requestConfig);
} catch (error) {
const axiosError = error as AxiosError<T>;
if (axiosError.response) {
return axiosError.response;
}
throw error;
}
};

export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return undefined;
};

export const getResponseBody = async (response: Response): Promise<any> => {
export const getResponseBody = (response: AxiosResponse<any>): any => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const jsonTypes = ['application/json', 'application/problem+json']
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
return response.data;
}
return undefined;
};
Expand Down Expand Up @@ -285,25 +283,26 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @param axiosClient The axios client instance to use
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
const headers = await getHeaders(config, options, formData);

if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

const result: ApiResult = {
url,
ok: response.ok,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
Expand Down
21 changes: 21 additions & 0 deletions src/api/generated/services/DownloadsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,25 @@ export class DownloadsService {
});
}

/**
* @param analogueModelIds
* @returns File Success
* @throws ApiError
*/
public static getApiDownloadsAnalogueModelsExcel(
analogueModelIds?: Array<string>,
): CancelablePromise<File> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/downloads/analogue-models-excel',
query: {
'AnalogueModelIds': analogueModelIds,
},
errors: {
403: `Forbidden`,
404: `Not Found`,
},
});
}

}
37 changes: 35 additions & 2 deletions src/features/ModelTable/ModelTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable no-empty-pattern */
/* eslint-disable max-lines-per-function */
import { CSSProperties } from 'react';
import { ChangeEvent, CSSProperties } from 'react';
import { useMsal } from '@azure/msal-react';
import { Button } from '@equinor/eds-core-react';
import { Button, Checkbox } from '@equinor/eds-core-react';
import { EdsDataGrid } from '@equinor/eds-data-grid-react';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
Expand All @@ -14,8 +14,11 @@ import {
} from '../../api/generated';
import { useAccessToken } from '../../hooks/useAccessToken';
import * as Styled from './ModelTable.styled';
import { usePepmContextStore } from '../../hooks/GlobalState';

export const ModelTable = () => {
const { addExportModel, deleteExportModel, exportModels } =
usePepmContextStore();
const { instance, accounts } = useMsal();
const token = useAccessToken(instance, accounts[0]);
if (token) OpenAPI.TOKEN = token;
Expand Down Expand Up @@ -83,6 +86,11 @@ export const ModelTable = () => {
return status;
};

const addModelToExport = (checked: boolean, modelId: string) => {
if (checked) addExportModel(modelId);
else deleteExportModel(modelId);
};

/* Make sure the header row in EdsDataGrid is vertically middle-aligned when filter icons are shown */
const headerStyle = (): CSSProperties => ({ verticalAlign: 'middle' });

Expand All @@ -102,6 +110,31 @@ export const ModelTable = () => {
headerStyle={headerStyle}
width="min(calc(100vw - 64px), 1400px)"
columns={[
{
accessorKey: 'analogueModelId',
enableColumnFilter: false,
header: function () {
<Styled.List>{<Checkbox></Checkbox>}</Styled.List>;
},
id: 'expand',
cell: ({ row }) => (
<Styled.List>
{
<Checkbox
checked={
exportModels.indexOf(row.original.analogueModelId) > -1
}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
addModelToExport(
e.target.checked,
row.original.analogueModelId,
)
}
></Checkbox>
}
</Styled.List>
),
},
{ accessorKey: 'name', header: 'Model name', id: 'name', size: 200 },
{
id: 'outcrops',
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/GlobalState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type IPepmContext = {
computeSettings: ListComputeSettingsMethodDto[];
objectResults: GetObjectResultsDto[];
variogramResults: GetVariogramResultsDto[];
exportModels: string[];
};

type IPepmContextActions = {
Expand Down Expand Up @@ -94,6 +95,8 @@ type IPepmContextActions = {
setVariogramResults: (variogramResults: GetVariogramResultsDto[]) => void;
updateObjectResult: (objectResult: GetObjectResultsDto) => void;
updateVariogramResult: (variogramResult: GetVariogramResultsDto) => void;
addExportModel: (modelId: string) => void;
deleteExportModel: (modelId: string) => void;
};

export const usePepmContextStore = create<IPepmContext & IPepmContextActions>()(
Expand All @@ -111,6 +114,7 @@ export const usePepmContextStore = create<IPepmContext & IPepmContextActions>()(
computeSettings: [],
objectResults: [],
variogramResults: [],
exportModels: [],
setAnalogueModel: (analogueModel: AnalogueModelDetail) =>
set((state) => {
state.analogueModel = analogueModel;
Expand Down Expand Up @@ -247,5 +251,13 @@ export const usePepmContextStore = create<IPepmContext & IPepmContextActions>()(
: variogramResult,
);
}),
addExportModel: (modelId: string) =>
set((state) => {
state.exportModels.push(modelId);
}),
deleteExportModel: (modelId: string) =>
set((state) => {
state.exportModels = state.exportModels.filter((id) => id !== modelId);
}),
})),
);
43 changes: 43 additions & 0 deletions src/hooks/useFetchAnaloguesExcel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { OpenAPI } from '../api/generated';
import axios from 'axios';

export const getFetchAnaloguesExcelAxios = async (
exportModels: string[],
): Promise<string> => {
const token = OpenAPI.TOKEN; // replace with your bearer token
const base = OpenAPI.BASE;
let params = '';

if (exportModels.length > 0) {
exportModels.forEach((element, index) => {
if (index === 0) params = '?AnalogueModelIds=' + element;
else params += '&AnalogueModelIds=' + element;
});
}

const response = await axios.get(
'/api/downloads/analogue-models-excel' + params,
{
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob', // response type of blob to handle images
baseURL: base,
},
);

if (response.data) {
const fileURL = window.URL.createObjectURL(response.data);

const link = document.createElement('a');
link.href = fileURL;
link.download = 'export.xlsx';

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(fileURL);
}

// create an object URL for the image blob and return it
return URL.createObjectURL(response.data);
};
Loading
Loading