From f51c51119e8a7f0ed31cce767ec2d79f3b7ecedd 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-upload-error/component.tsx | 4 +- .../containers/admin/data-uploader/index.ts | 1 - .../{component.tsx => index.tsx} | 28 ++++-- client/src/containers/uploader/component.tsx | 15 +--- .../src/containers/uploader/tracker/index.tsx | 86 +++++++++++++++++++ client/src/hooks/socket/index.ts | 51 +++++++++++ client/src/pages/data/index.tsx | 41 ++++++--- client/yarn.lock | 74 +++++++++++++++- 9 files changed, 263 insertions(+), 38 deletions(-) delete mode 100644 client/src/containers/admin/data-uploader/index.ts rename client/src/containers/admin/data-uploader/{component.tsx => index.tsx} (56%) create mode 100644 client/src/containers/uploader/tracker/index.tsx create mode 100644 client/src/hooks/socket/index.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-upload-error/component.tsx b/client/src/containers/admin/data-upload-error/component.tsx index 306d53057..b25251a30 100644 --- a/client/src/containers/admin/data-upload-error/component.tsx +++ b/client/src/containers/admin/data-upload-error/component.tsx @@ -50,7 +50,7 @@ const DataUploadError: React.FC = ({ task }) => { >
- {task?.status === 'processing' && ( + {/* {task?.status === 'processing' && ( <>

Upload in progress

@@ -58,7 +58,7 @@ const DataUploadError: React.FC = ({ task }) => { {format(new Date(task.createdAt), 'MMM d, yyyy HH:mm z')}.

- )} + )} */} {task?.status === 'completed' && task?.errors.length === 0 && ( <> diff --git a/client/src/containers/admin/data-uploader/index.ts b/client/src/containers/admin/data-uploader/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/admin/data-uploader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/admin/data-uploader/component.tsx b/client/src/containers/admin/data-uploader/index.tsx similarity index 56% rename from client/src/containers/admin/data-uploader/component.tsx rename to client/src/containers/admin/data-uploader/index.tsx index 23a2a8acf..c3f73b224 100644 --- a/client/src/containers/admin/data-uploader/component.tsx +++ b/client/src/containers/admin/data-uploader/index.tsx @@ -1,14 +1,13 @@ -import { useState } from 'react'; 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 { useLasTask } from '@/hooks/tasks'; -import type { Task } from 'types'; - -const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => { - const [isUploading, setIsUploading] = useState(false); +const AdminDataUploader: React.FC = () => { + const { data: lastTask } = useLasTask(); return (
@@ -27,14 +26,25 @@ const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => { Download template
-
+

2. Upload the filled Excel file.

- - {!isUploading && task?.status === 'failed' && } + {lastTask?.status !== 'processing' && ( +
+ +
+ )} + + {lastTask?.status === 'processing' && ( +
+ +
+ )} + + {lastTask?.status === 'failed' && }
); }; -export default AdminDataPage; +export default AdminDataUploader; diff --git a/client/src/containers/uploader/component.tsx b/client/src/containers/uploader/component.tsx index 5a1ab080e..c339b6a20 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 isUploading={isWorking} />
- - {isWorking && ( -
-
-
-

- {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..b7fb17989 --- /dev/null +++ b/client/src/containers/uploader/tracker/index.tsx @@ -0,0 +1,86 @@ +import { FC, useCallback, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; + +import { cn } from '@/lib/utils'; +import { formatPercentage } from '@/utils/number-format'; +import useSocket from '@/hooks/socket'; + +const STEPS_ORDER = [ + 'VALIDATING_DATA', + 'GEOCODING', + 'IMPORTING_DATA', + 'CALCULATING_IMPACT', +] as const; + +const STEPS_NAMES = { + VALIDATING_DATA: 'Validating Data', + IMPORTING_DATA: 'Importing Data', + GEOCODING: 'Geocoding', + CALCULATING_IMPACT: 'Calculating Impact', +} as const; + +type ProgressTask = { + kind: 'DATA_IMPORT_PROGRESS'; + data: Record< + keyof typeof STEPS_NAMES, + { + status: 'processing' | 'idle' | 'completed'; + progress: number; + } + >; +}; + +export const UploadTracker: FC = () => { + const queryClient = useQueryClient(); + const [tasksProgress, setTaskProgress] = useState(undefined); + + const onProgress = useCallback(({ data }: ProgressTask) => { + setTaskProgress(data); + }, []); + + const onSettle = useCallback(() => { + queryClient.invalidateQueries(['tasks']); + queryClient.invalidateQueries(['sourcingLocations']); + }, [queryClient]); + + useSocket({ + DATA_IMPORT_PROGRESS: onProgress, + DATA_IMPORT_COMPLETED: onSettle, + DATA_IMPORT_FAILURE: onSettle, + }); + + // if (!Object.keys(tasksProgress || {}).length) return null; + + return ( +
+
+ {Object.keys(STEPS_NAMES) + .sort( + (a, b) => + STEPS_ORDER.indexOf(a as keyof typeof STEPS_NAMES) - + STEPS_ORDER.indexOf(b as keyof typeof STEPS_NAMES), + ) + .map((key: keyof typeof STEPS_NAMES) => ( +
+ + {STEPS_NAMES[key]} + + {`Progress: ${ + tasksProgress?.[key]?.progress + ? formatPercentage(tasksProgress[key].progress / 100) + : '–' + }`} +
+ ))} +
+
+ ); +}; + +export default UploadTracker; diff --git a/client/src/hooks/socket/index.ts b/client/src/hooks/socket/index.ts new file mode 100644 index 000000000..04fa81419 --- /dev/null +++ b/client/src/hooks/socket/index.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef } from 'react'; +import { io, SocketOptions, Socket } from 'socket.io-client'; +import { useSession } from 'next-auth/react'; + +import { env } from '@/env.mjs'; + +const useSocket = ( + events: { [key: string]: (...args: unknown[]) => void } = {}, + options?: SocketOptions, +) => { + const socketRef = useRef(undefined); + const { data: { accessToken = undefined } = {} } = useSession(); + + useEffect(() => { + const socket = socketRef.current; + + if (socket || !accessToken) return () => {}; + + socketRef.current = io(env.NEXT_PUBLIC_API_URL, { + transports: ['websocket'], + extraHeaders: { + authorization: accessToken, + }, + ...options, + }); + + return () => { + socketRef.current.disconnect(); + }; + }, [accessToken, options]); + + useEffect(() => { + const socket = socketRef.current; + + if (!socket) return () => {}; + + Object.entries(events).forEach(([event, handler]) => { + socket.on(event, handler); + }); + + return () => { + Object.entries(events).forEach(([event, handler]) => { + socket.off(event, handler); + }); + }; + }, [events]); + + return socketRef.current; +}; + +export default useSocket; diff --git a/client/src/pages/data/index.tsx b/client/src/pages/data/index.tsx index c6e69d30c..bb0f1d2f7 100644 --- a/client/src/pages/data/index.tsx +++ b/client/src/pages/data/index.tsx @@ -1,26 +1,46 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import Head from 'next/head'; +import axios from 'axios'; +import { useSession } from 'next-auth/react'; import { useSourcingLocations } from 'hooks/sourcing-locations'; import { useLasTask } from 'hooks/tasks'; import AdminLayout from 'layouts/admin'; import AdminDataUploader from 'containers/admin/data-uploader'; import AdminDataTable from 'containers/admin/data-table'; -import Loading from 'components/loading'; import Search from 'components/search'; +import { env } from '@/env.mjs'; const AdminDataPage: React.FC = () => { // Getting sourcing locations to check if there are any data - const { data, isFetched, isLoading } = useSourcingLocations({ + const { data, isFetched } = useSourcingLocations({ fields: 'updatedAt', 'page[number]': 1, 'page[size]': 1, }); + const { data: { accessToken = undefined } = {} } = useSession(); + + // ! THIS IS FOR TESTING.REMOVE + const triggerWipe = useCallback(() => { + axios + .get(env.NEXT_PUBLIC_API_URL + '/api/v1/import/wipeout', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .catch(() => { + console.log('Error wiping data'); + }); + }, [accessToken]); + // Getting last task to check if there is a processing task const { data: lastTask } = useLasTask(); - const thereIsData = useMemo(() => data?.meta?.totalItems > 0, [data?.meta?.totalItems]); + const thereIsData = useMemo( + () => isFetched && data?.meta?.totalItems > 0, + [isFetched, data?.meta?.totalItems], + ); return ( { Manage data | Landgriffon - {(!isFetched || isLoading) && ( -
- -
- )} + {/* // ! THIS IS FOR TESTING.REMOVE */} + {/* Content when empty, or upload is processing or failed */} - {isFetched && !thereIsData && } + {!thereIsData && } {/* Content when data and upload is completed */} - {isFetched && thereIsData && } + {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"