From a500fad15e66a41945ad2d02b62a1c0faedd9c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 17 Apr 2024 17:33:30 +0200 Subject: [PATCH] WIP --- client/package.json | 1 + .../admin/data-uploader/component.tsx | 14 +- client/src/containers/uploader/component.tsx | 27 +- .../src/containers/uploader/tracker/index.tsx | 251 ++++++++++++++++++ client/src/lib/socket.ts | 7 + client/src/pages/data/index.tsx | 9 +- client/yarn.lock | 74 +++++- 7 files changed, 363 insertions(+), 20 deletions(-) create mode 100644 client/src/containers/uploader/tracker/index.tsx create mode 100644 client/src/lib/socket.ts diff --git a/client/package.json b/client/package.json index a9cb3915a..b462f6574 100644 --- a/client/package.json +++ b/client/package.json @@ -93,6 +93,7 @@ "recharts": "2.9.0", "rooks": "7.14.1", "sharp": "0.32.6", + "socket.io-client": "4.7.5", "tailwind-merge": "2.2.1", "tailwindcss": "3.4.1", "tailwindcss-animate": "1.0.7", diff --git a/client/src/containers/admin/data-uploader/component.tsx b/client/src/containers/admin/data-uploader/component.tsx index 23a2a8acf..668b3d45d 100644 --- a/client/src/containers/admin/data-uploader/component.tsx +++ b/client/src/containers/admin/data-uploader/component.tsx @@ -4,6 +4,7 @@ import { DownloadIcon } from '@heroicons/react/solid'; import DataUploadError from 'containers/admin/data-upload-error'; import DataUploader from 'containers/uploader'; import { Anchor } from 'components/button'; +import UploadTracker from '@/containers/uploader/tracker'; import type { Task } from 'types'; @@ -27,9 +28,18 @@ const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => { Download template -
+

2. Upload the filled Excel file.

- + {false && ( +
+ +
+ )} + {true && ( +
+ +
+ )} {!isUploading && task?.status === 'failed' && }
diff --git a/client/src/containers/uploader/component.tsx b/client/src/containers/uploader/component.tsx index 5a1ab080e..5d93bbee5 100644 --- a/client/src/containers/uploader/component.tsx +++ b/client/src/containers/uploader/component.tsx @@ -96,7 +96,7 @@ const DataUploader: React.FC = ({ variant = 'default', onUplo }, [isWorking, onUploadInProgress]); return ( -
+
= ({ variant = 'default', onUplo />
- {isWorking && ( -
-
-
-

- {isUploading && 'Uploading file...'} - {isWaiting && 'File uploaded successfully! Starting to process the data...'} - {isProcessing && 'Processing file...'} -

-
-
- )} + {/* {true && ( + + //
+ //
+ //
+ //

+ // {isUploading && 'Uploading file...'} + // {isWaiting && 'File uploaded successfully! Starting to process the data...'} + // {isProcessing && 'Processing file...'} + //

+ //
+ //
+ )} */}
); }; diff --git a/client/src/containers/uploader/tracker/index.tsx b/client/src/containers/uploader/tracker/index.tsx new file mode 100644 index 000000000..15846062f --- /dev/null +++ b/client/src/containers/uploader/tracker/index.tsx @@ -0,0 +1,251 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import axios from 'axios'; + +import { cn } from '@/lib/utils'; +import { socket } from '@/lib/socket'; +import { env } from '@/env.mjs'; +import { formatPercentage } from '@/utils/number-format'; + +const STEPS_NAMES = { + VALIDATING: 'Validating', + 'DATA SOURCING': 'Data Sourcing', + GEOCODING: 'Geocoding', + 'IMPACT CALCULATION': 'Impact Calculation', +} as const; + +type ProgressTask = { + kind: 'DATA_IMPORT_PROGRESS'; + data: { + step: 'VALIDATING' | 'DATA SOURCING' | 'IMPACT CALCULATION' | 'GEOCODING'; + status: 'running' | 'idle' | 'completed'; + progress: number; + }[]; +}; + +const SAMPLE_DATA: ProgressTask[] = [ + { + kind: 'DATA_IMPORT_PROGRESS', + data: [ + { + step: 'VALIDATING', + status: 'running', + progress: 0, + }, + { + step: 'DATA SOURCING', + status: 'idle', + progress: 0, + }, + { + step: 'GEOCODING', + status: 'idle', + progress: 0, + }, + { + step: 'IMPACT CALCULATION', + status: 'idle', + progress: 0, + }, + ], + }, + { + kind: 'DATA_IMPORT_PROGRESS', + data: [ + { + step: 'VALIDATING', + status: 'running', + progress: 10, + }, + { + step: 'DATA SOURCING', + status: 'idle', + progress: 0, + }, + { + step: 'GEOCODING', + status: 'idle', + progress: 0, + }, + { + step: 'IMPACT CALCULATION', + status: 'idle', + progress: 0, + }, + ], + }, + { + kind: 'DATA_IMPORT_PROGRESS', + data: [ + { + step: 'VALIDATING', + status: 'running', + progress: 20, + }, + { + step: 'DATA SOURCING', + status: 'idle', + progress: 0, + }, + { + step: 'GEOCODING', + status: 'idle', + progress: 0, + }, + { + step: 'IMPACT CALCULATION', + status: 'idle', + progress: 0, + }, + ], + }, + { + kind: 'DATA_IMPORT_PROGRESS', + data: [ + { + step: 'VALIDATING', + status: 'completed', + progress: 100, + }, + { + step: 'DATA SOURCING', + status: 'running', + progress: 23, + }, + { + step: 'GEOCODING', + status: 'idle', + progress: 0, + }, + { + step: 'IMPACT CALCULATION', + status: 'idle', + progress: 0, + }, + ], + }, + { + kind: 'DATA_IMPORT_PROGRESS', + data: [ + { + step: 'VALIDATING', + status: 'completed', + progress: 100, + }, + { + step: 'DATA SOURCING', + status: 'completed', + progress: 100, + }, + { + step: 'GEOCODING', + status: 'running', + progress: 34, + }, + { + step: 'IMPACT CALCULATION', + status: 'idle', + progress: 0, + }, + ], + }, + { + kind: 'DATA_IMPORT_PROGRESS', + data: [ + { + step: 'VALIDATING', + status: 'completed', + progress: 100, + }, + { + step: 'DATA SOURCING', + status: 'completed', + progress: 100, + }, + { + step: 'GEOCODING', + status: 'completed', + progress: 100, + }, + { + step: 'IMPACT CALCULATION', + status: 'completed', + progress: 100, + }, + ], + }, +]; + +export const UploadTracker: FC = () => { + const [tasksProgress, setTaskProgress] = useState(SAMPLE_DATA[0]); + // ! THIS IS FOR TESTING.REMOVE + const [testIndex, setTestIndex] = useState(0); + + // ! THIS IS FOR TESTING.REMOVE + const triggerSocket = useCallback(() => { + axios.get(env.NEXT_PUBLIC_API_URL + '/tasks/progress'); + }, []); + + useEffect(() => { + socket.connect(); + + return () => { + socket.disconnect(); + }; + }, []); + + useEffect(() => { + function setTasksProgress(tasks: ProgressTask) { + setTaskProgress(tasks); + } + + socket.on('DATA_IMPORT_PROGRESS', setTasksProgress); + + return () => { + socket.off('DATA_IMPORT_PROGRESS', setTasksProgress); + }; + }, []); + + // ! THIS IS FOR TESTING.REMOVE + useEffect(() => { + function setTestProgress() { + setTaskProgress(SAMPLE_DATA[testIndex]); + + setTestIndex((prev) => (prev + 1 >= SAMPLE_DATA.length ? 0 : prev + 1)); + } + + const intervalId = window.setInterval(setTestProgress, 1500); + + return () => { + window.clearInterval(intervalId); + }; + }, [tasksProgress, testIndex]); + + return ( +
+ {/* // ! THIS IS FOR TESTING.REMOVE */} + +
+ {tasksProgress?.data?.map(({ step, status, progress }) => ( +
+ + {STEPS_NAMES[step]} + + {status !== 'idle' && ( + {`Progress: ${formatPercentage( + progress / 100, + )}`} + )} +
+ ))} +
+
+ ); +}; + +export default UploadTracker; diff --git a/client/src/lib/socket.ts b/client/src/lib/socket.ts new file mode 100644 index 000000000..29f011859 --- /dev/null +++ b/client/src/lib/socket.ts @@ -0,0 +1,7 @@ +import { io } from 'socket.io-client'; + +import { env } from '@/env.mjs'; + +export const socket = io(env.NEXT_PUBLIC_API_URL, { + autoConnect: false, +}); diff --git a/client/src/pages/data/index.tsx b/client/src/pages/data/index.tsx index c6e69d30c..d712d8112 100644 --- a/client/src/pages/data/index.tsx +++ b/client/src/pages/data/index.tsx @@ -39,17 +39,18 @@ const AdminDataPage: React.FC = () => { Manage data | Landgriffon - {(!isFetched || isLoading) && ( + {/* {(!isFetched || isLoading) && (
- )} + )} */} {/* Content when empty, or upload is processing or failed */} - {isFetched && !thereIsData && } + {/* {isFetched && !thereIsData && } */} + {true && } {/* Content when data and upload is completed */} - {isFetched && thereIsData && } + {/* {isFetched && thereIsData && } */} ); }; diff --git a/client/yarn.lock b/client/yarn.lock index e9cdd9d02..0e1f06929 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2556,6 +2556,13 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.1.0": + version: 3.1.1 + resolution: "@socket.io/component-emitter@npm:3.1.1" + checksum: 93792eafb63ad15259ba00885c3cf4fdc01d969b1db10a273ccac70bed2373b5170cbc94682372d666a44e4ad8faeb176fb6cbaaeeb66c87231e2ff3d72583f9 + languageName: node + linkType: hard + "@streamparser/json@npm:^0.0.12": version: 0.0.12 resolution: "@streamparser/json@npm:0.0.12" @@ -4953,7 +4960,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -5292,6 +5299,26 @@ __metadata: languageName: node linkType: hard +"engine.io-client@npm:~6.5.2": + version: 6.5.3 + resolution: "engine.io-client@npm:6.5.3" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + engine.io-parser: ~5.2.1 + ws: ~8.11.0 + xmlhttprequest-ssl: ~2.0.0 + checksum: a72596fae99afbdb899926fccdb843f8fa790c69085b881dde121285a6935da2c2c665ebe88e0e6aa4285637782df84ac882084ff4892ad2430b059fc0045db0 + languageName: node + linkType: hard + +"engine.io-parser@npm:~5.2.1": + version: 5.2.2 + resolution: "engine.io-parser@npm:5.2.2" + checksum: 470231215f3136a9259efb1268bc9a71f789af4e8c74da8d3b49ceb149fe3cd5c315bf0cd13d2d8d9c8f0f051c6f93b68e8fa9c89a3b612b9217bf33765c943a + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.10.0": version: 5.10.0 resolution: "enhanced-resolve@npm:5.10.0" @@ -7943,6 +7970,7 @@ __metadata: recharts: 2.9.0 rooks: 7.14.1 sharp: 0.32.6 + socket.io-client: 4.7.5 start-server-and-test: 1.14.0 tailwind-merge: 2.2.1 tailwindcss: 3.4.1 @@ -10785,6 +10813,28 @@ __metadata: languageName: node linkType: hard +"socket.io-client@npm:4.7.5": + version: 4.7.5 + resolution: "socket.io-client@npm:4.7.5" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.2 + engine.io-client: ~6.5.2 + socket.io-parser: ~4.2.4 + checksum: a6994b93a753d14292682ee97ba3c925c54b63e6fcb2ed5e0aa1d7c1d6164ed4a30d993f7eaaa3017ddf868ad0a1ab996badc8310129070136d84668789ee6c9 + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.2.4": + version: 4.2.4 + resolution: "socket.io-parser@npm:4.2.4" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263 + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -12238,6 +12288,28 @@ __metadata: languageName: node linkType: hard +"ws@npm:~8.11.0": + version: 8.11.0 + resolution: "ws@npm:8.11.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 + languageName: node + linkType: hard + +"xmlhttprequest-ssl@npm:~2.0.0": + version: 2.0.0 + resolution: "xmlhttprequest-ssl@npm:2.0.0" + checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e + languageName: node + linkType: hard + "y18n@npm:^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3"