Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
agnlez committed Apr 22, 2024
1 parent 7453d3a commit f51c511
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 38 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions client/src/containers/admin/data-upload-error/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ const DataUploadError: React.FC<DataUploadErrorProps> = ({ task }) => {
>
<div className="flex w-full items-center space-x-6">
<div className="flex-1 space-y-1.5">
{task?.status === 'processing' && (
{/* {task?.status === 'processing' && (
<>
<h3>Upload in progress</h3>
<p className="text-gray-500">
There is a uploading task in progress created at{' '}
{format(new Date(task.createdAt), 'MMM d, yyyy HH:mm z')}.
</p>
</>
)}
)} */}

{task?.status === 'completed' && task?.errors.length === 0 && (
<>
Expand Down
1 change: 0 additions & 1 deletion client/src/containers/admin/data-uploader/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-full w-full items-center justify-center">
Expand All @@ -27,14 +26,25 @@ const AdminDataPage: React.FC<{ task: Task }> = ({ task }) => {
Download template
</Anchor>
</div>
<div className="w-[640px] space-y-4 text-center text-lg">
<div className="flex flex-col items-center space-y-4 text-center text-lg">
<p className="font-semibold">2. Upload the filled Excel file.</p>
<DataUploader onUploadInProgress={setIsUploading} />
{!isUploading && task?.status === 'failed' && <DataUploadError task={task} />}
{lastTask?.status !== 'processing' && (
<div className="w-[640px]">
<DataUploader />
</div>
)}

{lastTask?.status === 'processing' && (
<div className="w-[880px]">
<UploadTracker />
</div>
)}

{lastTask?.status === 'failed' && <DataUploadError task={lastTask} />}
</div>
</div>
</div>
);
};

export default AdminDataPage;
export default AdminDataUploader;
15 changes: 1 addition & 14 deletions client/src/containers/uploader/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const DataUploader: React.FC<DataUploaderProps> = ({ variant = 'default', onUplo
}, [isWorking, onUploadInProgress]);

return (
<div className="relative w-full min-w-[640px]">
<div className="relative w-full">
<div
className={classNames('relative z-10 rounded-xl bg-white', {
'p-4 shadow-lg': variant === 'default',
Expand All @@ -110,19 +110,6 @@ const DataUploader: React.FC<DataUploaderProps> = ({ variant = 'default', onUplo
isUploading={isWorking}
/>
</div>

{isWorking && (
<div className="w-full px-20">
<div className="rounded-b-xl bg-white px-10 py-4">
<div className="h-[4px] w-full rounded bg-gradient-to-r from-[#5FCFF9] via-[#42A56A] to-[#F5CA7D]" />
<p className="mt-1 text-left text-xs text-gray-500">
{isUploading && 'Uploading file...'}
{isWaiting && 'File uploaded successfully! Starting to process the data...'}
{isProcessing && 'Processing file...'}
</p>
</div>
</div>
)}
</div>
);
};
Expand Down
86 changes: 86 additions & 0 deletions client/src/containers/uploader/tracker/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ProgressTask['data']>(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 (
<div className="w-full rounded-3xl bg-white px-8 py-10">
<div className="grid grid-cols-4 gap-1">
{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) => (
<div className="flex flex-col items-start" key={key}>
<span
className={cn('text-base text-gray-400 transition-colors', {
'text-gray-900': ['processing', 'completed'].includes(
tasksProgress?.[key]?.status,
),
})}
>
{STEPS_NAMES[key]}
</span>
<span className="text-sm text-gray-500">{`Progress: ${
tasksProgress?.[key]?.progress
? formatPercentage(tasksProgress[key].progress / 100)
: '–'
}`}</span>
</div>
))}
</div>
</div>
);
};

export default UploadTracker;
51 changes: 51 additions & 0 deletions client/src/hooks/socket/index.ts
Original file line number Diff line number Diff line change
@@ -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<Socket | undefined>(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;
41 changes: 30 additions & 11 deletions client/src/pages/data/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AdminLayout
Expand All @@ -39,17 +59,16 @@ const AdminDataPage: React.FC = () => {
<title>Manage data | Landgriffon</title>
</Head>

{(!isFetched || isLoading) && (
<div className="flex h-full w-full items-center justify-center">
<Loading className="h-5 w-5 text-navy-400" />
</div>
)}
{/* // ! THIS IS FOR TESTING.REMOVE */}
<button type="button" onClick={triggerWipe} className="mb-8">
Trigger wipe data
</button>

{/* Content when empty, or upload is processing or failed */}
{isFetched && !thereIsData && <AdminDataUploader task={lastTask} />}
{!thereIsData && <AdminDataUploader />}

{/* Content when data and upload is completed */}
{isFetched && thereIsData && <AdminDataTable task={lastTask} />}
{thereIsData && <AdminDataTable task={lastTask} />}
</AdminLayout>
);
};
Expand Down
Loading

0 comments on commit f51c511

Please sign in to comment.