diff --git a/package-lock.json b/package-lock.json index c63a556a..c9c0b47b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-label": "^2.1.3", "@radix-ui/react-select": "^2.2.2", - "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.2.0", "@reown/appkit": "^1.6.9", @@ -26,6 +26,8 @@ "@tanstack/react-query": "^5.74.4", "@tanstack/react-router": "^1.116.0", "@tanstack/react-router-devtools": "^1.116.0", + "@tanstack/react-table": "^8.21.3", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "graphql": "^16.10.0", @@ -3211,6 +3213,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -3277,6 +3297,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -3503,6 +3541,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz", @@ -3757,6 +3813,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", @@ -3800,9 +3874,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3881,6 +3955,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -4954,6 +5046,26 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/router-core": { "version": "1.117.1", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.117.1.tgz", @@ -5115,6 +5227,19 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.115.0", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.115.0.tgz", diff --git a/package.json b/package.json index 5bb4b03a..5c24e259 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-label": "^2.1.3", "@radix-ui/react-select": "^2.2.2", - "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.2.0", "@reown/appkit": "^1.6.9", @@ -34,6 +34,8 @@ "@tanstack/react-query": "^5.74.4", "@tanstack/react-router": "^1.116.0", "@tanstack/react-router-devtools": "^1.116.0", + "@tanstack/react-table": "^8.21.3", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "graphql": "^16.10.0", diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index 6990ab0e..c23cc812 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -39,7 +39,11 @@ const CopyButton = ({ + )} + + {isConnected && ( + + )} + + ); +} diff --git a/src/modules/apps/AppsPreviewTable.tsx b/src/modules/apps/AppsPreviewTable.tsx index 0f363b9b..0a6458bf 100644 --- a/src/modules/apps/AppsPreviewTable.tsx +++ b/src/modules/apps/AppsPreviewTable.tsx @@ -2,22 +2,14 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config'; import { execute } from '@/graphql/execute'; import { cn } from '@/lib/utils'; import { useQuery } from '@tanstack/react-query'; +import { Link } from '@tanstack/react-router'; import { Box, LoaderCircle, Terminal } from 'lucide-react'; import { CircularLoader } from '@/components/CircularLoader'; -import CopyButton from '@/components/CopyButton'; +import { DataTable } from '@/components/DataTable'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { formatElapsedTime } from '@/utils/formatElapsedTime'; -import { truncateAddress } from '@/utils/truncateAddress'; import { appsQuery } from './appsQuery'; +import { columns } from './appsTable/columns'; export function AppsPreviewTable({ className }: { className?: string }) { const apps = useQuery({ @@ -27,6 +19,12 @@ export function AppsPreviewTable({ className }: { className?: string }) { refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL, }); + const formattedData = + apps.data?.apps.map((app) => ({ + ...app, + destination: `/app/${app.address}`, + })) ?? []; + return (
@@ -38,92 +36,28 @@ export function AppsPreviewTable({ className }: { className?: string }) { (outdated) )} - {apps.isFetching && !apps.isPending && ( - - )} + {apps.isFetching && } -
- - - - Address - Name - Owner - TxHash - Time - - - - {apps.isPending || - (apps.isError && !apps.data) || - !apps.data?.apps.length ? ( - - - {apps.isPending ? ( - - ) : apps.isError ? ( - - - Error - - A error occurred during apps loading. - - - ) : ( -

No apps to display.

- )} -
-
- ) : ( - apps.data.apps.map((app) => ( - - - - - - - - - - - - - - - {formatElapsedTime(app.timestamp)} - - - )) - )} -
-
+ {(apps.isError || apps.errorUpdateCount > 0) && !apps.data ? ( + + + Error + + A error occurred during apps loading. + + + ) : ( + + )}
); } diff --git a/src/modules/apps/appsTable/columns.tsx b/src/modules/apps/appsTable/columns.tsx new file mode 100644 index 00000000..1043b66b --- /dev/null +++ b/src/modules/apps/appsTable/columns.tsx @@ -0,0 +1,75 @@ +import { AppsQuery } from '@/graphql/graphql'; +import { ColumnDef } from '@tanstack/react-table'; +import CopyButton from '@/components/CopyButton'; +import { formatElapsedTime } from '@/utils/formatElapsedTime'; +import { truncateAddress } from '@/utils/truncateAddress'; + +type App = AppsQuery['apps'][number]; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'address', + header: 'Address', + cell: ({ row }) => { + const appAddress = row.getValue('address'); + return ( + + ); + }, + }, + { + accessorKey: 'name', + header: 'Name', + cell: ({ row }) => { + const name = row.getValue('name'); + return ( +
+ +
+ ); + }, + }, + { + accessorKey: 'owner.address', + header: 'Owner', + cell: ({ row }) => { + const ownerAddress = row.original.owner.address; + return ( + + ); + }, + }, + { + accessorKey: 'tx_hash', + header: 'TxHash', + cell: ({ row }) => { + const txHash = row.original.transfers[0].transaction.txHash; + return ( + + ); + }, + }, + { + accessorKey: 'time', + header: 'Time', + cell: ({ row }) => { + const timestamp = row.original.timestamp; + return
{formatElapsedTime(timestamp)}
; + }, + }, +]; diff --git a/src/modules/apps/nextAppsQuery.ts b/src/modules/apps/nextAppsQuery.ts new file mode 100644 index 00000000..f107cb01 --- /dev/null +++ b/src/modules/apps/nextAppsQuery.ts @@ -0,0 +1,14 @@ +import { graphql } from '@/graphql/gql'; + +export const nextAppsQuery = graphql(` + query NextApps($length: Int = 20, $skip: Int = 0) { + apps( + first: $length + skip: $skip + orderBy: timestamp + orderDirection: desc + ) { + address: id + } + } +`); diff --git a/src/modules/datasets/DatasetsPreviewTable.tsx b/src/modules/datasets/DatasetsPreviewTable.tsx index 6051ebb1..2dda14a9 100644 --- a/src/modules/datasets/DatasetsPreviewTable.tsx +++ b/src/modules/datasets/DatasetsPreviewTable.tsx @@ -2,22 +2,14 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config'; import { execute } from '@/graphql/execute'; import { cn } from '@/lib/utils'; import { useQuery } from '@tanstack/react-query'; +import { Link } from '@tanstack/react-router'; import { Box, LoaderCircle, Terminal } from 'lucide-react'; import { CircularLoader } from '@/components/CircularLoader'; -import CopyButton from '@/components/CopyButton'; +import { DataTable } from '@/components/DataTable'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { formatElapsedTime } from '@/utils/formatElapsedTime'; -import { truncateAddress } from '@/utils/truncateAddress'; import { datasetsQuery } from './datasetsQuery'; +import { columns } from './datasetsTable/columns'; export function DatasetsPreviewTable({ className }: { className?: string }) { const datasets = useQuery({ @@ -27,6 +19,12 @@ export function DatasetsPreviewTable({ className }: { className?: string }) { refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL, }); + const formattedData = + datasets.data?.datasets.map((dataset) => ({ + ...dataset, + destination: `/dataset/${dataset.address}`, + })) ?? []; + return (
@@ -38,95 +36,28 @@ export function DatasetsPreviewTable({ className }: { className?: string }) { (outdated) )} - {datasets.isFetching && !datasets.isPending && ( - - )} + {datasets.isFetching && } -
- - - - Address - Name - Owner - TxHash - Time - - - - {datasets.isPending || - (datasets.isError && !datasets.data) || - !datasets.data?.datasets.length ? ( - - - {datasets.isPending ? ( - - ) : datasets.isError ? ( - - - Error - - A error occurred during datasets loading. - - - ) : ( -

No datasets to display.

- )} -
-
- ) : ( - datasets.data.datasets.map((dataset) => ( - - - - - - - - - - - - - - - {formatElapsedTime(dataset.timestamp)} - - - )) - )} -
-
+ {(datasets.isError || datasets.errorUpdateCount > 0) && !datasets.data ? ( + + + Error + + A error occurred during datasets loading. + + + ) : ( + + )}
); } diff --git a/src/modules/datasets/datasetsTable/columns.tsx b/src/modules/datasets/datasetsTable/columns.tsx new file mode 100644 index 00000000..8c267059 --- /dev/null +++ b/src/modules/datasets/datasetsTable/columns.tsx @@ -0,0 +1,77 @@ +import { DatasetsQuery } from '@/graphql/graphql'; +import { ColumnDef } from '@tanstack/react-table'; +import CopyButton from '@/components/CopyButton'; +import { formatElapsedTime } from '@/utils/formatElapsedTime'; +import { truncateAddress } from '@/utils/truncateAddress'; + +type Dataset = DatasetsQuery['datasets'][number]; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'datasetAddress', + header: 'Dataset', + cell: ({ row }) => { + const datasetAddress = row.original.address; + return ( + + ); + }, + }, + { + accessorKey: 'datasetName', + header: 'Name', + cell: ({ row }) => { + const datasetName = row.original.name; + return datasetName ? ( +
+ {datasetName} +
+ ) : ( + No dataset name + ); + }, + }, + { + accessorKey: 'owner.address', + header: 'Owner', + cell: ({ row }) => { + const ownerAddress = row.original.owner.address; + return ( + + ); + }, + }, + { + accessorKey: 'tx_hash', + header: 'TxHash', + cell: ({ row }) => { + const txHash = row.original.transfers[0].transaction.txHash; + return ( + + ); + }, + }, + { + accessorKey: 'time', + header: 'Time', + cell: ({ row }) => { + const timestamp = row.original.timestamp; + return
{formatElapsedTime(timestamp)}
; + }, + }, +]; diff --git a/src/modules/datasets/nextDatasetsQuery.ts b/src/modules/datasets/nextDatasetsQuery.ts new file mode 100644 index 00000000..3ad886f5 --- /dev/null +++ b/src/modules/datasets/nextDatasetsQuery.ts @@ -0,0 +1,14 @@ +import { graphql } from '@/graphql/gql'; + +export const nextDatasetsQuery = graphql(` + query NextDatasets($length: Int = 20, $skip: Int = 0) { + datasets( + first: $length + skip: $skip + orderBy: timestamp + orderDirection: desc + ) { + address: id + } + } +`); diff --git a/src/modules/deals/DealsPreviewTable.tsx b/src/modules/deals/DealsPreviewTable.tsx index 4f951d10..a4d778fa 100644 --- a/src/modules/deals/DealsPreviewTable.tsx +++ b/src/modules/deals/DealsPreviewTable.tsx @@ -2,23 +2,14 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config'; import { execute } from '@/graphql/execute'; import { cn } from '@/lib/utils'; import { useQuery } from '@tanstack/react-query'; +import { Link } from '@tanstack/react-router'; import { Box, LoaderCircle, Terminal } from 'lucide-react'; import { CircularLoader } from '@/components/CircularLoader'; -import CopyButton from '@/components/CopyButton'; +import { DataTable } from '@/components/DataTable'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { formatElapsedTime } from '@/utils/formatElapsedTime'; -import { truncateAddress } from '@/utils/truncateAddress'; -import { SuccessCell } from './SuccessCell'; import { dealsQuery } from './dealsQuery'; +import { columns } from './dealsTable/columns'; export function DealsPreviewTable({ className }: { className?: string }) { const deals = useQuery({ @@ -28,6 +19,12 @@ export function DealsPreviewTable({ className }: { className?: string }) { refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL, }); + const formattedData = + deals.data?.deals.map((deal) => ({ + ...deal, + destination: `/deal/${deal.dealid}`, + })) ?? []; + return (
@@ -39,116 +36,28 @@ export function DealsPreviewTable({ className }: { className?: string }) { (outdated) )} - {deals.isFetching && !deals.isPending && ( - - )} + {deals.isFetching && } -
- - - - Deal - App - Time - Workerpool - Dataset - Price - Success - - - - {deals.isPending || - (deals.isError && !deals.data) || - !deals.data?.deals.length ? ( - - - {deals.isPending ? ( - - ) : deals.isError ? ( - - - Error - - A error occurred during deals loading. - - - ) : ( -

No deals to display.

- )} -
-
- ) : ( - deals.data.deals.map((deal) => ( - - - - - - - - - {formatElapsedTime(deal.timestamp)} - - - - - - {deal.dataset?.address ? ( - - ) : ( - - No dataset address - - )} - - - {( - Number(deal.appPrice) + - Number(deal.datasetPrice) + - Number(deal.workerpoolPrice) - ).toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 10, - })}{' '} - xRLC - - - - - - )) - )} -
-
+ {(deals.isError || deals.errorUpdateCount > 0) && !deals.data ? ( + + + Error + + A error occurred during deals loading. + + + ) : ( + + )}
); } diff --git a/src/modules/deals/dealsTable/columns.tsx b/src/modules/deals/dealsTable/columns.tsx new file mode 100644 index 00000000..6f1d7e5d --- /dev/null +++ b/src/modules/deals/dealsTable/columns.tsx @@ -0,0 +1,108 @@ +import { DealsQuery } from '@/graphql/graphql'; +import { ColumnDef } from '@tanstack/react-table'; +import CopyButton from '@/components/CopyButton'; +import { formatElapsedTime } from '@/utils/formatElapsedTime'; +import { truncateAddress } from '@/utils/truncateAddress'; +import { SuccessCell } from '../SuccessCell'; + +type Deal = DealsQuery['deals'][number]; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'dealid', + header: 'Deal', + cell: ({ row }) => { + const dealAddress = row.getValue('dealid'); + return ( + + ); + }, + }, + { + accessorKey: 'app', + header: 'App', + cell: ({ row }) => { + const appAddress = row.original.app.address; + return ( +
+ +
+ ); + }, + }, + { + accessorKey: 'time', + header: 'Time', + cell: ({ row }) => { + const timestamp = row.original.timestamp; + return
{formatElapsedTime(timestamp)}
; + }, + }, + { + accessorKey: 'workerpool.address', + header: 'Workerpool', + cell: ({ row }) => { + const workerpoolAddress = row.original.workerpool.address; + return ( + + ); + }, + }, + { + accessorKey: 'dataset.address', + header: 'Dataset', + cell: ({ row }) => { + const datasetAddress = row.original.dataset?.address; + if (!datasetAddress) { + return ( + No dataset address + ); + } + return ( + + ); + }, + }, + { + accessorKey: 'price', + header: 'Price', + cell: ({ row }) => { + const price = ( + Number(row.original.appPrice) + + Number(row.original.datasetPrice) + + Number(row.original.workerpoolPrice) + ).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 10, + }); + return {price} xRLC; + }, + }, + { + accessorKey: 'success', + header: 'Success', + cell: ({ row }) => { + return ; + }, + }, +]; diff --git a/src/modules/deals/nextDealsQuery.ts b/src/modules/deals/nextDealsQuery.ts new file mode 100644 index 00000000..68e03e4b --- /dev/null +++ b/src/modules/deals/nextDealsQuery.ts @@ -0,0 +1,14 @@ +import { graphql } from '@/graphql/gql'; + +export const nextDealsQuery = graphql(` + query NextDeals($length: Int = 20, $skip: Int = 0) { + deals( + first: $length + skip: $skip + orderBy: timestamp + orderDirection: desc + ) { + dealid: id + } + } +`); diff --git a/src/modules/tasks/TasksPreviewTable.tsx b/src/modules/tasks/TasksPreviewTable.tsx index 6cfe0a8d..5086e16b 100644 --- a/src/modules/tasks/TasksPreviewTable.tsx +++ b/src/modules/tasks/TasksPreviewTable.tsx @@ -2,31 +2,29 @@ import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config'; import { execute } from '@/graphql/execute'; import { cn } from '@/lib/utils'; import { useQuery } from '@tanstack/react-query'; +import { Link } from '@tanstack/react-router'; import { Box, LoaderCircle, Terminal } from 'lucide-react'; import { CircularLoader } from '@/components/CircularLoader'; -import CopyButton from '@/components/CopyButton'; +import { DataTable } from '@/components/DataTable'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { truncateAddress } from '@/utils/truncateAddress'; -import StatusCell from './StatusCell'; -import { taskQuery } from './tasksQuery'; +import { tasksQuery } from './tasksQuery'; +import { columns } from './tasksTable/columns'; export function TasksPreviewTable({ className }: { className?: string }) { const tasks = useQuery({ queryKey: ['tasks_preview'], queryFn: () => - execute(taskQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }), + execute(tasksQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }), refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL, }); + const formattedData = + tasks.data?.tasks.map((task) => ({ + ...task, + destination: `/task/${task.taskid}`, + })) ?? []; + return (
@@ -38,80 +36,28 @@ export function TasksPreviewTable({ className }: { className?: string }) { (outdated) )} - {tasks.isFetching && !tasks.isPending && ( - - )} + {tasks.isFetching && } -
- - - - Task - Deadline - Status - - - - {tasks.isPending || - (tasks.isError && !tasks.data) || - !tasks.data?.tasks.length ? ( - - - {tasks.isPending ? ( - - ) : tasks.isError ? ( - - - Error - - A error occurred during tasks loading. - - - ) : ( -

No tasks to display.

- )} -
-
- ) : ( - tasks.data.tasks.map((task) => ( - - - - - - {new Intl.DateTimeFormat('en-US', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }).format(new Date(task.finalDeadline * 1000))} - - - - - - )) - )} -
-
+ {(tasks.isError || tasks.errorUpdateCount > 0) && !tasks.data ? ( + + + Error + + A error occurred during tasks loading. + + + ) : ( + + )}
); } diff --git a/src/modules/tasks/nextTasksQuery.ts b/src/modules/tasks/nextTasksQuery.ts new file mode 100644 index 00000000..c7cceebe --- /dev/null +++ b/src/modules/tasks/nextTasksQuery.ts @@ -0,0 +1,14 @@ +import { graphql } from '@/graphql/gql'; + +export const nextTasksQuery = graphql(` + query NextTasks($length: Int = 20, $skip: Int = 0) { + tasks( + first: $length + skip: $skip + orderBy: timestamp + orderDirection: desc + ) { + taskid: id + } + } +`); diff --git a/src/modules/tasks/tasksQuery.ts b/src/modules/tasks/tasksQuery.ts index 9e5275a6..39f9242d 100644 --- a/src/modules/tasks/tasksQuery.ts +++ b/src/modules/tasks/tasksQuery.ts @@ -1,6 +1,6 @@ import { graphql } from '@/graphql/gql'; -export const taskQuery = graphql(` +export const tasksQuery = graphql(` query Tasks($length: Int = 20, $skip: Int = 0) { tasks( first: $length diff --git a/src/modules/tasks/tasksTable/columns.tsx b/src/modules/tasks/tasksTable/columns.tsx new file mode 100644 index 00000000..6db6d2fd --- /dev/null +++ b/src/modules/tasks/tasksTable/columns.tsx @@ -0,0 +1,53 @@ +import { TasksQuery } from '@/graphql/graphql'; +import { ColumnDef } from '@tanstack/react-table'; +import CopyButton from '@/components/CopyButton'; +import { truncateAddress } from '@/utils/truncateAddress'; +import StatusCell from '../StatusCell'; + +type Task = TasksQuery['tasks'][number]; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'taskid', + header: 'Task', + cell: ({ row }) => { + const taskAddress = row.getValue('taskid'); + return ( + + ); + }, + }, + { + accessorKey: 'deadline', + header: 'Deadline', + cell: ({ row }) => { + const deadline = new Intl.DateTimeFormat('en-US', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(row.original.finalDeadline * 1000)); + return deadline; + }, + }, + { + accessorKey: 'workerpool.address', + header: 'Status', + cell: ({ row }) => { + const status = row.original.status; + const finalDeadlineTimestamp = row.original.finalDeadline * 1000; + return ( + + ); + }, + }, +]; diff --git a/src/modules/workerpools/WorkerpoolsPreviewTable.tsx b/src/modules/workerpools/WorkerpoolsPreviewTable.tsx new file mode 100644 index 00000000..eac85811 --- /dev/null +++ b/src/modules/workerpools/WorkerpoolsPreviewTable.tsx @@ -0,0 +1,64 @@ +import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config'; +import { execute } from '@/graphql/execute'; +import { cn } from '@/lib/utils'; +import { useQuery } from '@tanstack/react-query'; +import { Link } from '@tanstack/react-router'; +import { Box, LoaderCircle, Terminal } from 'lucide-react'; +import { CircularLoader } from '@/components/CircularLoader'; +import { DataTable } from '@/components/DataTable'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { workerpoolsQuery } from './workerpoolsQuery'; +import { columns } from './workerpoolsTable/columns'; + +export function WorkerpoolsPreviewTable({ className }: { className?: string }) { + const workerpools = useQuery({ + queryKey: ['workerpools_preview'], + queryFn: () => + execute(workerpoolsQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }), + refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL, + }); + + const formattedData = + workerpools.data?.workerpools.map((workerpool) => ({ + ...workerpool, + destination: `/workerpool/${workerpool.address}`, + })) ?? []; + + return ( +
+
+

+ + Latest workerpools deployed + {workerpools.data && workerpools.isError && ( + + (outdated) + + )} + {workerpools.isFetching && } +

+ +
+ {(workerpools.isError || workerpools.errorUpdateCount > 0) && + !workerpools.data ? ( + + + Error + + A error occurred during workerpools loading. + + + ) : ( + + )} +
+ ); +} diff --git a/src/modules/workerpools/nextWorkerpoolsQuery.ts b/src/modules/workerpools/nextWorkerpoolsQuery.ts new file mode 100644 index 00000000..68501a73 --- /dev/null +++ b/src/modules/workerpools/nextWorkerpoolsQuery.ts @@ -0,0 +1,14 @@ +import { graphql } from '@/graphql/gql'; + +export const nextWorkerpoolsQuery = graphql(` + query NextWorkerpools($length: Int = 20, $skip: Int = 0) { + workerpools( + first: $length + skip: $skip + orderBy: timestamp + orderDirection: desc + ) { + address: id + } + } +`); diff --git a/src/modules/workerpools/workerpoolsPreviewTable.tsx b/src/modules/workerpools/workerpoolsPreviewTable.tsx deleted file mode 100644 index 7bc069cc..00000000 --- a/src/modules/workerpools/workerpoolsPreviewTable.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config'; -import { execute } from '@/graphql/execute'; -import { cn } from '@/lib/utils'; -import { useQuery } from '@tanstack/react-query'; -import { Box, LoaderCircle, Terminal } from 'lucide-react'; -import { CircularLoader } from '@/components/CircularLoader'; -import CopyButton from '@/components/CopyButton'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { formatElapsedTime } from '@/utils/formatElapsedTime'; -import { truncateAddress } from '@/utils/truncateAddress'; -import { workerpoolsQuery } from './workerpoolsQuery'; - -export function WorkerpoolsPreviewTable({ className }: { className?: string }) { - const workerpools = useQuery({ - queryKey: ['workerpools_preview'], - queryFn: () => - execute(workerpoolsQuery, { length: PREVIEW_TABLE_LENGTH, skip: 0 }), - refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL, - }); - - return ( -
-
-

- - Latest workerpools deployed - {workerpools.data && workerpools.isError && ( - - (outdated) - - )} - {workerpools.isFetching && !workerpools.isPending && ( - - )} -

- -
- - - - Address - Description - Owner - TxHash - Time - - - - {workerpools.isPending || - (workerpools.isError && !workerpools.data) || - !workerpools.data?.workerpools.length ? ( - - - {workerpools.isPending ? ( - - ) : workerpools.isError ? ( - - - Error - - A error occurred during workerpools loading. - - - ) : ( -

No workerpools to display.

- )} -
-
- ) : ( - workerpools.data.workerpools.map((workerpool) => ( - - - - - - - - - - - - - - - {formatElapsedTime(workerpool.timestamp)} - - - )) - )} -
-
-
- ); -} diff --git a/src/modules/workerpools/workerpoolsTable/columns.tsx b/src/modules/workerpools/workerpoolsTable/columns.tsx new file mode 100644 index 00000000..896299d6 --- /dev/null +++ b/src/modules/workerpools/workerpoolsTable/columns.tsx @@ -0,0 +1,75 @@ +import { WorkerpoolsQuery } from '@/graphql/graphql'; +import { ColumnDef } from '@tanstack/react-table'; +import CopyButton from '@/components/CopyButton'; +import { formatElapsedTime } from '@/utils/formatElapsedTime'; +import { truncateAddress } from '@/utils/truncateAddress'; + +type Workerpool = WorkerpoolsQuery['workerpools'][number]; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'address', + header: 'Workerpool', + cell: ({ row }) => { + const workerpoolAddress = row.original.address; + return ( + + ); + }, + }, + { + accessorKey: 'description', + header: 'Description', + cell: ({ row }) => { + const workerpoolDescription = row.original.description; + return ( +
+ {workerpoolDescription} +
+ ); + }, + }, + { + accessorKey: 'owner', + header: 'Owner', + cell: ({ row }) => { + const owner = row.original.owner.address; + return ( + + ); + }, + }, + { + accessorKey: 'txHash', + header: 'TxHash', + cell: ({ row }) => { + const txHash = row.original.transfers[0].transaction.txHash; + return ( + + ); + }, + }, + { + accessorKey: 'dataset.address', + header: 'Dataset', + cell: ({ row }) => { + const timestamp = row.original.timestamp; + return
{formatElapsedTime(timestamp)}
; + }, + }, +]; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index fd01c2cb..82452268 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,66 +10,168 @@ // Import Routes -import { Route as rootRoute } from "./routes/__root"; -import { Route as IndexImport } from "./routes/index"; +import { Route as rootRoute } from './routes/__root' +import { Route as WorkerpoolsImport } from './routes/workerpools' +import { Route as TasksImport } from './routes/tasks' +import { Route as DealsImport } from './routes/deals' +import { Route as DatasetsImport } from './routes/datasets' +import { Route as AppsImport } from './routes/apps' +import { Route as IndexImport } from './routes/index' // Create/Update Routes +const WorkerpoolsRoute = WorkerpoolsImport.update({ + id: '/workerpools', + path: '/workerpools', + getParentRoute: () => rootRoute, +} as any) + +const TasksRoute = TasksImport.update({ + id: '/tasks', + path: '/tasks', + getParentRoute: () => rootRoute, +} as any) + +const DealsRoute = DealsImport.update({ + id: '/deals', + path: '/deals', + getParentRoute: () => rootRoute, +} as any) + +const DatasetsRoute = DatasetsImport.update({ + id: '/datasets', + path: '/datasets', + getParentRoute: () => rootRoute, +} as any) + +const AppsRoute = AppsImport.update({ + id: '/apps', + path: '/apps', + getParentRoute: () => rootRoute, +} as any) + const IndexRoute = IndexImport.update({ - id: "/", - path: "/", + id: '/', + path: '/', getParentRoute: () => rootRoute, -} as any); +} as any) // Populate the FileRoutesByPath interface -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface FileRoutesByPath { - "/": { - id: "/"; - path: "/"; - fullPath: "/"; - preLoaderRoute: typeof IndexImport; - parentRoute: typeof rootRoute; - }; + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/apps': { + id: '/apps' + path: '/apps' + fullPath: '/apps' + preLoaderRoute: typeof AppsImport + parentRoute: typeof rootRoute + } + '/datasets': { + id: '/datasets' + path: '/datasets' + fullPath: '/datasets' + preLoaderRoute: typeof DatasetsImport + parentRoute: typeof rootRoute + } + '/deals': { + id: '/deals' + path: '/deals' + fullPath: '/deals' + preLoaderRoute: typeof DealsImport + parentRoute: typeof rootRoute + } + '/tasks': { + id: '/tasks' + path: '/tasks' + fullPath: '/tasks' + preLoaderRoute: typeof TasksImport + parentRoute: typeof rootRoute + } + '/workerpools': { + id: '/workerpools' + path: '/workerpools' + fullPath: '/workerpools' + preLoaderRoute: typeof WorkerpoolsImport + parentRoute: typeof rootRoute + } } } // Create and export the route tree export interface FileRoutesByFullPath { - "/": typeof IndexRoute; + '/': typeof IndexRoute + '/apps': typeof AppsRoute + '/datasets': typeof DatasetsRoute + '/deals': typeof DealsRoute + '/tasks': typeof TasksRoute + '/workerpools': typeof WorkerpoolsRoute } export interface FileRoutesByTo { - "/": typeof IndexRoute; + '/': typeof IndexRoute + '/apps': typeof AppsRoute + '/datasets': typeof DatasetsRoute + '/deals': typeof DealsRoute + '/tasks': typeof TasksRoute + '/workerpools': typeof WorkerpoolsRoute } export interface FileRoutesById { - __root__: typeof rootRoute; - "/": typeof IndexRoute; + __root__: typeof rootRoute + '/': typeof IndexRoute + '/apps': typeof AppsRoute + '/datasets': typeof DatasetsRoute + '/deals': typeof DealsRoute + '/tasks': typeof TasksRoute + '/workerpools': typeof WorkerpoolsRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/"; - fileRoutesByTo: FileRoutesByTo; - to: "/"; - id: "__root__" | "/"; - fileRoutesById: FileRoutesById; + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/apps' | '/datasets' | '/deals' | '/tasks' | '/workerpools' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/apps' | '/datasets' | '/deals' | '/tasks' | '/workerpools' + id: + | '__root__' + | '/' + | '/apps' + | '/datasets' + | '/deals' + | '/tasks' + | '/workerpools' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute; + IndexRoute: typeof IndexRoute + AppsRoute: typeof AppsRoute + DatasetsRoute: typeof DatasetsRoute + DealsRoute: typeof DealsRoute + TasksRoute: typeof TasksRoute + WorkerpoolsRoute: typeof WorkerpoolsRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, -}; + AppsRoute: AppsRoute, + DatasetsRoute: DatasetsRoute, + DealsRoute: DealsRoute, + TasksRoute: TasksRoute, + WorkerpoolsRoute: WorkerpoolsRoute, +} export const routeTree = rootRoute ._addFileChildren(rootRouteChildren) - ._addFileTypes(); + ._addFileTypes() /* ROUTE_MANIFEST_START { @@ -77,11 +179,31 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/" + "/", + "/apps", + "/datasets", + "/deals", + "/tasks", + "/workerpools" ] }, "/": { "filePath": "index.tsx" + }, + "/apps": { + "filePath": "apps.tsx" + }, + "/datasets": { + "filePath": "datasets.tsx" + }, + "/deals": { + "filePath": "deals.tsx" + }, + "/tasks": { + "filePath": "tasks.tsx" + }, + "/workerpools": { + "filePath": "workerpools.tsx" } } } diff --git a/src/routes/apps.tsx b/src/routes/apps.tsx new file mode 100644 index 00000000..8e3948f9 --- /dev/null +++ b/src/routes/apps.tsx @@ -0,0 +1,109 @@ +import { TABLE_LENGTH, TABLE_REFETCH_INTERVAL } from '@/config'; +import { execute } from '@/graphql/execute'; +import { useQuery } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; +import { Box, LoaderCircle, Terminal } from 'lucide-react'; +import { useState } from 'react'; +import { DataTable } from '@/components/DataTable'; +import { PaginatedNavigation } from '@/components/PaginatedNavigation'; +import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; +import { SearcherBar } from '@/modules/SearcherBar'; +import { appsQuery } from '@/modules/apps/appsQuery'; +import { columns } from '@/modules/apps/appsTable/columns'; +import { nextAppsQuery } from '@/modules/apps/nextAppsQuery'; + +export const Route = createFileRoute('/apps')({ + component: AppsRoute, +}); + +function useAppsData(currentPage: number) { + const skip = currentPage * TABLE_LENGTH; + + const { data, isLoading, isRefetching, isError, errorUpdateCount } = useQuery( + { + queryKey: ['apps', currentPage], + queryFn: () => execute(appsQuery, { length: TABLE_LENGTH, skip }), + refetchInterval: TABLE_REFETCH_INTERVAL, + } + ); + + const { data: nextData } = useQuery({ + queryKey: ['apps-next', currentPage], + queryFn: () => + execute(nextAppsQuery, { + length: TABLE_LENGTH * 2, + skip: (currentPage + 1) * TABLE_LENGTH, + }), + refetchInterval: TABLE_REFETCH_INTERVAL, + }); + + const nextApps = nextData?.apps ?? []; + + const additionalPages = Math.ceil(nextApps.length / TABLE_LENGTH); + + const formattedData = + data?.apps.map((app) => ({ + ...app, + destination: `/app/${app.address}`, + })) ?? []; + + return { + data: formattedData, + isLoading, + isRefetching, + isError, + hasPastError: isError || errorUpdateCount > 0, + additionalPages, + }; +} + +function AppsRoute() { + const [currentPage, setCurrentPage] = useState(0); + const { + data, + isLoading, + isRefetching, + isError, + hasPastError, + additionalPages, + } = useAppsData(currentPage); + + return ( +
+ + +

+ + Apps deployed + {data.length > 0 && isError && ( + + (outdated) + + )} + {isLoading && isRefetching && } +

+ + {hasPastError && !data.length ? ( + + + Error + + A error occurred during apps loading. + + + ) : ( + + )} + setCurrentPage(newPage - 1)} + /> +
+ ); +} diff --git a/src/routes/datasets.tsx b/src/routes/datasets.tsx new file mode 100644 index 00000000..db4beb7d --- /dev/null +++ b/src/routes/datasets.tsx @@ -0,0 +1,111 @@ +import { TABLE_LENGTH, TABLE_REFETCH_INTERVAL } from '@/config'; +import { execute } from '@/graphql/execute'; +import { useQuery } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; +import { Box, LoaderCircle, Terminal } from 'lucide-react'; +import { useState } from 'react'; +import { DataTable } from '@/components/DataTable'; +import { PaginatedNavigation } from '@/components/PaginatedNavigation'; +import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; +import { SearcherBar } from '@/modules/SearcherBar'; +import { datasetsQuery } from '@/modules/datasets/datasetsQuery'; +import { columns } from '@/modules/datasets/datasetsTable/columns'; +import { nextDatasetsQuery } from '@/modules/datasets/nextDatasetsQuery'; + +export const Route = createFileRoute('/datasets')({ + component: DatasetsRoute, +}); + +function useDatasetsData(currentPage: number) { + const skip = currentPage * TABLE_LENGTH; + + const { data, isLoading, isRefetching, isError, errorUpdateCount } = useQuery( + { + queryKey: ['datasets', currentPage], + queryFn: () => execute(datasetsQuery, { length: TABLE_LENGTH, skip }), + refetchInterval: TABLE_REFETCH_INTERVAL, + } + ); + + const { data: nextData } = useQuery({ + queryKey: ['datasets-next', currentPage], + queryFn: () => + execute(nextDatasetsQuery, { + length: TABLE_LENGTH * 2, + skip: (currentPage + 1) * TABLE_LENGTH, + }), + refetchInterval: TABLE_REFETCH_INTERVAL, + }); + + const nextDatasets = nextData?.datasets ?? []; + + const additionalPages = Math.ceil(nextDatasets.length / TABLE_LENGTH); + + const formattedData = + data?.datasets.map((dataset) => ({ + ...dataset, + destination: `/dataset/${dataset.address}`, + })) ?? []; + + return { + data: formattedData, + isLoading, + isRefetching, + isError: isError, + hasPastError: isError || errorUpdateCount > 0, + additionalPages, + }; +} + +function DatasetsRoute() { + const [currentPage, setCurrentPage] = useState(0); + const { + data, + isLoading, + isRefetching, + isError, + hasPastError, + additionalPages, + } = useDatasetsData(currentPage); + + return ( +
+ + +

+ + Datasets + {data.length > 0 && isError && ( + + (outdated) + + )} + {(isLoading || isRefetching) && ( + + )} +

+ + {hasPastError && !data.length ? ( + + + Error + + A error occurred during datasets loading. + + + ) : ( + + )} + setCurrentPage(newPage - 1)} + /> +
+ ); +} diff --git a/src/routes/deals.tsx b/src/routes/deals.tsx new file mode 100644 index 00000000..e22240a9 --- /dev/null +++ b/src/routes/deals.tsx @@ -0,0 +1,110 @@ +import { TABLE_LENGTH, TABLE_REFETCH_INTERVAL } from '@/config'; +import { execute } from '@/graphql/execute'; +import { useQuery } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; +import { Box, LoaderCircle, Terminal } from 'lucide-react'; +import { useState } from 'react'; +import { DataTable } from '@/components/DataTable'; +import { PaginatedNavigation } from '@/components/PaginatedNavigation'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { SearcherBar } from '@/modules/SearcherBar'; +import { dealsQuery } from '@/modules/deals/dealsQuery'; +import { columns } from '@/modules/deals/dealsTable/columns'; +import { nextDealsQuery } from '@/modules/deals/nextDealsQuery'; + +export const Route = createFileRoute('/deals')({ + component: DealsRoute, +}); + +function useDealsData(currentPage: number) { + const skip = currentPage * TABLE_LENGTH; + + const { data, isLoading, isRefetching, isError, errorUpdateCount } = useQuery( + { + queryKey: ['deals', currentPage], + queryFn: () => execute(dealsQuery, { length: TABLE_LENGTH, skip }), + refetchInterval: TABLE_REFETCH_INTERVAL, + } + ); + + const { data: nextData } = useQuery({ + queryKey: ['deals-next', currentPage], + queryFn: () => + execute(nextDealsQuery, { + length: TABLE_LENGTH * 2, + skip: (currentPage + 1) * TABLE_LENGTH, + }), + refetchInterval: TABLE_REFETCH_INTERVAL, + }); + + const nextDeals = nextData?.deals ?? []; + + const additionalPages = Math.ceil(nextDeals.length / TABLE_LENGTH); + + const formattedData = + data?.deals.map((deal) => ({ + ...deal, + destination: `/deal/${deal.dealid}`, + })) ?? []; + + return { + data: formattedData, + isLoading, + isRefetching, + isError: isError, + hasPastError: isError || errorUpdateCount > 0, + additionalPages, + }; +} + +function DealsRoute() { + const [currentPage, setCurrentPage] = useState(0); + const { + data, + isLoading, + isRefetching, + isError, + hasPastError, + additionalPages, + } = useDealsData(currentPage); + + return ( +
+ + +

+ + Deals + {data.length > 0 && isError && ( + + (outdated) + + )} + {(isLoading || isRefetching) && ( + + )} +

+ {hasPastError && !data.length ? ( + + + Error + + A error occurred during deals loading. + + + ) : ( + + )} + setCurrentPage(newPage - 1)} + /> +
+ ); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index df78bc7a..38efbc68 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,9 +1,10 @@ import { createFileRoute } from '@tanstack/react-router'; +import { SearcherBar } from '@/modules/SearcherBar'; import { AppsPreviewTable } from '@/modules/apps/AppsPreviewTable'; import { DatasetsPreviewTable } from '@/modules/datasets/DatasetsPreviewTable'; import { DealsPreviewTable } from '@/modules/deals/DealsPreviewTable'; import { TasksPreviewTable } from '@/modules/tasks/TasksPreviewTable'; -import { WorkerpoolsPreviewTable } from '@/modules/workerpools/workerpoolsPreviewTable'; +import { WorkerpoolsPreviewTable } from '@/modules/workerpools/WorkerpoolsPreviewTable'; export const Route = createFileRoute('/')({ component: Index, @@ -11,9 +12,10 @@ export const Route = createFileRoute('/')({ function Index() { return ( -
-
-
+
+
+ +
diff --git a/src/routes/tasks.tsx b/src/routes/tasks.tsx new file mode 100644 index 00000000..36b15858 --- /dev/null +++ b/src/routes/tasks.tsx @@ -0,0 +1,111 @@ +import { TABLE_LENGTH, TABLE_REFETCH_INTERVAL } from '@/config'; +import { execute } from '@/graphql/execute'; +import { useQuery } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; +import { Box, LoaderCircle, Terminal } from 'lucide-react'; +import { useState } from 'react'; +import { DataTable } from '@/components/DataTable'; +import { PaginatedNavigation } from '@/components/PaginatedNavigation'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { SearcherBar } from '@/modules/SearcherBar'; +import { nextTasksQuery } from '@/modules/tasks/nextTasksQuery'; +import { tasksQuery } from '@/modules/tasks/tasksQuery'; +import { columns } from '@/modules/tasks/tasksTable/columns'; + +export const Route = createFileRoute('/tasks')({ + component: TasksRoute, +}); + +function useTasksData(currentPage: number) { + const skip = currentPage * TABLE_LENGTH; + + const { data, isLoading, isRefetching, isError, errorUpdateCount } = useQuery( + { + queryKey: ['tasks', currentPage], + queryFn: () => execute(tasksQuery, { length: TABLE_LENGTH, skip }), + refetchInterval: TABLE_REFETCH_INTERVAL, + } + ); + + const { data: nextData } = useQuery({ + queryKey: ['tasks-next', currentPage], + queryFn: () => + execute(nextTasksQuery, { + length: TABLE_LENGTH * 2, + skip: (currentPage + 1) * TABLE_LENGTH, + }), + refetchInterval: TABLE_REFETCH_INTERVAL, + }); + + const nextTasks = nextData?.tasks ?? []; + + const additionalPages = Math.ceil(nextTasks.length / TABLE_LENGTH); + + const formattedData = + data?.tasks.map((task) => ({ + ...task, + destination: `/task/${task.taskid}`, + })) ?? []; + + return { + data: formattedData, + isLoading, + isRefetching, + isError: isError, + hasPastError: isError || errorUpdateCount > 0, + additionalPages, + }; +} + +function TasksRoute() { + const [currentPage, setCurrentPage] = useState(0); + const { + data, + isLoading, + isRefetching, + isError, + hasPastError, + additionalPages, + } = useTasksData(currentPage); + + return ( +
+ + +

+ + Tasks + {data.length > 0 && isError && ( + + (outdated) + + )} + {(isLoading || isRefetching) && ( + + )} +

+ + {hasPastError && !data.length ? ( + + + Error + + A error occurred during tasks loading. + + + ) : ( + + )} + setCurrentPage(newPage - 1)} + /> +
+ ); +} diff --git a/src/routes/workerpools.tsx b/src/routes/workerpools.tsx new file mode 100644 index 00000000..bb9d0676 --- /dev/null +++ b/src/routes/workerpools.tsx @@ -0,0 +1,111 @@ +import { TABLE_LENGTH, TABLE_REFETCH_INTERVAL } from '@/config'; +import { execute } from '@/graphql/execute'; +import { useQuery } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; +import { Box, LoaderCircle, Terminal } from 'lucide-react'; +import { useState } from 'react'; +import { DataTable } from '@/components/DataTable'; +import { PaginatedNavigation } from '@/components/PaginatedNavigation'; +import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; +import { SearcherBar } from '@/modules/SearcherBar'; +import { nextWorkerpoolsQuery } from '@/modules/workerpools/nextWorkerpoolsQuery'; +import { workerpoolsQuery } from '@/modules/workerpools/workerpoolsQuery'; +import { columns } from '@/modules/workerpools/workerpoolsTable/columns'; + +export const Route = createFileRoute('/workerpools')({ + component: WorkerpoolsRoute, +}); + +function useWorkerpoolsData(currentPage: number) { + const skip = currentPage * TABLE_LENGTH; + + const { data, isLoading, isRefetching, isError, errorUpdateCount } = useQuery( + { + queryKey: ['workerpools', currentPage], + queryFn: () => execute(workerpoolsQuery, { length: TABLE_LENGTH, skip }), + refetchInterval: TABLE_REFETCH_INTERVAL, + } + ); + + const { data: nextData } = useQuery({ + queryKey: ['workerpools-next', currentPage], + queryFn: () => + execute(nextWorkerpoolsQuery, { + length: TABLE_LENGTH * 2, + skip: (currentPage + 1) * TABLE_LENGTH, + }), + refetchInterval: TABLE_REFETCH_INTERVAL, + }); + + const nextWorkerpools = nextData?.workerpools ?? []; + + const additionalPages = Math.ceil(nextWorkerpools.length / TABLE_LENGTH); + + const formattedData = + data?.workerpools.map((workerpool) => ({ + ...workerpool, + destination: `/workerpool/${workerpool.address}`, + })) ?? []; + + return { + data: formattedData, + isLoading, + isRefetching, + isError: isError, + hasPastError: isError || errorUpdateCount > 0, + additionalPages, + }; +} + +function WorkerpoolsRoute() { + const [currentPage, setCurrentPage] = useState(0); + const { + data, + isLoading, + isRefetching, + isError, + hasPastError, + additionalPages, + } = useWorkerpoolsData(currentPage); + + return ( +
+ + +

+ + Workerpools + {data.length > 0 && isError && ( + + (outdated) + + )} + {(isLoading || isRefetching) && ( + + )} +

+ + {hasPastError && !data.length ? ( + + + Error + + A error occurred during workerpool loading. + + + ) : ( + + )} + setCurrentPage(newPage - 1)} + /> +
+ ); +}