From af9042878e3778f1b0e9bff67f5db17adc0dfc00 Mon Sep 17 00:00:00 2001 From: Jensen Zhang Date: Thu, 12 Oct 2023 21:17:21 +0800 Subject: [PATCH] Components: Add pages and tags for Transfers --- .../Pages/Transfers/ListTransfer.stories.tsx | 15 ++++ .../Pages/Transfers/ListTransfer.tsx | 76 +++++++++++++++++++ .../ListTransferStatistics.stories.tsx | 15 ++++ .../Transfers/ListTransferStatistics.tsx | 48 ++++++++++++ .../Transfers/TransferStatistics.stories.tsx | 15 ++++ .../Pages/Transfers/TransferStatistics.tsx | 26 +++++++ .../Tags/RequestTypeTag.stories.tsx | 15 ++++ src/component-library/Tags/RequestTypeTag.tsx | 68 +++++++++++++++++ src/lib/core/entity/rucio.ts | 22 ++++++ .../data/view-model/request-stats.ts | 4 + .../infrastructure/data/view-model/request.ts | 32 ++++++++ test/fixtures/table-fixtures.ts | 65 +++++++++++++++- 12 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 src/component-library/Pages/Transfers/ListTransfer.stories.tsx create mode 100644 src/component-library/Pages/Transfers/ListTransfer.tsx create mode 100644 src/component-library/Pages/Transfers/ListTransferStatistics.stories.tsx create mode 100644 src/component-library/Pages/Transfers/ListTransferStatistics.tsx create mode 100644 src/component-library/Pages/Transfers/TransferStatistics.stories.tsx create mode 100644 src/component-library/Pages/Transfers/TransferStatistics.tsx create mode 100644 src/component-library/Tags/RequestTypeTag.stories.tsx create mode 100644 src/component-library/Tags/RequestTypeTag.tsx create mode 100644 src/lib/infrastructure/data/view-model/request-stats.ts create mode 100644 src/lib/infrastructure/data/view-model/request.ts diff --git a/src/component-library/Pages/Transfers/ListTransfer.stories.tsx b/src/component-library/Pages/Transfers/ListTransfer.stories.tsx new file mode 100644 index 00000000..3e758826 --- /dev/null +++ b/src/component-library/Pages/Transfers/ListTransfer.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryFn } from "@storybook/react"; +import { ListTransfer as L } from "./ListTransfer"; +import { fixtureTransferViewModel, mockUseComDOM } from "test/fixtures/table-fixtures"; + +export default { + title: 'Components/Pages/Transfers', + component: L, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const ListTransfer = Template.bind({}) +ListTransfer.args = { + comdom: mockUseComDOM(Array.from({ length: 50 }, () => fixtureTransferViewModel())) +}; diff --git a/src/component-library/Pages/Transfers/ListTransfer.tsx b/src/component-library/Pages/Transfers/ListTransfer.tsx new file mode 100644 index 00000000..3b943267 --- /dev/null +++ b/src/component-library/Pages/Transfers/ListTransfer.tsx @@ -0,0 +1,76 @@ +import useReponsiveHook from "@/component-library/Helpers/ResponsiveHook" +import { TableFilterDiscrete } from "@/component-library/StreamedTables/TableFilterDiscrete" +import { TableFilterString } from "@/component-library/StreamedTables/TableFilterString" +import { RequestTypeTag } from "@/component-library/Tags/RequestTypeTag" +import { RequestType } from "@/lib/core/entity/rucio" +import { TransferViewModel } from "@/lib/infrastructure/data/view-model/request" +import { UseComDOM } from "@/lib/infrastructure/hooks/useComDOM" +import { createColumnHelper } from "@tanstack/react-table" +import { HiDotsHorizontal } from "react-icons/hi" +import { twMerge } from "tailwind-merge" +import { Heading } from "../Helpers/Heading" +import { StreamedTable } from "@/component-library/StreamedTables/StreamedTable" +import { Body } from "../Helpers/Body" + +export const ListTransfer = ( + props: { + comdom: UseComDOM + } +) => { + const columnHelper = createColumnHelper() + const tablecolumns = [ + columnHelper.accessor("scope", {}), + columnHelper.accessor("name", {}), + columnHelper.accessor("request_type", { + id: "request_type", + header: info => { + return ( + + name="Request Type" + keys={Object.values(RequestType)} + renderFunc={key => key === undefined ? : } + column={info.column} + stack + /> + ) + }, + cell: info => , + meta: { + style: "w-8 md:w-32" + } + }), + columnHelper.accessor("requested_at", { + }), + columnHelper.accessor("bytes", { + }), + columnHelper.accessor("priority", { + }), + columnHelper.accessor("transfertool", { + }) + ] + + const responsive = useReponsiveHook() + + return ( +
+ + + + tablecomdom={props.comdom} + tablecolumns={tablecolumns} + tablestyling={{ + tableHeadRowStyle: "md:h-16", + visibility: { + } + }} + /> + +
+ ) +} \ No newline at end of file diff --git a/src/component-library/Pages/Transfers/ListTransferStatistics.stories.tsx b/src/component-library/Pages/Transfers/ListTransferStatistics.stories.tsx new file mode 100644 index 00000000..612951f1 --- /dev/null +++ b/src/component-library/Pages/Transfers/ListTransferStatistics.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryFn } from "@storybook/react"; +import { ListTransferStatistics as L } from "./ListTransferStatistics"; +import { fixtureTransferStatsViewModel, mockUseComDOM } from "test/fixtures/table-fixtures"; + +export default { + title: 'Components/Pages/Transfers', + component: L, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const ListTransferStatistics = Template.bind({}) +ListTransferStatistics.args = { + comdom: mockUseComDOM(Array.from({ length: 50 }, () => fixtureTransferStatsViewModel())) +} \ No newline at end of file diff --git a/src/component-library/Pages/Transfers/ListTransferStatistics.tsx b/src/component-library/Pages/Transfers/ListTransferStatistics.tsx new file mode 100644 index 00000000..17bc4ed9 --- /dev/null +++ b/src/component-library/Pages/Transfers/ListTransferStatistics.tsx @@ -0,0 +1,48 @@ +import { StreamedTable } from "@/component-library/StreamedTables/StreamedTable" +import { TransferStatsViewModel } from "@/lib/infrastructure/data/view-model/request-stats" +import { UseComDOM } from "@/lib/infrastructure/hooks/useComDOM" +import { Heading } from "../Helpers/Heading" +import { createColumnHelper } from "@tanstack/react-table" +import { Body } from "../Helpers/Body" +import { Contenttd, Generaltable, Titleth } from "@/component-library/Helpers/Metatable" +import { H3 } from "@/component-library/Text/Headings/H3" +import { TransferStatistics } from "./TransferStatistics" + +export const ListTransferStatistics = ( + props: { + comdom: UseComDOM + } +) => { + const columnHelper = createColumnHelper() + const tablecolumns = [ + columnHelper.accessor("source_rse", { + }), + columnHelper.accessor("dest_rse", { + }), + columnHelper.accessor("request_stats", { + id: "request_stats", + header: info =>

Transfer Statistics

, + cell: info => { + return + } + }) + ] + + return ( +
+ + + + tablecomdom={props.comdom} + tablecolumns={tablecolumns} + tablestyling={{ + tableHeadRowStyle: "md:h-16", + visibility: {} + }} + /> + +
+ ) +} \ No newline at end of file diff --git a/src/component-library/Pages/Transfers/TransferStatistics.stories.tsx b/src/component-library/Pages/Transfers/TransferStatistics.stories.tsx new file mode 100644 index 00000000..7e26089a --- /dev/null +++ b/src/component-library/Pages/Transfers/TransferStatistics.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryFn } from "@storybook/react"; +import { TransferStatistics as T } from "./TransferStatistics"; +import { fixtureRequestStatsPerPair } from "test/fixtures/table-fixtures"; + +export default { + title: 'Components/Pages/Transfers', + component: T, +} as Meta + +const Template: StoryFn = (args) => ; + +export const TransferStatistics = Template.bind({}) +TransferStatistics.args = { + request_stats: fixtureRequestStatsPerPair() +} \ No newline at end of file diff --git a/src/component-library/Pages/Transfers/TransferStatistics.tsx b/src/component-library/Pages/Transfers/TransferStatistics.tsx new file mode 100644 index 00000000..80770926 --- /dev/null +++ b/src/component-library/Pages/Transfers/TransferStatistics.tsx @@ -0,0 +1,26 @@ +import { NormalTable } from "@/component-library/StreamedTables/NormalTable" +import { TableStyling } from "@/component-library/StreamedTables/types" +import { RequestStatsPerPair } from "@/lib/core/entity/rucio" +import { createColumnHelper } from "@tanstack/react-table" + +export const TransferStatistics = ( + props: { + request_stats: RequestStatsPerPair[] + } +) => { + const columnHelper = createColumnHelper() + const tablecolumns: any[] = [ + columnHelper.accessor("activity", {}), + columnHelper.accessor("counter", {}), + columnHelper.accessor("bytes", {}), + ] + return ( + + tabledata={props.request_stats} + tablecolumns={tablecolumns} + tablestyling={{ + pageSize: 5 + } as TableStyling} + /> + ) +} \ No newline at end of file diff --git a/src/component-library/Tags/RequestTypeTag.stories.tsx b/src/component-library/Tags/RequestTypeTag.stories.tsx new file mode 100644 index 00000000..068019ba --- /dev/null +++ b/src/component-library/Tags/RequestTypeTag.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryFn } from "@storybook/react"; +import { RequestTypeTag as R } from "./RequestTypeTag"; +import { RequestType } from "@/lib/core/entity/rucio"; + +export default { + title: 'Components/Tags', + component: R, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const RequestTypeTag = Template.bind({}); +RequestTypeTag.args = { + requesttype: RequestType.TRANSFER +} \ No newline at end of file diff --git a/src/component-library/Tags/RequestTypeTag.tsx b/src/component-library/Tags/RequestTypeTag.tsx new file mode 100644 index 00000000..0016488e --- /dev/null +++ b/src/component-library/Tags/RequestTypeTag.tsx @@ -0,0 +1,68 @@ +import { RequestType } from "@/lib/core/entity/rucio"; +import { RowSelection } from "@tanstack/react-table"; +import React, { useEffect, useState } from "react"; +import { twMerge } from "tailwind-merge"; + +type RequestTypeTagProps = JSX.IntrinsicElements["span"] & { + requesttype: RequestType; + forcesmall?: boolean; + neversmall?: boolean; +}; + +export const RequestTypeTag: React.FC = ( + { + requesttype = RequestType.TRANSFER, + forcesmall = false, + neversmall = false, + ...props + } +) => { + const { className, ...restprops } = props + + const [windowWidth, setWindowWidth] = useState(720) + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth) + handleResize() + window.addEventListener('resize', handleResize) + }, []) + const belowMedium = (windowWidth < 768) || forcesmall + const stringMatch = { + [RequestType.UPLOAD]: "Upload", + [RequestType.DOWNLOAD]: "Download", + [RequestType.STAGEIN]: "Stage In", + [RequestType.STAGEOUT]: "Stage Out", + [RequestType.TRANSFER]: "Transfer" + } + + const colPicker = (requesttype: RequestType) => { + switch (requesttype) { + case RequestType.UPLOAD: + return "emerald" + case RequestType.DOWNLOAD: + return "blue" + case RequestType.STAGEIN: + return "rose" + case RequestType.STAGEOUT: + return "amber" + case RequestType.TRANSFER: + return "gray" + } + } + + const color = colPicker(requesttype) + + return ( + + {belowMedium && !neversmall ? stringMatch[requesttype].split(' ').slice(-1)[0].slice(0, 1) : stringMatch[requesttype]} + + ); +}; \ No newline at end of file diff --git a/src/lib/core/entity/rucio.ts b/src/lib/core/entity/rucio.ts index f052ecd8..b03c46a0 100644 --- a/src/lib/core/entity/rucio.ts +++ b/src/lib/core/entity/rucio.ts @@ -256,6 +256,28 @@ export type Request = { requested_at: DateISO, priority: number, transfertool: string, + source_rse: string, + dest_rse: string, +} + +/** + * Represents statistics for the nubmer of transfer requests over dest_rse and + * source_rse in Rucio. + */ +export type RequestStatsPerPair = { + activity: string, + counter: number, + bytes: number, +} + +export type RequestStats = { + account: string, + state: RequestState, + dest_rse_id: string, + source_rse_id: string, + dest_rse: string, + source_rse: string, + request_stats: RequestStatsPerPair[], } /* diff --git a/src/lib/infrastructure/data/view-model/request-stats.ts b/src/lib/infrastructure/data/view-model/request-stats.ts new file mode 100644 index 00000000..1cdf0e16 --- /dev/null +++ b/src/lib/infrastructure/data/view-model/request-stats.ts @@ -0,0 +1,4 @@ +import { RequestStats, RequestStatsPerPair } from "@/lib/core/entity/rucio"; +import { BaseViewModel } from "@/lib/sdk/view-models"; + +export interface TransferStatsViewModel extends BaseViewModel, RequestStats {} diff --git a/src/lib/infrastructure/data/view-model/request.ts b/src/lib/infrastructure/data/view-model/request.ts new file mode 100644 index 00000000..17d78c9d --- /dev/null +++ b/src/lib/infrastructure/data/view-model/request.ts @@ -0,0 +1,32 @@ +import { DIDType, Request, RequestState, RequestType } from "@/lib/core/entity/rucio"; +import { BaseViewModel } from "@/lib/sdk/view-models"; + +export interface TransferViewModel extends BaseViewModel, Request {} + +/** + * A utility function to retrieve an empty {@link TransferViewModel}. + * @return an empty TransferViewModel + */ +export function getEmptyTransferViewModel(): TransferViewModel { + const viewModel: TransferViewModel = { + status: 'error', + id: '', + request_type: RequestType.TRANSFER, + scope: '', + name: '', + did_type: DIDType.UNKNOWN, + dest_rse_id: '', + source_rse_id: '', + attributes: '', + state: RequestState.FAILED, + activity: '', + bytes: 0, + account: '', + requested_at: '', + priority: 0, + transfertool: '', + source_rse: '', + dest_rse: '', + } + return viewModel +} \ No newline at end of file diff --git a/test/fixtures/table-fixtures.ts b/test/fixtures/table-fixtures.ts index 33046051..45612f78 100644 --- a/test/fixtures/table-fixtures.ts +++ b/test/fixtures/table-fixtures.ts @@ -7,6 +7,9 @@ import { RSEType, RSEProtocol, RSEAttribute, + RequestType, + RequestState, + RequestStatsPerPair, } from '@/lib/core/entity/rucio' import { RSEAccountUsageLimitViewModel, RSEAttributeViewModel, RSEProtocolViewModel, RSEViewModel } from '@/lib/infrastructure/data/view-model/rse'; import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; @@ -15,6 +18,8 @@ import { BaseViewModel } from '@/lib/sdk/view-models'; import { DIDDatasetReplicasViewModel, DIDKeyValuePairsDataViewModel, DIDLongViewModel, DIDMetaViewModel, DIDRulesViewModel, DIDViewModel, FilereplicaStateDViewModel, FilereplicaStateViewModel } from '@/lib/infrastructure/data/view-model/did'; import { RuleMetaViewModel, RulePageLockEntryViewModel, RuleViewModel } from '@/lib/infrastructure/data/view-model/rule'; import { DIDKeyValuePair } from '@/lib/core/entity/rucio'; +import { TransferViewModel } from '@/lib/infrastructure/data/view-model/request'; +import { TransferStatsViewModel } from '@/lib/infrastructure/data/view-model/request-stats'; export function mockUseComDOM(data: T[]): UseComDOM { return { @@ -66,6 +71,17 @@ function createRSEExpression(): string { return strings.join("&") } +function randomRequestStats(activities: string[]): RequestStatsPerPair[] { + const avail_activities = faker.helpers.arrayElements(activities) + return avail_activities.map((activity) => { + return { + activity: activity, + counter: faker.number.int({ max: 100 }), + bytes: faker.number.int({ min: 0, max: 1e12 }), + }; + }) +} + export function fixtureDIDViewModel(): DIDViewModel { return { ...mockBaseVM(), @@ -371,11 +387,58 @@ export function fixtureSubscriptionViewModel(): SubscriptionViewModel { } } - export function generateSequenceArray(length: number, generator: () => any): any[] { const result: number[] = []; for (let i = 1; i <= length; i++) { result.push(generator()); } return result; +} + +export function fixtureTransferViewModel(): TransferViewModel { + const did_type = faker.helpers.arrayElement([DIDType.CONTAINER, DIDType.DATASET, DIDType.FILE]) + return { + ...mockBaseVM(), + id: faker.string.uuid(), + request_type: randomEnum(RequestType), + scope: createRandomScope(), + name: faker.lorem.words(3).replace(/\s/g, "."), + did_type: did_type, + dest_rse_id: faker.string.uuid(), + source_rse_id: faker.string.uuid(), + attributes: JSON.stringify({ + "source_replica_expression": createRSEExpression(), + "allow_tape_source": faker.datatype.boolean(), + "ds_name": faker.lorem.word(), + "lifetime": faker.date.future().toISOString() + }), + state: randomEnum(RequestState), + activity: faker.lorem.words({ min: 1, max: 3 }), + bytes: faker.number.int({ min: 0, max: 1e12 }), + account: faker.internet.userName(), + priority: faker.number.int({ min: 0, max: 3 }), + transfertool: faker.helpers.arrayElement(['fts', 'globus']), + requested_at: faker.date.past().toISOString(), + source_rse: createRSEName(), + dest_rse: createRSEName(), + } +} + +export function fixtureTransferStatsViewModel(): TransferStatsViewModel { + const activities = faker.helpers.multiple(() => faker.lorem.words({ min: 1, max: 3 }), { count: 9 }) + return { + ...mockBaseVM(), + account: faker.internet.userName(), + state: randomEnum(RequestState), + source_rse_id: faker.string.uuid(), + dest_rse_id: faker.string.uuid(), + source_rse: createRSEName(), + dest_rse: createRSEName(), + request_stats: randomRequestStats(activities), + } +} + +export function fixtureRequestStatsPerPair(): RequestStatsPerPair[] { + const activities = faker.helpers.multiple(() => faker.lorem.words({ min: 1, max: 3 }), { count: 9 }) + return randomRequestStats(activities) } \ No newline at end of file