diff --git a/client/package.json b/client/package.json index 9bf1ab9c2..4c0482036 100644 --- a/client/package.json +++ b/client/package.json @@ -92,6 +92,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-table/index.ts b/client/src/containers/admin/data-table/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/admin/data-table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/admin/data-table/component.tsx b/client/src/containers/admin/data-table/index.tsx similarity index 86% rename from client/src/containers/admin/data-table/component.tsx rename to client/src/containers/admin/data-table/index.tsx index 7e7109f86..126507e73 100644 --- a/client/src/containers/admin/data-table/component.tsx +++ b/client/src/containers/admin/data-table/index.tsx @@ -4,32 +4,33 @@ import { format } from 'date-fns'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; -import useModal from 'hooks/modals'; +import useModal from '@/hooks/modals'; import { useSourcingLocations, useSourcingLocationsMaterials, useSourcingLocationsMaterialsTabularData, -} from 'hooks/sourcing-locations'; -import DownloadMaterialsDataButton from 'containers/admin/download-materials-data-button'; -import DataUploadError from 'containers/admin/data-upload-error'; -import DataUploader from 'containers/uploader'; -import Button, { Anchor } from 'components/button'; -import Modal from 'components/modal'; -import Table from 'components/table'; -import { DEFAULT_PAGE_SIZES } from 'components/table/pagination/constants'; -import { usePermissions } from 'hooks/permissions'; -import { RoleName } from 'hooks/permissions/enums'; +} from '@/hooks/sourcing-locations'; +import DownloadMaterialsDataButton from '@/containers/admin/download-materials-data-button'; +import DataUploadError from '@/containers/admin/data-upload-error'; +import DataUploader from '@/containers/uploader'; +import Button, { Anchor } from '@/components/button'; +import Modal from '@/components/modal'; +import Table from '@/components/table'; +import { DEFAULT_PAGE_SIZES } from '@/components/table/pagination/constants'; +import { usePermissions } from '@/hooks/permissions'; +import { RoleName } from '@/hooks/permissions/enums'; +import { useLasTask } from '@/hooks/tasks'; import type { PaginationState, SortingState, VisibilityState } from '@tanstack/react-table'; -import type { TableProps } from 'components/table/component'; -import type { Task } from 'types'; +import type { TableProps } from '@/components/table/component'; const YEARS_COLUMNS_UNIT = 't/yr'; -const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => { +const AdminDataPage: React.FC = () => { const { push, query } = useRouter(); const [sorting, setSorting] = useState([]); const { data: session } = useSession(); + const { data: task } = useLasTask(); const { hasRole } = usePermissions(); const isAdmin = hasRole(RoleName.ADMIN); @@ -220,7 +221,12 @@ const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => { Uploading a new file will replace all the current data.

- + { + if (!isUploadInProgress) closeUploadDataSourceModal(); + }} + />
diff --git a/client/src/containers/admin/data-upload-error/index.ts b/client/src/containers/admin/data-upload-error/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/admin/data-upload-error/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/admin/data-upload-error/component.tsx b/client/src/containers/admin/data-upload-error/index.tsx similarity index 86% rename from client/src/containers/admin/data-upload-error/component.tsx rename to client/src/containers/admin/data-upload-error/index.tsx index 306d53057..d6af0d006 100644 --- a/client/src/containers/admin/data-upload-error/component.tsx +++ b/client/src/containers/admin/data-upload-error/index.tsx @@ -1,15 +1,15 @@ import { useCallback, useState } from 'react'; import { format } from 'date-fns'; -import { useUpdateTask, useTaskErrors } from 'hooks/tasks'; -import { useProfile } from 'hooks/profile'; -import UploadIcon from 'components/icons/upload-icon'; -import Disclaimer from 'components/disclaimer'; -import Button from 'components/button'; -import { triggerCsvDownload } from 'utils/csv-download'; +import { useUpdateTask, useTaskErrors } from '@/hooks/tasks'; +import { useProfile } from '@/hooks/profile'; +import UploadIcon from '@/components/icons/upload-icon'; +import Disclaimer from '@/components/disclaimer'; +import Button from '@/components/button'; +import { triggerCsvDownload } from '@/utils/csv-download'; -import type { Task } from 'types'; -import type { DisclaimerProps } from 'components/disclaimer/component'; +import type { Task } from '@/types'; +import type { DisclaimerProps } from '@/components/disclaimer/component'; const VARIANT_STATUS: Record = { completed: 'success', @@ -50,16 +50,6 @@ const DataUploadError: React.FC = ({ task }) => { >
- {task?.status === 'processing' && ( - <> -

Upload in progress

-

- There is a uploading task in progress created at{' '} - {format(new Date(task.createdAt), 'MMM d, yyyy HH:mm z')}. -

- - )} - {task?.status === 'completed' && task?.errors.length === 0 && ( <>

Upload completed

diff --git a/client/src/containers/admin/data-uploader/component.tsx b/client/src/containers/admin/data-uploader/component.tsx deleted file mode 100644 index 23a2a8acf..000000000 --- a/client/src/containers/admin/data-uploader/component.tsx +++ /dev/null @@ -1,40 +0,0 @@ -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 type { Task } from 'types'; - -const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => { - const [isUploading, setIsUploading] = useState(false); - - return ( -
-
-
-

- 1. Download the Excel template and fill it with your data. -

- -
-
-

2. Upload the filled Excel file.

- - {!isUploading && task?.status === 'failed' && } -
-
-
- ); -}; - -export default AdminDataPage; 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/index.tsx b/client/src/containers/admin/data-uploader/index.tsx new file mode 100644 index 000000000..ac77d56e9 --- /dev/null +++ b/client/src/containers/admin/data-uploader/index.tsx @@ -0,0 +1,48 @@ +import { DownloadIcon } from '@heroicons/react/solid'; + +import DataUploadError from '@/containers/admin/data-upload-error'; +import { Anchor } from '@/components/button'; +import UploadTracker from '@/containers/uploader/tracker'; +import { useLasTask } from '@/hooks/tasks'; +import DataUploader from '@/containers/uploader'; + +const AdminDataUploader: React.FC = () => { + const { data: lastTask } = useLasTask(); + + return ( +
+
+
+

+ 1. Download the Excel template and fill it with your data. +

+ +
+
+

2. Upload the filled Excel file.

+ {lastTask?.status !== 'processing' && ( +
+ +
+ )} + {lastTask?.status === 'processing' && ( +
+ +
+ )} + {lastTask?.status === 'failed' && } +
+
+
+ ); +}; + +export default AdminDataUploader; diff --git a/client/src/containers/uploader/index.ts b/client/src/containers/uploader/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/uploader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/uploader/component.tsx b/client/src/containers/uploader/index.tsx similarity index 65% rename from client/src/containers/uploader/component.tsx rename to client/src/containers/uploader/index.tsx index 5a1ab080e..2949e886f 100644 --- a/client/src/containers/uploader/component.tsx +++ b/client/src/containers/uploader/index.tsx @@ -2,13 +2,14 @@ import { useCallback, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import classNames from 'classnames'; import { useRouter } from 'next/router'; +import { useQueryClient } from '@tanstack/react-query'; -import { useUploadDataSource } from 'hooks/sourcing-data'; -import { useLasTask } from 'hooks/tasks'; -import FileDropzone from 'components/file-dropzone'; +import { useUploadDataSource } from '@/hooks/sourcing-data'; +import { LAST_TASK_PARAMS, useLasTask } from '@/hooks/tasks'; import { env } from '@/env.mjs'; +import FileDropzone from '@/components/file-dropzone'; -import type { FileDropZoneProps } from 'components/file-dropzone/types'; +import type { FileDropZoneProps } from '@/components/file-dropzone/types'; import type { Task } from 'types'; type DataUploaderProps = { @@ -29,7 +30,7 @@ const DataUploader: React.FC = ({ variant = 'default', onUplo const router = useRouter(); const uploadDataSource = useUploadDataSource(); const lastTask = useLasTask(); - const { refetch: refetchLastTask } = lastTask; + const queryClient = useQueryClient(); const handleOnDrop: FileDropZoneProps['onDrop'] = useCallback( (acceptedFiles) => { @@ -40,8 +41,16 @@ const DataUploader: React.FC = ({ variant = 'default', onUplo }); uploadDataSource.mutate(formData, { - onSuccess: () => { - refetchLastTask(); + onSuccess: ({ data }) => { + const { attributes, ...rest } = data; + queryClient.setQueryData(['tasks', LAST_TASK_PARAMS], { + data: [ + { + ...rest, + ...attributes, + }, + ], + }); }, onError: ({ response }) => { const errors = response?.data?.errors; @@ -53,7 +62,7 @@ const DataUploader: React.FC = ({ variant = 'default', onUplo }, }); }, - [refetchLastTask, uploadDataSource], + [uploadDataSource, queryClient], ); const handleFileRejected: FileDropZoneProps['onDropRejected'] = useCallback((rejectedFiles) => { @@ -69,12 +78,9 @@ const DataUploader: React.FC = ({ variant = 'default', onUplo // Status of the uploading process const isUploading = uploadDataSource.isLoading; const isWaiting = uploadDataSource.isSuccess && currentTaskId === lastTask.data?.id; - const isProcessing = lastTask.data?.status === 'processing'; - const isWorking = isUploading || isWaiting || isProcessing; - const isCompleted = - !isWorking && - uploadDataSource.isSuccess && - (lastTask.data?.status === 'completed' || lastTask.data?.status === 'failed'); + const isTaskFailed = lastTask.data?.status === 'failed'; + const isWorking = (isUploading || isWaiting) && !isTaskFailed; + const isCompleted = !isWorking && uploadDataSource.isSuccess; useEffect(() => { if (!currentTaskId && lastTask.data?.id) { @@ -83,20 +89,15 @@ const DataUploader: React.FC = ({ variant = 'default', onUplo }, [currentTaskId, lastTask.data?.id]); useEffect(() => { - if (isCompleted && router.isReady) { - router.reload(); - onUploadInProgress(false); - } + if (isCompleted) onUploadInProgress?.(false); }, [isCompleted, onUploadInProgress, router]); useEffect(() => { - if (isWorking && onUploadInProgress) { - onUploadInProgress(true); - } + if (isWorking && onUploadInProgress) onUploadInProgress?.(true); }, [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..dec4ac08e --- /dev/null +++ b/client/src/containers/uploader/tracker/index.tsx @@ -0,0 +1,88 @@ +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 onComplete = useCallback(() => { + queryClient.invalidateQueries(['tasks']); + queryClient.invalidateQueries(['sourcingLocations']); + }, [queryClient]); + + const onFailure = useCallback(() => { + queryClient.invalidateQueries(['tasks']); + }, [queryClient]); + + useSocket({ + DATA_IMPORT_PROGRESS: onProgress, + DATA_IMPORT_COMPLETED: onComplete, + DATA_IMPORT_FAILURE: onFailure, + }); + + 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..ab0cc47bd --- /dev/null +++ b/client/src/hooks/socket/index.ts @@ -0,0 +1,57 @@ +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(() => { + if (!accessToken || socketRef.current) return () => {}; + + socketRef.current = io(env.NEXT_PUBLIC_API_URL, { + transports: ['websocket'], + auth: { + token: accessToken, + }, + ...options, + }); + }, [accessToken, options]); + + useEffect(() => { + const socket = socketRef.current; + + if (!socket) return () => {}; + + socket.connect(); + + return () => { + socket.disconnect(); + }; + }, []); + + 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/hooks/sourcing-data/index.ts b/client/src/hooks/sourcing-data/index.ts index 29fd901aa..3bfb76e65 100644 --- a/client/src/hooks/sourcing-data/index.ts +++ b/client/src/hooks/sourcing-data/index.ts @@ -1,15 +1,18 @@ import { useMutation } from '@tanstack/react-query'; import { apiRawService } from 'services/api'; +import { Task } from '@/types'; -import type { UseMutationResult } from '@tanstack/react-query'; - -type ApiResponse = { data: { id: string } }; - -export function useUploadDataSource(): UseMutationResult { - const importDataSource = (data) => +export function useUploadDataSource() { + const importDataSource = (data: FormData) => apiRawService - .request({ + .request<{ + data: { + id: Task['id']; + type: Task['type']; + attributes: Omit; + }; + }>({ method: 'POST', data, url: 'import/sourcing-data', @@ -17,7 +20,7 @@ export function useUploadDataSource(): UseMutationResult { }) .then((response) => response.data); - return useMutation(importDataSource, { + return useMutation(importDataSource, { mutationKey: ['importSourcingData'], }); } diff --git a/client/src/hooks/tasks/index.ts b/client/src/hooks/tasks/index.ts index 30cb7cd32..ce9bed50f 100644 --- a/client/src/hooks/tasks/index.ts +++ b/client/src/hooks/tasks/index.ts @@ -21,6 +21,13 @@ const DEFAULT_QUERY_OPTIONS = { refetchOnReconnect: false, }; +export const LAST_TASK_PARAMS = { + 'page[size]': 1, + 'page[number]': 1, + sort: '-createdAt', + include: 'user', +}; + export const useTasks = ( params: Record = {}, options: UseQueryOptions = {}, @@ -45,18 +52,10 @@ export const useTasks = ( }; export const useLasTask = () => { - const tasks = useTasks( - { - 'page[size]': 1, - 'page[number]': 1, - sort: '-createdAt', - include: 'user', - }, - { - refetchInterval: 20000, - refetchOnReconnect: true, - }, - ); + const tasks = useTasks(LAST_TASK_PARAMS, { + refetchInterval: 20000, + refetchOnReconnect: true, + }); return { ...tasks, data: tasks?.data?.data?.[0] } as UseQueryResult; }; diff --git a/client/src/pages/data/index.tsx b/client/src/pages/data/index.tsx index c6e69d30c..019aaa391 100644 --- a/client/src/pages/data/index.tsx +++ b/client/src/pages/data/index.tsx @@ -1,26 +1,27 @@ import { useMemo } from 'react'; import Head from 'next/head'; -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 { useSourcingLocations } from '@/hooks/sourcing-locations'; +import AdminLayout from '@/layouts/admin'; +import AdminDataUploader from '@/containers/admin/data-uploader'; +import AdminDataTable from '@/containers/admin/data-table'; +import Search from '@/components/search'; +import { useLasTask } from '@/hooks/tasks'; 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, }); - // Getting last task to check if there is a processing task - const { data: lastTask } = useLasTask(); + const { data: lastTask, isFetched: lastTaskIsFetched } = 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) && ( -
- -
- )} - - {/* Content when empty, or upload is processing or failed */} - {isFetched && !thereIsData && } + {thereIsData && lastTask?.status !== 'processing' && } - {/* Content when data and upload is completed */} - {isFetched && thereIsData && } + {(['processing', 'failed'].includes(lastTask?.status) || + (!lastTask && lastTaskIsFetched)) && }
); }; diff --git a/client/src/types.d.ts b/client/src/types.d.ts index eb06176e4..7cb1b9386 100644 --- a/client/src/types.d.ts +++ b/client/src/types.d.ts @@ -361,13 +361,14 @@ export type MakePropOptional = Omit & Partial[]; user?: User; data?: Record; createdAt: string; dismissedBy: string; + logs?: Record[]; }; // User diff --git a/client/yarn.lock b/client/yarn.lock index 2a6a7a618..3f7f9f1ab 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2354,6 +2354,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: 10/93792eafb63ad15259ba00885c3cf4fdc01d969b1db10a273ccac70bed2373b5170cbc94682372d666a44e4ad8faeb176fb6cbaaeeb66c87231e2ff3d72583f9 + languageName: node + linkType: hard + "@streamparser/json@npm:^0.0.12": version: 0.0.12 resolution: "@streamparser/json@npm:0.0.12" @@ -4495,7 +4502,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: @@ -4834,6 +4841,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": "npm:~3.1.0" + debug: "npm:~4.3.1" + engine.io-parser: "npm:~5.2.1" + ws: "npm:~8.11.0" + xmlhttprequest-ssl: "npm:~2.0.0" + checksum: 10/0d7c3e6de23f37706c163bc8a0e90e70e613c7768be0705bda3675124d5e24d849810fddda005f8dcc721da35aee713976a03a0465d71f0856adfc1af7a80e5d + languageName: node + linkType: hard + +"engine.io-parser@npm:~5.2.1": + version: 5.2.2 + resolution: "engine.io-parser@npm:5.2.2" + checksum: 10/135b1278547bde501412ac462e93b3b4f6a2fecc30a2b843bb9408b96301e8068bb2496c32d124a3d2544eb0aec8b8eddcb4ef0d0d0b84b7d642b1ffde1b2dcf + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.10.0": version: 5.10.0 resolution: "enhanced-resolve@npm:5.10.0" @@ -7414,6 +7441,7 @@ __metadata: recharts: "npm:2.9.0" rooks: "npm:7.14.1" sharp: "npm:0.32.6" + socket.io-client: "npm:4.7.5" start-server-and-test: "npm:1.14.0" tailwind-merge: "npm:2.2.1" tailwindcss: "npm:3.4.1" @@ -10113,6 +10141,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": "npm:~3.1.0" + debug: "npm:~4.3.2" + engine.io-client: "npm:~6.5.2" + socket.io-parser: "npm:~4.2.4" + checksum: 10/a9e118081dc1669a63af3abd9defce94f85c8ed8d9146cd7a77665b5f1f78baf0b9f4155cf0fce7770856f97493416551abcba686f02778045f4768ceaafed5c + 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": "npm:~3.1.0" + debug: "npm:~4.3.1" + checksum: 10/4be500a9ff7e79c50ec25af11048a3ed34b4c003a9500d656786a1e5bceae68421a8394cf3eb0aa9041f85f36c1a9a737617f4aee91a42ab4ce16ffb2aa0c89c + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -11427,6 +11477,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: 10/f759ea19e42f6d94727b3d8590693f2d92521a78ec2de5c6064c3356f50d4815d427b7ddb10bf39596cc67d3b18232a1b2dfbc3b6361d4772bdfec69d4c130f4 + languageName: node + linkType: hard + +"xmlhttprequest-ssl@npm:~2.0.0": + version: 2.0.0 + resolution: "xmlhttprequest-ssl@npm:2.0.0" + checksum: 10/3c2edfce0c49c7a494ed16c87e6897c9e3eba29763a5505526de83ddefd195d224fa5cdf41092298c99cd6ee473c9f259a0679f6ff3b8a9535dcd09900db91f9 + languageName: node + linkType: hard + "y18n@npm:^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3"