Skip to content

Commit

Permalink
Add progress bar for file upload, and replace to use axios in api man…
Browse files Browse the repository at this point in the history
…ager
  • Loading branch information
dyang415 committed Sep 20, 2023
1 parent 071eafb commit 76be3a0
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 86 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"apache-arrow": "^12.0.1",
"axios": "^1.5.0",
"babel-jest": "^27.5.1",
"babel-loader": "^8.3.0",
"babel-plugin-named-asset-import": "^0.3.8",
Expand Down
22 changes: 22 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 60 additions & 58 deletions frontend/src/common/apiManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios, { AxiosError, AxiosProgressEvent, AxiosResponse } from "axios";
import url from "url";

export interface T {
Expand Down Expand Up @@ -31,81 +32,82 @@ export class APIManager {
);
}

private async handleResponse<T>(response: Response): Promise<T> {
const responseData: T = await response.json();

if (response.ok) {
return responseData;
} else {
throw {
statusCode: response.status,
error:
// @ts-ignore
responseData.error ?? "UNKNOWN_ERROR",
message:
// @ts-ignore
responseData.error ??
"Something wrong happened, please try again or contact founders@dsensei.app.",
};
private async handleRequest<T>(
request: () => Promise<AxiosResponse<T, any>>
): Promise<T> {
try {
return (await request()).data;
} catch (e) {
if (e instanceof AxiosError) {
// eslint-disable-next-line no-throw-literal
throw {
statusCode: e.response?.status,
error: e.response?.data.error,
message:
e.response?.data.error ??
"Something wrong happened, please try again or contact founders@dsensei.app.",
};
} else {
throw e;
}
}
}

public async get<T>(endpoint: string): Promise<T> {
const requestUrl = (this.baseUrl + endpoint).replace(/([^:]\/)\/+/g, "$1");
const response = await fetch(requestUrl, {
cache: "no-cache",
headers: jsonRequestHeaders,
});
private getRequestUrl(endpoint: string): string {
return (this.baseUrl + endpoint).replace(/([^:]\/)\/+/g, "$1");
}

return this.handleResponse<T>(response);
public async get<T>(endpoint: string): Promise<T> {
return await this.handleRequest<T>(
async () =>
await axios.get<T>(this.getRequestUrl(endpoint), {
headers: jsonRequestHeaders,
})
);
}

public async postForm<T>(endpoint: string, formData: FormData): Promise<T> {
public async postForm<T>(
endpoint: string,
formData: FormData,
onUploadProgress: (progressEvent: AxiosProgressEvent) => void = (
event
) => {}
): Promise<T> {
const requestUrl = (this.baseUrl + endpoint).replace(/([^:]\/)\/+/g, "$1");
const response = await fetch(requestUrl, {
cache: "no-cache",
method: "POST",
body: formData,
});

return this.handleResponse<T>(response);
return await this.handleRequest<T>(
async () =>
await axios.post<T>(requestUrl, formData, {
onUploadProgress,
})
);
}

public async post<T>(endpoint: string, body: object): Promise<T> {
const requestUrl = (this.baseUrl + endpoint).replace(/([^:]\/)\/+/g, "$1");
const response = await fetch(requestUrl, {
cache: "no-cache",
method: "POST",
headers: jsonRequestHeaders,
body: JSON.stringify(body),
});

return this.handleResponse<T>(response);
return await this.handleRequest<T>(
async () =>
await axios.post<T>(this.getRequestUrl(endpoint), body, {
headers: jsonRequestHeaders,
})
);
}

public async put<T>(endpoint: string, body: object): Promise<T> {
const requestUrl = (this.baseUrl + endpoint).replace(/([^:]\/)\/+/g, "$1");
const response = await fetch(requestUrl, {
cache: "no-cache",
method: "PUT",
headers: jsonRequestHeaders,
body: JSON.stringify(body),
});

return this.handleResponse<T>(response);
return await this.handleRequest<T>(
async () =>
await axios.put<T>(this.getRequestUrl(endpoint), body, {
headers: jsonRequestHeaders,
})
);
}

// Add methods for other HTTP methods like PUT, DELETE, etc. as needed
// Delete
public async delete<T>(endpoint: string): Promise<T> {
const requestUrl = (this.baseUrl + endpoint).replace(/([^:]\/)\/+/g, "$1");
const response = await fetch(requestUrl, {
cache: "no-cache",
method: "DELETE",
headers: jsonRequestHeaders,
});

return this.handleResponse<T>(response);
return await this.handleRequest<T>(
async () =>
await axios.delete<T>(this.getRequestUrl(endpoint), {
headers: jsonRequestHeaders,
})
);
}
}

Expand Down
13 changes: 11 additions & 2 deletions frontend/src/components/uploader/data-source-loader/CSVLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ import { CSVSchema } from "../../../types/data-source";

interface Props {
useSampleFile?: boolean;
onUploadingFile: (progress: number) => void;
onLoadingSchema: () => void;
onSchemaLoaded: (schema: CSVSchema) => void;
}

function CSVLoader({ useSampleFile, onLoadingSchema, onSchemaLoaded }: Props) {
function CSVLoader({
useSampleFile,
onLoadingSchema,
onSchemaLoaded,
onUploadingFile,
}: Props) {
const [error, setError] = useState<string>("");

useEffect(() => {
Expand All @@ -39,7 +45,10 @@ function CSVLoader({ useSampleFile, onLoadingSchema, onSchemaLoaded }: Props) {
try {
const schema = await apiManager.postForm<CSVSchema>(
"/api/v1/source/file/schema",
formData
formData,
(progressEvent) => {
onUploadingFile((progressEvent.progress ?? 0) * 100);
}
);
onSchemaLoaded(schema);
} catch (e) {
Expand Down
74 changes: 48 additions & 26 deletions frontend/src/routes/NewReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Divider,
Flex,
Grid,
ProgressBar,
Select,
SelectItem,
Text,
Expand All @@ -13,6 +14,7 @@ import {
import { useState } from "react";
import { NavBar } from "../common/navbar";
import getSettings from "../common/server-data/settings";
import { formatNumber } from "../common/utils";
import DataPreviewer from "../components/uploader/DataPreviewer";
import InformationCard from "../components/uploader/InformationCard";
import BigqueryLoader from "../components/uploader/data-source-loader/BigqueryLoader";
Expand All @@ -28,27 +30,35 @@ import {

function NewReport() {
const [isLoadingSchema, setIsLoadingSchema] = useState<boolean>(false);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [schema, setSchema] = useState<Schema>();
const [useSampleFile, setUseSampleFile] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<DataSourceType>("csv");

function renderCSVLoader() {
return (
<CSVLoader
useSampleFile={useSampleFile}
onLoadingSchema={() => {
setIsLoadingSchema(true);
}}
onSchemaLoaded={(schema: CSVSchema) => {
setSchema(schema);
setIsLoadingSchema(false);
}}
onUploadingFile={(progress) => setUploadProgress(progress)}
/>
);
}

function renderDataSourceLoader() {
if (window.location.hostname === "app.dsensei.app") {
return (
<>
<Flex justifyContent="center" className="pb-4">
<Text>Start a new report by uploading a CSV file</Text>
</Flex>
<CSVLoader
useSampleFile={useSampleFile}
onLoadingSchema={() => {
setIsLoadingSchema(true);
}}
onSchemaLoaded={(schema: CSVSchema) => {
setSchema(schema);
setIsLoadingSchema(false);
}}
/>
{renderCSVLoader()}
</>
);
}
Expand Down Expand Up @@ -80,18 +90,7 @@ function NewReport() {
</Flex>
)}
</Grid>
{dataSource === "csv" && (
<CSVLoader
useSampleFile={useSampleFile}
onLoadingSchema={() => {
setIsLoadingSchema(true);
}}
onSchemaLoaded={(schema: CSVSchema) => {
setSchema(schema);
setIsLoadingSchema(false);
}}
/>
)}
{dataSource === "csv" && renderCSVLoader()}
{dataSource === "bigquery" && (
<BigqueryLoader
onLoadingSchema={() => {
Expand All @@ -109,16 +108,39 @@ function NewReport() {
);
}

function renderProgress() {
if (dataSource !== "csv" || uploadProgress === 100.0) {
return (
<Flex className="h-64 gap-3" justifyContent="center">
<p>Processing...</p>
<span className="loading loading-bars loading-lg"></span>
</Flex>
);
}

if (uploadProgress < 100) {
return (
<Flex
className="h-64 gap-3 w-[60%]"
flexDirection="col"
justifyContent="center"
>
<ProgressBar value={uploadProgress} />
Uploading file &bull; {formatNumber(uploadProgress)}%
</Flex>
);
}
}

return (
<>
<NavBar />
<div className="flex flex-col gap-2 justify-center items-center pt-20">
<Title>New Report</Title>
{isLoadingSchema && !schema && (
<Card className="max-w-6xl">
<Flex className="h-64 gap-3" justifyContent="center">
<p>Processing</p>
<span className="loading loading-bars loading-lg"></span>
<Card className="max-w-6xl justify-center flex">
<Flex className="h-64 gap-3 w-[60%]" justifyContent="center">
{renderProgress()}
</Flex>
</Card>
)}
Expand Down

0 comments on commit 76be3a0

Please sign in to comment.