diff --git a/src/app/(rucio)/did/page/[scope]/[name]/page.tsx b/src/app/(rucio)/did/page/[scope]/[name]/page.tsx index fc2fa4ea8..d66bc35fe 100644 --- a/src/app/(rucio)/did/page/[scope]/[name]/page.tsx +++ b/src/app/(rucio)/did/page/[scope]/[name]/page.tsx @@ -1,132 +1,15 @@ 'use client'; -import { PageDID as PageDIDStory } from '@/component-library/pages/legacy/DID/PageDID'; -import useComDOM from '@/lib/infrastructure/hooks/useComDOM'; -import { useEffect, useState } from 'react'; -import { HTTPRequest } from '@/lib/sdk/http'; -import { - DIDDatasetReplicasViewModel, - DIDKeyValuePairsDataViewModel, - DIDMetaViewModel, - DIDRulesViewModel, - DIDViewModel, - FileReplicaStateViewModel, -} from '@/lib/infrastructure/data/view-model/did'; -import { didKeyValuePairsDataQuery, didMetaQueryBase } from '@/app/(rucio)/did/queries'; -import { Loading } from '@/component-library/pages/legacy/Helpers/Loading'; + +import { DetailsDID } from '@/component-library/pages/DID/details/DetailsDID'; +import { useEffect } from 'react'; export default function Page({ params }: { params: { scope: string; name: string } }) { const decodedScope = decodeURIComponent(params.scope); const decodedName = decodeURIComponent(params.name); - const [didMeta, setDIDMeta] = useState({ status: 'pending' } as DIDMetaViewModel); - const [didKeyValuePairsData, setDIDKeyValuePairsData] = useState({ status: 'pending' } as DIDKeyValuePairsDataViewModel); - const [fromDidList, setFromDidList] = useState('yosearch'); - useEffect(() => { - didMetaQueryBase(decodedScope, decodedName).then(setDIDMeta); - }, []); useEffect(() => { - didKeyValuePairsDataQuery(decodedScope, decodedName).then(setDIDKeyValuePairsData); + document.title = `${decodedScope}:${decodedName} - Rucio`; }, []); - const didParentsComDOM = useComDOM('page-did-parents-query', [], false, Infinity, 200, true); - const didContentsComDOM = useComDOM('page-did-contents-query', [], false, Infinity, 200, true); - const didFileReplicasComDOM = useComDOM('page-did-filereplicas-query', [], false, Infinity, 200, true); - const didFileReplicasDOnChange = (scope: string, name: string) => { - didFileReplicasComDOM.setRequest({ - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/list-file-replicas`), - method: 'GET', - params: { - scope: scope, - name: name, - }, - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - } as HTTPRequest); - didFileReplicasComDOM.start(); - }; - const didRulesComDOM = useComDOM('page-did-rules-query', [], false, Infinity, 200, true); - const didDatasetReplicasComDOM = useComDOM('page-did-datasetreplicas-query', [], false, Infinity, 200, true); - useEffect(() => { - const setRequests = async () => { - await didContentsComDOM.setRequest({ - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/list-did-contents`), - method: 'GET', - params: { - scope: decodedScope, - name: decodedName, - }, - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - } as HTTPRequest); - await didParentsComDOM.setRequest({ - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/list-did-parents`), - method: 'GET', - params: { - scope: decodedScope, - name: decodedName, - }, - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - } as HTTPRequest); - await didFileReplicasComDOM.setRequest({ - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/list-file-replicas`), - method: 'GET', - params: { - scope: decodedScope, - name: decodedName, - }, - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - } as HTTPRequest); - await didRulesComDOM.setRequest({ - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/list-did-rules`), - method: 'GET', - params: { - scope: decodedScope, - name: decodedName, - }, - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - } as HTTPRequest); - await didDatasetReplicasComDOM.setRequest({ - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/list-dataset-replicas`), - method: 'GET', - params: { - scope: decodedScope, - name: decodedName, - }, - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - } as HTTPRequest); - }; - setRequests(); - }, []); - if (didMeta.status === 'pending') { - return ; - } - return ( - - ); + return ; } diff --git a/src/app/(rucio)/rse/page/[name]/page.tsx b/src/app/(rucio)/rse/page/[name]/page.tsx index a91d1baf0..8f98b11e5 100644 --- a/src/app/(rucio)/rse/page/[name]/page.tsx +++ b/src/app/(rucio)/rse/page/[name]/page.tsx @@ -1,43 +1,9 @@ -'use client'; -import { Loading } from '@/component-library/pages/legacy/Helpers/Loading'; -import { PageRSE as PageRSEStory } from '@/component-library/pages/legacy/RSE/PageRSE'; -import { RSEBlockState } from '@/lib/core/entity/rucio'; -import { RSEAttributeViewModel, RSEProtocolViewModel, RSEViewModel } from '@/lib/infrastructure/data/view-model/rse'; -import { useEffect, useState } from 'react'; - -async function getRSE(rseName: string): Promise { - const url = `${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/get-rse?` + new URLSearchParams({ rseName }); - const res = await fetch(url); - return await res.json(); -} - -async function getProtocols(rseName: string): Promise { - const url = `${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/get-rse-protocols?` + new URLSearchParams({ rseName }); - const res = await fetch(url); - return await res.json(); -} - -async function getAttributes(rseName: string): Promise { - const url = `${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/get-rse-attributes?` + new URLSearchParams({ rseName }); - const res = await fetch(url); - return await res.json(); -} +import { DetailsRSE } from '@/component-library/pages/RSE/details/DetailsRSE'; export default function Page({ params }: { params: { name: string } }) { - const [rse, setRSE] = useState({ status: 'pending' } as RSEViewModel); - const [protocols, setProtocols] = useState({ status: 'pending' } as RSEProtocolViewModel); - const [attributes, setAttributes] = useState({ status: 'pending' } as RSEAttributeViewModel); - useEffect(() => { - getRSE(params.name).then(setRSE); - }, [params.name]); - useEffect(() => { - getProtocols(params.name).then(setProtocols); - }, [params.name]); - useEffect(() => { - getAttributes(params.name).then(setAttributes); - }, [params.name]); - if (rse.status === 'pending' || protocols.status === 'pending' || attributes.status === 'pending') { - return ; - } - return ; + return ; } + +export const metadata = { + title: 'RSE - Rucio', +}; diff --git a/src/app/(rucio)/rule/page/[id]/page.tsx b/src/app/(rucio)/rule/page/[id]/page.tsx index cbf43affe..a8e3c1dc8 100644 --- a/src/app/(rucio)/rule/page/[id]/page.tsx +++ b/src/app/(rucio)/rule/page/[id]/page.tsx @@ -1,61 +1,12 @@ 'use client'; -import { PageRule as PageRuleStory } from '@/component-library/pages/legacy/Rule/PageRule'; -import { fixtureRuleMetaViewModel } from 'test/fixtures/table-fixtures'; -import { useState, useEffect } from 'react'; -import useComDOM from '@/lib/infrastructure/hooks/useComDOM'; -import { HTTPRequest } from '@/lib/sdk/http'; -import { RuleMetaViewModel, RulePageLockEntryViewModel } from '@/lib/infrastructure/data/view-model/rule'; -import { getSiteHeader } from '@/app/(rucio)/queries'; -import { SiteHeaderViewModel } from '@/lib/infrastructure/data/view-model/site-header'; -export default function PageRule({ params }: { params: { id: string } }) { - const comDOM = useComDOM('rule-page-lock-query', [], false, Infinity, 50, true); - const [isAdmin, setIsAdmin] = useState(false); - useEffect(() => { - getSiteHeader().then((vm: SiteHeaderViewModel) => setIsAdmin(vm.activeAccount?.role === 'admin')); - }, []); - const [meta, setMeta] = useState({} as RuleMetaViewModel); +import { DetailsRule } from '@/component-library/pages/Rule/details/DetailsRule'; +import { useEffect } from 'react'; +export default function PageRule({ params }: { params: { id: string } }) { useEffect(() => { - // TODO get from mock endpoint - fetch(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/mock-get-rule-meta`) - .then(res => { - if (res.ok) { - return res.json(); - } - throw new Error(res.statusText); - }) - .then(data => { - setMeta({ ...data, id: params.id }); - }) - .catch(err => { - console.error(err); - }); - // setMeta({ ...fixtureRuleMetaViewModel(), id: params.id }) + document.title = 'Rule - Rucio'; }, []); - useEffect(() => { - const runQuery = async () => { - const request: HTTPRequest = { - url: new URL(`${process.env.NEXT_PUBLIC_WEBUI_HOST}/api/feature/mock-list-rule-page-lock`), - method: 'GET', - headers: new Headers({ - 'Content-Type': 'application/json', - } as HeadersInit), - body: null, - }; - await comDOM.setRequest(request); - }; - runQuery(); - }, []); - return ( - { - console.log('boost not implemented'); - }} - ruleBoostShow={isAdmin} - /> - ); + return ; } diff --git a/src/component-library/features/badges/DID/ReplicaStateBadge.tsx b/src/component-library/features/badges/DID/ReplicaStateBadge.tsx new file mode 100644 index 000000000..c5960d632 --- /dev/null +++ b/src/component-library/features/badges/DID/ReplicaStateBadge.tsx @@ -0,0 +1,29 @@ +import { ReplicaState } from '@/lib/core/entity/rucio'; +import React from 'react'; +import { Badge } from '@/component-library/atoms/misc/Badge'; +import { cn } from '@/component-library/utils'; + +const stateString: Record = { + Available: 'Available', + Bad: 'Bad', + Being_Deleted: 'Being Deleted', + Copying: 'Copying', + Temporary_Unavailable: 'Temporary Unavailable', + Unavailable: 'Unavailable', + Unknown: 'Unknown', +}; + +const stateColorClasses: Record = { + Available: 'bg-base-success-500', + Bad: 'bg-base-error-500', + Being_Deleted: 'bg-base-error-300', + Copying: 'bg-base-info-500', + Temporary_Unavailable: 'bg-base-warning-400', + Unavailable: 'bg-neutral-0 dark:bg-neutral-800', + Unknown: 'bg-neutral-0 dark:bg-neutral-800', +}; + +export const ReplicaStateBadge = (props: { value: ReplicaState; className?: string }) => { + const classes = cn(stateColorClasses[props.value], props.className); + return ; +}; diff --git a/src/component-library/features/badges/NullBadge.tsx b/src/component-library/features/badges/NullBadge.tsx new file mode 100644 index 000000000..398ffe90d --- /dev/null +++ b/src/component-library/features/badges/NullBadge.tsx @@ -0,0 +1,8 @@ +import { cn } from '@/component-library/utils'; +import { Badge } from '@/component-library/atoms/misc/Badge'; +import React from 'react'; + +export const NullBadge = (props: { className?: string }) => { + const classes = cn('bg-neutral-200 text-neutral-400 dark:bg-neutral-800 dark:text-neutral-600', props.className); + return ; +}; diff --git a/src/component-library/features/badges/RSE/RSEAvailabilityBadge.tsx b/src/component-library/features/badges/RSE/RSEAvailabilityBadge.tsx new file mode 100644 index 000000000..c645a567d --- /dev/null +++ b/src/component-library/features/badges/RSE/RSEAvailabilityBadge.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { Badge } from '@/component-library/atoms/misc/Badge'; +import { cn } from '@/component-library/utils'; + +export const RSEAvailabilityBadge = (props: { operation: string; className?: string }) => { + const classes = cn('bg-neutral-200 dark:bg-neutral-800', props.className); + return ; +}; diff --git a/src/component-library/features/badges/Rule/RuleGroupingBadge.tsx b/src/component-library/features/badges/Rule/RuleGroupingBadge.tsx new file mode 100644 index 000000000..d7b6256bc --- /dev/null +++ b/src/component-library/features/badges/Rule/RuleGroupingBadge.tsx @@ -0,0 +1,21 @@ +import { RuleGrouping } from '@/lib/core/entity/rucio'; +import React from 'react'; +import { Badge } from '@/component-library/atoms/misc/Badge'; +import { cn } from '@/component-library/utils'; + +const groupingString: Record = { + A: 'All', + D: 'Dataset', + N: 'None', +}; + +const groupingColorClasses: Record = { + A: 'bg-base-success-500', + D: 'bg-base-warning-400', + N: 'bg-base-error-500', +}; + +export const RuleGroupingBadge = (props: { value: RuleGrouping; className?: string }) => { + const classes = cn(groupingColorClasses[props.value], props.className); + return ; +}; diff --git a/src/component-library/features/badges/Rule/RuleNotificationBadge.tsx b/src/component-library/features/badges/Rule/RuleNotificationBadge.tsx new file mode 100644 index 000000000..4852abdfe --- /dev/null +++ b/src/component-library/features/badges/Rule/RuleNotificationBadge.tsx @@ -0,0 +1,23 @@ +import { RuleNotification } from '@/lib/core/entity/rucio'; +import React from 'react'; +import { Badge } from '@/component-library/atoms/misc/Badge'; +import { cn } from '@/component-library/utils'; + +const notificationString: Record = { + C: 'Close', + N: 'No', + P: 'Progress', + Y: 'Yes', +}; + +const notificationColorClasses: Record = { + Y: 'bg-base-success-500', + P: 'bg-base-info-500', + C: 'bg-base-warning-400', + N: 'bg-base-error-500', +}; + +export const RuleNotificationBadge = (props: { value: RuleNotification; className?: string }) => { + const classes = cn(notificationColorClasses[props.value], props.className); + return ; +}; diff --git a/src/component-library/features/key-value/KeyValueRow.tsx b/src/component-library/features/key-value/KeyValueRow.tsx index 4011b1d75..6eb8f59d2 100644 --- a/src/component-library/features/key-value/KeyValueRow.tsx +++ b/src/component-library/features/key-value/KeyValueRow.tsx @@ -5,7 +5,7 @@ export const KeyValueRow = (props: { name: string; children: ReactNode }) => { return (
{props.name} - {props.children} + {props.children}
); }; diff --git a/src/component-library/features/table/cells/AttributeCell.tsx b/src/component-library/features/table/cells/AttributeCell.tsx new file mode 100644 index 000000000..d80870f8d --- /dev/null +++ b/src/component-library/features/table/cells/AttributeCell.tsx @@ -0,0 +1,23 @@ +import { DateISO } from '@/lib/core/entity/rucio'; +import Checkbox from '@/component-library/atoms/form/Checkbox'; +import React from 'react'; +import { formatDate } from '@/component-library/features/utils/text-formatters'; +import { NullBadge } from '@/component-library/features/badges/NullBadge'; + +const isDateISO = (value: unknown): value is DateISO => { + if (typeof value !== 'string') return false; + const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|([+-]\d{2}:\d{2}))$/; + return isoDateRegex.test(value); +}; + +export const AttributeCell = ({ value }: { value: string | DateISO | number | boolean | null }) => { + if (value === null) { + return ; + } else if (typeof value === 'boolean') { + return ; + } else if (isDateISO(value)) { + return formatDate(value); + } else { + return value; + } +}; diff --git a/src/component-library/features/tabs/TabSwitcher.stories.tsx b/src/component-library/features/tabs/TabSwitcher.stories.tsx new file mode 100644 index 000000000..2bc165097 --- /dev/null +++ b/src/component-library/features/tabs/TabSwitcher.stories.tsx @@ -0,0 +1,30 @@ +import { StoryFn, Meta } from '@storybook/react'; +import { TabSwitcher } from '@/component-library/features/tabs/TabSwitcher'; +import { useState } from 'react'; + +export default { + title: 'Components/Tabs', + component: TabSwitcher, + parameters: { + docs: { disable: true }, + }, +} as Meta; + +type TemplateArgs = { + tabs: string[]; +}; + +const Template: StoryFn = ({ tabs }) => { + const [activeIndex, setActiveIndex] = useState(0); + return ; +}; + +export const DefaultSwitcher = Template.bind({}); +DefaultSwitcher.args = { + tabs: ['Tab 1', 'Tab 2', 'Tab 3'], +}; + +export const ExtendedSwitcher = Template.bind({}); +ExtendedSwitcher.args = { + tabs: ['Hello', 'World', 'Hi', 'Earth'], +}; diff --git a/src/component-library/features/tabs/TabSwitcher.tsx b/src/component-library/features/tabs/TabSwitcher.tsx new file mode 100644 index 000000000..ed44d94d0 --- /dev/null +++ b/src/component-library/features/tabs/TabSwitcher.tsx @@ -0,0 +1,32 @@ +import { KeyValueWrapper } from '@/component-library/features/key-value/KeyValueWrapper'; +import { cn } from '@/component-library/utils'; + +type TabSwitcherProps = { + tabNames: string[]; + onSwitch: (index: number) => void; + activeIndex: number; +}; + +export const TabSwitcher = ({ tabNames, onSwitch, activeIndex }: TabSwitcherProps) => { + const regularTabClasses = 'cursor-pointer flex flex-1 items-center justify-center py-1 px-2 text-neutral-600 dark:text-neutral-400'; + const activeTabClasses = cn( + 'rounded', + 'text-neutral-900 dark:text-neutral-100 whitespace-nowrap', + 'bg-neutral-200 dark:bg-neutral-700', + 'border border-neutral-900 dark:border-neutral-100 border-opacity-10 dark:border-opacity-10', + ); + + return ( + + {tabNames.map((name, index) => { + const isActive = index === activeIndex; + const tabClasses = cn(regularTabClasses, { [activeTabClasses]: isActive }); + return ( +
onSwitch(index)} key={name} className={tabClasses}> + {name} +
+ ); + })} +
+ ); +}; diff --git a/src/component-library/outputtailwind.css b/src/component-library/outputtailwind.css index fffe648bc..1138a49e0 100644 --- a/src/component-library/outputtailwind.css +++ b/src/component-library/outputtailwind.css @@ -941,6 +941,10 @@ html { min-height: 300px; } +.min-h-\[450px\] { + min-height: 450px; +} + .min-h-screen { min-height: 100vh; } @@ -1238,6 +1242,11 @@ html { gap: 1rem; } +.gap-x-2 { + -moz-column-gap: 0.5rem; + column-gap: 0.5rem; +} + .gap-y-2 { row-gap: 0.5rem; } @@ -1340,6 +1349,10 @@ html { overflow-y: auto; } +.overflow-y-hidden { + overflow-y: hidden; +} + .overflow-y-visible { overflow-y: visible; } @@ -2253,6 +2266,11 @@ html { color: rgb(241 245 249 / var(--tw-text-opacity)); } +.text-neutral-400 { + --tw-text-opacity: 1; + color: rgb(148 163 184 / var(--tw-text-opacity)); +} + .text-neutral-500 { --tw-text-opacity: 1; color: rgb(100 116 139 / var(--tw-text-opacity)); @@ -3295,6 +3313,11 @@ html { color: rgb(148 163 184 / var(--tw-text-opacity)); } +.dark .dark\:text-neutral-600 { + --tw-text-opacity: 1; + color: rgb(71 85 105 / var(--tw-text-opacity)); +} + .dark .dark\:text-red-400 { --tw-text-opacity: 1; color: rgb(248 113 113 / var(--tw-text-opacity)); @@ -3548,6 +3571,10 @@ html { display: inline; } + .sm\:flex { + display: flex; + } + .sm\:hidden { display: none; } @@ -3807,6 +3834,10 @@ html { } @media (min-width: 1024px) { + .lg\:hidden { + display: none; + } + .lg\:w-32 { width: 8rem; } @@ -3823,6 +3854,14 @@ html { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .lg\:flex-row { + flex-direction: row; + } + + .lg\:justify-between { + justify-content: space-between; + } + .lg\:gap-x-2 { -moz-column-gap: 0.5rem; column-gap: 0.5rem; @@ -3863,7 +3902,15 @@ html { } @media (min-width: 1536px) { + .\32xl\:hidden { + display: none; + } + .\32xl\:w-44 { width: 11rem; } + + .\32xl\:flex-row { + flex-direction: row; + } } diff --git a/src/component-library/pages/DID/details/DetailsDID.tsx b/src/component-library/pages/DID/details/DetailsDID.tsx new file mode 100644 index 000000000..34dea16f5 --- /dev/null +++ b/src/component-library/pages/DID/details/DetailsDID.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { Heading } from '@/component-library/atoms/misc/Heading'; +import { useQuery } from '@tanstack/react-query'; +import { useToast } from '@/lib/infrastructure/hooks/useToast'; +import { BaseViewModelValidator } from '@/component-library/features/utils/BaseViewModelValidator'; +import { DIDMetaViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { LoadingSpinner } from '@/component-library/atoms/loading/LoadingSpinner'; +import { TabSwitcher } from '@/component-library/features/tabs/TabSwitcher'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import { DetailsDIDAttributes } from '@/component-library/pages/DID/details/views/DetailsDIDAttributes'; +import { DetailsDIDFileReplicas } from '@/component-library/pages/DID/details/views/DetailsDIDFileReplicas'; +import { useState } from 'react'; +import { DetailsDIDMeta } from '@/component-library/pages/DID/details/DetailsDIDMeta'; +import { DIDType } from '@/lib/core/entity/rucio'; +import { DetailsDIDRules } from '@/component-library/pages/DID/details/views/DetailsDIDRules'; +import { cn } from '@/component-library/utils'; +import { DetailsDIDParents } from '@/component-library/pages/DID/details/views/DetailsDIDParents'; +import { DetailsDIDContents } from '@/component-library/pages/DID/details/views/DetailsDIDContents'; +import { DetailsDIDContentsReplicas } from '@/component-library/pages/DID/details/views/DetailsDIDContentsReplicas'; +import { WarningField } from '@/component-library/features/fields/WarningField'; + +type DetailsDIDTablesProps = { + scope: string; + name: string; + type: DIDType; +}; + +export const DetailsDIDTables = ({ scope, name, type }: DetailsDIDTablesProps) => { + const allTabs: Map = new Map([ + ['Attributes', DetailsDIDAttributes], + ['Replicas', DetailsDIDFileReplicas], + ['Rules', DetailsDIDRules], + ['Parents', DetailsDIDParents], + ['Contents', DetailsDIDContents], + ['Contents Replicas', DetailsDIDContentsReplicas], + ]); + + const tabsByType: Record = { + File: ['Replicas', 'Parents', 'Attributes'], + Dataset: ['Rules', 'Replicas', 'Contents Replicas', 'Attributes'], + Container: ['Contents', 'Rules', 'Attributes'], + All: [], + Collection: [], + Derived: [], + Unknown: [], + }; + + const tabNames = tabsByType[type]; + const [activeIndex, setActiveIndex] = useState(0); + + if (tabNames.length === 0) { + return ( + + Unsupported type of the DID. + + ); + } + + return ( + <> + + {tabNames.map((tabName, index) => { + const ViewComponent = allTabs.get(tabName); + const visibilityClass = index === activeIndex ? 'flex' : 'hidden'; + + if (ViewComponent === undefined) return; + + const viewClasses = cn('flex-col grow min-h-[450px]', visibilityClass); + + return ( +
+ +
+ ); + })} + + ); +}; + +export const DetailsDID = ({ scope, name }: DetailsDIDProps) => { + const { toast } = useToast(); + const validator = new BaseViewModelValidator(toast); + + const queryMeta = async () => { + const url = '/api/feature/get-did-meta?' + new URLSearchParams({ scope, name }); + + const res = await fetch(url); + if (!res.ok) { + try { + const json = await res.json(); + toast({ + title: 'Fatal error', + description: json.message, + variant: 'error', + }); + } catch (e) {} + throw new Error(res.statusText); + } + + const json = await res.json(); + if (validator.isValid(json)) return json; + + return null; + }; + + const metaQueryKey = ['meta']; + const { + data: meta, + error: metaError, + isFetching: isMetaFetching, + } = useQuery({ + queryKey: metaQueryKey, + queryFn: queryMeta, + retry: false, + refetchOnWindowFocus: false, + }); + + if (metaError) { + return ( + + + Could not load the DID {scope}:{name}. + + + ); + } + + const isLoading = isMetaFetching || meta === undefined; + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+ +
+ + +
+ ); +}; diff --git a/src/component-library/pages/DID/details/DetailsDIDMeta.tsx b/src/component-library/pages/DID/details/DetailsDIDMeta.tsx new file mode 100644 index 000000000..8d8dc5ebf --- /dev/null +++ b/src/component-library/pages/DID/details/DetailsDIDMeta.tsx @@ -0,0 +1,92 @@ +import { DIDMetaViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { Divider } from '@/component-library/atoms/misc/Divider'; +import { KeyValueRow } from '@/component-library/features/key-value/KeyValueRow'; +import { Field } from '@/component-library/atoms/misc/Field'; +import { formatDate, formatFileSize } from '@/component-library/features/utils/text-formatters'; +import { CopyableField } from '@/component-library/features/fields/CopyableField'; +import { KeyValueWrapper } from '@/component-library/features/key-value/KeyValueWrapper'; +import { DIDTypeBadge } from '@/component-library/features/badges/DID/DIDTypeBadge'; +import { DIDAvailabilityBadge } from '@/component-library/features/badges/DID/DIDAvailabilityBadge'; +import Checkbox from '@/component-library/atoms/form/Checkbox'; +import { DIDType } from '@/lib/core/entity/rucio'; + +export const DetailsDIDMeta = ({ meta }: { meta: DIDMetaViewModel }) => { + const getFileInformation = () => { + return ( +
+ + {meta.bytes && ( + + {formatFileSize(meta.bytes)} + + )} + {meta.guid && ( + + + + )} + {meta.adler32 && ( + + + + )} + {meta.md5 && ( + + + + )} +
+ ); + }; + + return ( + +
+
+ + + + + {meta.account} + + + {formatDate(meta.created_at)} + + + {formatDate(meta.updated_at)} + + + + + {meta.is_open !== null && ( + + + + )} +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ + {meta.did_type === DIDType.FILE && getFileInformation()} +
+ ); +}; diff --git a/src/component-library/pages/DID/details/tables/DetailsDIDFileReplicasTable.tsx b/src/component-library/pages/DID/details/tables/DetailsDIDFileReplicasTable.tsx new file mode 100644 index 000000000..ce169699f --- /dev/null +++ b/src/component-library/pages/DID/details/tables/DetailsDIDFileReplicasTable.tsx @@ -0,0 +1,52 @@ +import { UseStreamReader } from '@/lib/infrastructure/hooks/useStreamReader'; +import { FileReplicaStateViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { GridReadyEvent } from 'ag-grid-community'; +import { ClickableCell } from '@/component-library/features/table/cells/ClickableCell'; +import React, { useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { buildDiscreteFilterParams, DefaultTextFilterParams } from '@/component-library/features/utils/filter-parameters'; +import { badgeCellClasses, badgeCellWrapperStyle } from '@/component-library/features/table/cells/badge-cell'; +import { ReplicaStateBadge } from '@/component-library/features/badges/DID/ReplicaStateBadge'; +import { ReplicaState } from '@/lib/core/entity/rucio'; +import { StreamedTable } from '@/component-library/features/table/StreamedTable/StreamedTable'; + +type DetailsDIDFileReplicasTableProps = { + streamingHook: UseStreamReader; + onGridReady: (event: GridReadyEvent) => void; +}; + +const ClickableRSE = (props: { value: string }) => { + return {props.value}; +}; + +export const DetailsDIDFileReplicasTable = (props: DetailsDIDFileReplicasTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'RSE', + field: 'rse', + flex: 1, + sortable: false, + cellRenderer: ClickableRSE, + filter: true, + filterParams: DefaultTextFilterParams, + }, + { + headerName: 'State', + field: 'state', + flex: 1, + cellStyle: badgeCellWrapperStyle, + cellRenderer: ReplicaStateBadge, + cellRendererParams: { + className: badgeCellClasses, + }, + filter: true, + sortable: false, + // TODO: fix the string values + filterParams: buildDiscreteFilterParams(Object.values(ReplicaState)), + }, + ]); + + return ; +}; diff --git a/src/component-library/pages/DID/details/tables/DetailsDIDSimpleTable.tsx b/src/component-library/pages/DID/details/tables/DetailsDIDSimpleTable.tsx new file mode 100644 index 000000000..0332bbf66 --- /dev/null +++ b/src/component-library/pages/DID/details/tables/DetailsDIDSimpleTable.tsx @@ -0,0 +1,57 @@ +import { UseStreamReader } from '@/lib/infrastructure/hooks/useStreamReader'; +import { DIDViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { GridReadyEvent, SelectionChangedEvent, ValueGetterParams } from 'ag-grid-community'; +import { ClickableCell } from '@/component-library/features/table/cells/ClickableCell'; +import React, { useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { ListDIDsViewModel } from '@/lib/infrastructure/data/view-model/list-did'; +import { DIDTypeBadge } from '@/component-library/features/badges/DID/DIDTypeBadge'; +import { badgeCellClasses, badgeCellWrapperStyle } from '@/component-library/features/table/cells/badge-cell'; +import { StreamedTable } from '@/component-library/features/table/StreamedTable/StreamedTable'; + +type DetailsDIDSimpleTableProps = { + streamingHook: UseStreamReader; + onSelectionChanged?: (event: SelectionChangedEvent) => void; + onGridReady: (event: GridReadyEvent) => void; +}; + +const ClickableDID = (props: { value: string[] }) => { + const [scope, name] = props.value; + return ( + + {scope}:{name} + + ); +}; + +export const DetailsDIDSimpleTable = (props: DetailsDIDSimpleTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'Identifier', + flex: 1, + valueGetter: (params: ValueGetterParams) => { + if (props.onSelectionChanged) return `${params.data?.scope}:${params.data?.name}`; + return [params.data?.scope, params.data?.name]; + }, + cellRenderer: props.onSelectionChanged ? undefined : ClickableDID, + minWidth: 250, + sortable: false, + }, + { + headerName: 'Type', + field: 'did_type', + cellRenderer: DIDTypeBadge, + minWidth: 180, + maxWidth: 180, + cellStyle: badgeCellWrapperStyle, + cellRendererParams: { + className: badgeCellClasses, + }, + sortable: false, + }, + ]); + + return ; +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDAttributes.tsx b/src/component-library/pages/DID/details/views/DetailsDIDAttributes.tsx new file mode 100644 index 000000000..0d3914a15 --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDAttributes.tsx @@ -0,0 +1,92 @@ +import React, { useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { RegularTable } from '@/component-library/features/table/RegularTable/RegularTable'; +import { DIDKeyValuePair } from '@/lib/core/entity/rucio'; +import { DefaultTextFilterParams } from '@/component-library/features/utils/filter-parameters'; +import { AttributeCell } from '@/component-library/features/table/cells/AttributeCell'; +import { DIDKeyValuePairsDataViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { useQuery } from '@tanstack/react-query'; +import { useToast } from '@/lib/infrastructure/hooks/useToast'; +import { BaseViewModelValidator } from '@/component-library/features/utils/BaseViewModelValidator'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import { WarningField } from '@/component-library/features/fields/WarningField'; +import { LoadingSpinner } from '@/component-library/atoms/loading/LoadingSpinner'; + +type DetailsDIDAttributesTableProps = { + viewModel: DIDKeyValuePairsDataViewModel; +}; + +export const DetailsDIDAttributesTable = (props: DetailsDIDAttributesTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'Key', + field: 'key', + flex: 1, + sortable: true, + filter: true, + filterParams: DefaultTextFilterParams, + }, + { + headerName: 'Value', + field: 'value', + cellRenderer: AttributeCell, + flex: 1, + sortable: false, + }, + ]); + + return ; +}; + +export const DetailsDIDAttributes: DetailsDIDView = ({ scope, name }: DetailsDIDProps) => { + const { toast } = useToast(); + const validator = new BaseViewModelValidator(toast); + + const queryKeyValuePairs = async () => { + const url = '/api/feature/get-did-keyvaluepairs?' + new URLSearchParams({ scope, name }); + + const res = await fetch(url); + if (!res.ok) { + const json = await res.json(); + throw new Error(json.message); + } + + const json = await res.json(); + if (validator.isValid(json)) return json; + + return null; + }; + + const keyValuePairsQueryKey = ['key-value-pairs']; + const { + data: keyValuePairs, + error: keyValuePairsError, + isFetching: areKeyValuePairsFetching, + } = useQuery({ + queryKey: keyValuePairsQueryKey, + queryFn: queryKeyValuePairs, + retry: false, + refetchOnWindowFocus: false, + }); + + if (keyValuePairsError) { + return ( + + Could not load DID attributes. + + ); + } + + const isLoading = keyValuePairs === undefined || areKeyValuePairsFetching; + if (isLoading) { + return ( +
+ +
+ ); + } + + return ; +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDContents.tsx b/src/component-library/pages/DID/details/views/DetailsDIDContents.tsx new file mode 100644 index 000000000..1984b5bf5 --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDContents.tsx @@ -0,0 +1,18 @@ +import { DIDViewModel } from '@/lib/infrastructure/data/view-model/did'; +import React, { useEffect } from 'react'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import useTableStreaming from '@/lib/infrastructure/hooks/useTableStreaming'; +import { DetailsDIDSimpleTable } from '@/component-library/pages/DID/details/tables/DetailsDIDSimpleTable'; + +export const DetailsDIDContents: DetailsDIDView = ({ scope, name }: DetailsDIDProps) => { + const { gridApi, onGridReady, streamingHook, startStreaming, stopStreaming } = useTableStreaming(); + + useEffect(() => { + if (gridApi) { + const url = '/api/feature/list-did-contents?' + new URLSearchParams({ scope, name }); + startStreaming(url); + } + }, [gridApi]); + + return ; +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDContentsReplicas.tsx b/src/component-library/pages/DID/details/views/DetailsDIDContentsReplicas.tsx new file mode 100644 index 000000000..554ecf8d3 --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDContentsReplicas.tsx @@ -0,0 +1,69 @@ +import { DIDViewModel, FileReplicaStateViewModel } from '@/lib/infrastructure/data/view-model/did'; +import React, { useEffect, useState } from 'react'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import useTableStreaming from '@/lib/infrastructure/hooks/useTableStreaming'; +import { DetailsDIDSimpleTable } from '@/component-library/pages/DID/details/tables/DetailsDIDSimpleTable'; +import { SelectionChangedEvent } from 'ag-grid-community'; +import { DetailsDIDFileReplicasTable } from '@/component-library/pages/DID/details/tables/DetailsDIDFileReplicasTable'; + +export const DetailsDIDContentsReplicas: DetailsDIDView = ({ scope, name }: DetailsDIDProps) => { + const { + gridApi: contentsGridApi, + onGridReady: onContentsGridReady, + streamingHook: contentsStreamingHook, + startStreaming: startContentsStreaming, + } = useTableStreaming(); + + useEffect(() => { + if (contentsGridApi) { + const url = '/api/feature/list-did-contents?' + new URLSearchParams({ scope, name }); + startContentsStreaming(url); + } + }, [contentsGridApi]); + + const [selectedContent, setSelectedContent] = useState(null); + + const onSelectionChanged = (event: SelectionChangedEvent) => { + const selectedRows = event.api.getSelectedRows(); + if (selectedRows.length === 1) { + setSelectedContent(selectedRows[0] as DIDViewModel); + } else { + setSelectedContent(null); + } + }; + + const { + onGridReady: onReplicasGridReady, + streamingHook: replicasStreamingHook, + startStreaming: startReplicasStreaming, + } = useTableStreaming(); + + // TODO: handle continuing streaming + + useEffect(() => { + if (selectedContent) { + const url = + '/api/feature/list-file-replicas?' + + new URLSearchParams({ + scope: selectedContent.scope, + name: setSelectedContent.name, + }); + startReplicasStreaming(url); + } + }, [selectedContent]); + + return ( +
+
+ +
+
+ +
+
+ ); +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDFileReplicas.tsx b/src/component-library/pages/DID/details/views/DetailsDIDFileReplicas.tsx new file mode 100644 index 000000000..3024b75c5 --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDFileReplicas.tsx @@ -0,0 +1,18 @@ +import React, { useEffect } from 'react'; +import { FileReplicaStateViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import useTableStreaming from '@/lib/infrastructure/hooks/useTableStreaming'; +import { DetailsDIDFileReplicasTable } from '@/component-library/pages/DID/details/tables/DetailsDIDFileReplicasTable'; + +export const DetailsDIDFileReplicas: DetailsDIDView = ({ scope, name }: DetailsDIDProps) => { + const { gridApi, onGridReady, streamingHook, startStreaming, stopStreaming } = useTableStreaming(); + + useEffect(() => { + if (gridApi) { + const url = '/api/feature/list-file-replicas?' + new URLSearchParams({ scope, name }); + startStreaming(url); + } + }, [gridApi]); + + return ; +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDParents.tsx b/src/component-library/pages/DID/details/views/DetailsDIDParents.tsx new file mode 100644 index 000000000..46b2353c5 --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDParents.tsx @@ -0,0 +1,18 @@ +import { DIDViewModel } from '@/lib/infrastructure/data/view-model/did'; +import React, { useEffect } from 'react'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import useTableStreaming from '@/lib/infrastructure/hooks/useTableStreaming'; +import { DetailsDIDSimpleTable } from '@/component-library/pages/DID/details/tables/DetailsDIDSimpleTable'; + +export const DetailsDIDParents: DetailsDIDView = ({ scope, name }: DetailsDIDProps) => { + const { gridApi, onGridReady, streamingHook, startStreaming, stopStreaming } = useTableStreaming(); + + useEffect(() => { + if (gridApi) { + const url = '/api/feature/list-did-parents?' + new URLSearchParams({ scope, name }); + startStreaming(url); + } + }, [gridApi]); + + return ; +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDRules.tsx b/src/component-library/pages/DID/details/views/DetailsDIDRules.tsx new file mode 100644 index 000000000..541d42bd1 --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDRules.tsx @@ -0,0 +1,123 @@ +import { UseStreamReader } from '@/lib/infrastructure/hooks/useStreamReader'; +import { DIDRulesViewModel } from '@/lib/infrastructure/data/view-model/did'; +import { GridReadyEvent, ValueFormatterParams } from 'ag-grid-community'; +import { ClickableCell } from '@/component-library/features/table/cells/ClickableCell'; +import React, { useEffect, useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { buildDiscreteFilterParams, DefaultDateFilterParams, DefaultTextFilterParams } from '@/component-library/features/utils/filter-parameters'; +import { badgeCellClasses, badgeCellWrapperStyle } from '@/component-library/features/table/cells/badge-cell'; +import { RuleState } from '@/lib/core/entity/rucio'; +import { StreamedTable } from '@/component-library/features/table/StreamedTable/StreamedTable'; +import { DetailsDIDView, DetailsDIDProps } from '@/component-library/pages/DID/details/views/DetailsDIDView'; +import useTableStreaming from '@/lib/infrastructure/hooks/useTableStreaming'; +import { formatDate, formatSeconds } from '@/component-library/features/utils/text-formatters'; +import { RuleStateBadge } from '@/component-library/features/badges/Rule/RuleStateBadge'; + +type DetailsDIDRulesTableProps = { + streamingHook: UseStreamReader; + onGridReady: (event: GridReadyEvent) => void; +}; + +const ClickableRSE = (props: { value: string }) => { + return {props.value}; +}; + +const ClickableId = (props: { value: string }) => { + return {props.value}; +}; + +export const DetailsDIDRulesTable = (props: DetailsDIDRulesTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'ID', + field: 'id', + minWidth: 390, + maxWidth: 390, + sortable: false, + cellRenderer: ClickableId, + }, + { + headerName: 'RSE', + field: 'rse_expression', + minWidth: 150, + flex: 1, + filter: true, + filterParams: DefaultTextFilterParams, + cellRenderer: ClickableRSE, + }, + { + headerName: 'Created At', + field: 'created_at', + minWidth: 150, + maxWidth: 150, + valueFormatter: (params: ValueFormatterParams) => { + return formatDate(params.value); + }, + filter: 'agDateColumnFilter', + filterParams: DefaultDateFilterParams, + }, + { + headerName: 'Remaining', + field: 'remaining_lifetime', + minWidth: 125, + maxWidth: 125, + valueFormatter: (params: ValueFormatterParams) => { + return formatSeconds(params.value); + }, + }, + { + headerName: 'State', + field: 'state', + minWidth: 200, + maxWidth: 200, + cellStyle: badgeCellWrapperStyle, + cellRenderer: RuleStateBadge, + cellRendererParams: { + className: badgeCellClasses, + }, + filter: true, + sortable: false, + // TODO: fix the string values + filterParams: buildDiscreteFilterParams(Object.values(RuleState)), + }, + // TODO: minified header with a tooltip + { + headerName: 'OK', + field: 'locks_ok_cnt', + minWidth: 75, + maxWidth: 75, + sortable: false, + }, + { + headerName: 'Replicating', + field: 'locks_replicating_cnt', + minWidth: 135, + maxWidth: 135, + sortable: false, + }, + { + headerName: 'Stuck', + field: 'locks_stuck_cnt', + minWidth: 90, + maxWidth: 90, + sortable: false, + }, + ]); + + return ; +}; + +export const DetailsDIDRules: DetailsDIDView = ({ scope, name }: DetailsDIDProps) => { + const { gridApi, onGridReady, streamingHook, startStreaming, stopStreaming } = useTableStreaming(); + + useEffect(() => { + if (gridApi) { + const url = '/api/feature/list-did-rules?' + new URLSearchParams({ scope, name }); + startStreaming(url); + } + }, [gridApi]); + + return ; +}; diff --git a/src/component-library/pages/DID/details/views/DetailsDIDView.ts b/src/component-library/pages/DID/details/views/DetailsDIDView.ts new file mode 100644 index 000000000..183d2e38b --- /dev/null +++ b/src/component-library/pages/DID/details/views/DetailsDIDView.ts @@ -0,0 +1,6 @@ +export type DetailsDIDProps = { + scope: string; + name: string; +}; + +export type DetailsDIDView = (props: DetailsDIDProps) => JSX.Element; diff --git a/src/component-library/pages/RSE/details/DetailsRSE.tsx b/src/component-library/pages/RSE/details/DetailsRSE.tsx new file mode 100644 index 000000000..f399ac1e2 --- /dev/null +++ b/src/component-library/pages/RSE/details/DetailsRSE.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { Heading } from '@/component-library/atoms/misc/Heading'; +import { RSEAttributeViewModel, RSEDetailsViewModel } from '@/lib/infrastructure/data/view-model/rse'; +import { useQuery } from '@tanstack/react-query'; +import { useToast } from '@/lib/infrastructure/hooks/useToast'; +import { BaseViewModelValidator } from '@/component-library/features/utils/BaseViewModelValidator'; +import { LoadingSpinner } from '@/component-library/atoms/loading/LoadingSpinner'; +import { KeyValueWrapper } from '@/component-library/features/key-value/KeyValueWrapper'; +import { KeyValueRow } from '@/component-library/features/key-value/KeyValueRow'; +import { RSETypeBadge } from '@/component-library/features/badges/RSE/RSETypeBadge'; +import Checkbox from '@/component-library/atoms/form/Checkbox'; +import { RSEAvailabilityBadge } from '@/component-library/features/badges/RSE/RSEAvailabilityBadge'; +import { DetailsRSEProtocolsTable } from '@/component-library/pages/RSE/details/DetailsRSEProtocolsTable'; +import { DetailsRSEAttributesTable } from '@/component-library/pages/RSE/details/DetailsRSEAttributesTable'; +import { WarningField } from '@/component-library/features/fields/WarningField'; +import { RSEDetailsProtocol } from '@/lib/core/entity/rucio'; +import { InfoField } from '@/component-library/features/fields/InfoField'; + +const DetailsRSEKeyValues = ({ meta }: { meta: RSEDetailsViewModel }) => { + return ( + +
+ + + + + {meta.availability_read && } + {meta.availability_write && } + {meta.availability_delete && } + + + + +
+
+ + + + + + +
+
+ ); +}; + +const DetailsRSEAttributes = ({ attributes }: { attributes: RSEAttributeViewModel }) => { + return attributes.attributes.length !== 0 ? ( + <> + + + + ) : ( + + No attributes found. + + ); +}; + +const DetailsRSEProtocols = ({ protocols }: { protocols: RSEDetailsProtocol[] }) => { + return protocols.length !== 0 ? ( + <> + + + + ) : ( + + No protocols found. + + ); +}; + +type DetailsRSEProps = { + name: string; +}; + +export const DetailsRSE = (props: DetailsRSEProps) => { + const { toast } = useToast(); + const validator = new BaseViewModelValidator(toast); + + const queryMeta = async () => { + const url = '/api/feature/get-rse?' + new URLSearchParams({ rseName: props.name }); + + const res = await fetch(url); + if (!res.ok) { + try { + const json = await res.json(); + toast({ + title: 'Fatal error', + description: json.message, + variant: 'error', + }); + } catch (e) {} + throw new Error(res.statusText); + } + + const json = await res.json(); + if (validator.isValid(json)) return json; + + return null; + }; + + const metaQueryKey = ['meta']; + const { + data: meta, + error: metaError, + isFetching: isMetaFetching, + } = useQuery({ + queryKey: metaQueryKey, + queryFn: queryMeta, + retry: false, + refetchOnWindowFocus: false, + }); + + const queryAttributes = async () => { + const url = '/api/feature/get-rse-attributes?' + new URLSearchParams({ rseName: props.name }); + + const res = await fetch(url); + if (!res.ok) throw new Error(res.statusText); + + const json = await res.json(); + if (validator.isValid(json)) return json; + + return null; + }; + + const attributesQueryKey = ['attributes']; + const { + data: attributes, + error: attributesError, + isFetching: isAttributesFetching, + } = useQuery({ + queryKey: attributesQueryKey, + queryFn: queryAttributes, + retry: false, + refetchOnWindowFocus: false, + }); + + const hasError = metaError || attributesError; + if (hasError) { + return ( + + Could not load the RSE {props.name}. + + ); + } + + const isLoading = isMetaFetching || isAttributesFetching || attributes === undefined || meta === undefined; + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + +
+ ); +}; diff --git a/src/component-library/pages/RSE/details/DetailsRSEAttributesTable.tsx b/src/component-library/pages/RSE/details/DetailsRSEAttributesTable.tsx new file mode 100644 index 000000000..e5272f1ef --- /dev/null +++ b/src/component-library/pages/RSE/details/DetailsRSEAttributesTable.tsx @@ -0,0 +1,35 @@ +import React, { useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { RegularTable } from '@/component-library/features/table/RegularTable/RegularTable'; +import { RSEAttribute } from '@/lib/core/entity/rucio'; +import { RSEAttributeViewModel } from '@/lib/infrastructure/data/view-model/rse'; +import { DefaultTextFilterParams } from '@/component-library/features/utils/filter-parameters'; +import { AttributeCell } from '@/component-library/features/table/cells/AttributeCell'; + +type DetailsRSEAttributesTableProps = { + viewModel: RSEAttributeViewModel; +}; + +export const DetailsRSEAttributesTable = (props: DetailsRSEAttributesTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'Key', + field: 'key', + flex: 1, + sortable: true, + filter: true, + filterParams: DefaultTextFilterParams, + }, + { + headerName: 'Value', + field: 'value', + cellRenderer: AttributeCell, + flex: 1, + sortable: false, + }, + ]); + + return ; +}; diff --git a/src/component-library/pages/RSE/details/DetailsRSEProtocolsTable.tsx b/src/component-library/pages/RSE/details/DetailsRSEProtocolsTable.tsx new file mode 100644 index 000000000..366e173c7 --- /dev/null +++ b/src/component-library/pages/RSE/details/DetailsRSEProtocolsTable.tsx @@ -0,0 +1,108 @@ +import React, { useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { RegularTable } from '@/component-library/features/table/RegularTable/RegularTable'; +import { RSEDetailsProtocol } from '@/lib/core/entity/rucio'; +import { ValueGetterParams } from 'ag-grid-community'; + +type DetailsRSEProtocolsTableProps = { + rowData: RSEDetailsProtocol[]; +}; + +export const DetailsRSEProtocolsTable = (props: DetailsRSEProtocolsTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'Scheme', + field: 'scheme', + minWidth: 100, + sortable: false, + }, + { + headerName: 'Hostname', + field: 'hostname', + minWidth: 200, + flex: 1, + sortable: false, + }, + { + headerName: 'Port', + field: 'port', + minWidth: 80, + sortable: false, + }, + { + headerName: 'Prefix', + field: 'prefix', + minWidth: 250, + flex: 1, + sortable: false, + }, + { + headerName: 'LAN/R', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.lan.read; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'LAN/W', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.lan.write; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'LAN/D', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.lan.delete; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'WAN/R', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.wan.read; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'WAN/W', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.wan.write; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'WAN/D', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.wan.delete; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'TPC/R', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.wan.third_party_copy_read; + }, + minWidth: 80, + sortable: true, + }, + { + headerName: 'TPC/W', + valueGetter: (params: ValueGetterParams) => { + return params.data?.domains.wan.third_party_copy_write; + }, + minWidth: 80, + sortable: true, + }, + ]); + + return ; +}; diff --git a/src/component-library/pages/Rule/details/DetailsRule.tsx b/src/component-library/pages/Rule/details/DetailsRule.tsx new file mode 100644 index 000000000..1cb1f17ca --- /dev/null +++ b/src/component-library/pages/Rule/details/DetailsRule.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { Heading } from '@/component-library/atoms/misc/Heading'; +import { useQuery } from '@tanstack/react-query'; +import { useToast } from '@/lib/infrastructure/hooks/useToast'; +import { BaseViewModelValidator } from '@/component-library/features/utils/BaseViewModelValidator'; +import { LoadingSpinner } from '@/component-library/atoms/loading/LoadingSpinner'; +import { TabSwitcher } from '@/component-library/features/tabs/TabSwitcher'; +import { useState } from 'react'; +import { WarningField } from '@/component-library/features/fields/WarningField'; +import { RuleMetaViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { cn } from '@/component-library/utils'; +import { DetailsRuleLocks } from '@/component-library/pages/Rule/details/DetailsRuleLocks'; +import { DetailsRuleMeta } from '@/component-library/pages/Rule/details/DetailsRuleMeta'; + +export const DetailsRuleTabs = ({ id, meta }: { id: string; meta: RuleMetaViewModel }) => { + const tabNames = ['Attributes', 'Locks']; + const [activeIndex, setActiveIndex] = useState(0); + + const getViewClasses = (index: number) => { + const visibilityClass = index === activeIndex ? 'flex' : 'hidden'; + return cn('flex-col grow min-h-[450px]', visibilityClass); + }; + + return ( + <> + +
+ +
+
+ +
+ + ); +}; + +export const DetailsRule = ({ id }: { id: string }) => { + const { toast } = useToast(); + const validator = new BaseViewModelValidator(toast); + + const queryMeta = async () => { + const url = '/api/feature/get-rule?' + new URLSearchParams({ id }); + + const res = await fetch(url); + if (!res.ok) { + try { + const json = await res.json(); + toast({ + title: 'Fatal error', + description: json.message, + variant: 'error', + }); + } catch (e) {} + throw new Error(res.statusText); + } + + const json = await res.json(); + if (validator.isValid(json)) return json; + + return null; + }; + + const metaQueryKey = ['rule-meta']; + const { + data: meta, + error: metaError, + isFetching: isMetaFetching, + } = useQuery({ + queryKey: metaQueryKey, + queryFn: queryMeta, + retry: false, + refetchOnWindowFocus: false, + }); + + if (metaError) { + return ( + + Could not load the rule with ID {id}. + + ); + } + + const isLoading = isMetaFetching || meta === undefined; + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+ +
+ +
+ ); +}; diff --git a/src/component-library/pages/Rule/details/DetailsRuleLocks.tsx b/src/component-library/pages/Rule/details/DetailsRuleLocks.tsx new file mode 100644 index 000000000..1e5851f4a --- /dev/null +++ b/src/component-library/pages/Rule/details/DetailsRuleLocks.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { AgGridReact } from 'ag-grid-react'; +import { UseStreamReader } from '@/lib/infrastructure/hooks/useStreamReader'; +import { StreamedTable } from '@/component-library/features/table/StreamedTable/StreamedTable'; +import { badgeCellClasses, badgeCellWrapperStyle } from '@/component-library/features/table/cells/badge-cell'; +import { DefaultTextFilterParams, buildDiscreteFilterParams } from '@/component-library/features/utils/filter-parameters'; +import { GridReadyEvent, ValueGetterParams } from 'ag-grid-community'; +import { ListRuleReplicaLockStatesViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { LockState } from '@/lib/core/entity/rucio'; +import { LockStateBadge } from '@/component-library/features/badges/Rule/LockStateBadge'; +import useTableStreaming from '@/lib/infrastructure/hooks/useTableStreaming'; + +type DetailsRuleLocksTableProps = { + streamingHook: UseStreamReader; + onGridReady: (event: GridReadyEvent) => void; +}; + +const DetailsRuleLocksTable = (props: DetailsRuleLocksTableProps) => { + const tableRef = useRef>(null); + + const [columnDefs] = useState([ + { + headerName: 'DID', + valueGetter: (params: ValueGetterParams) => { + return params.data?.scope + ':' + params.data?.name; + }, + minWidth: 150, + flex: 1, + filter: true, + filterParams: DefaultTextFilterParams, + }, + { + headerName: 'RSE', + field: 'rse', + minWidth: 150, + flex: 1, + filter: true, + filterParams: DefaultTextFilterParams, + }, + { + headerName: 'State', + field: 'state', + minWidth: 200, + maxWidth: 200, + cellStyle: badgeCellWrapperStyle, + cellRenderer: LockStateBadge, + cellRendererParams: { + className: badgeCellClasses, + }, + filter: true, + sortable: false, + // TODO: fix the string values + filterParams: buildDiscreteFilterParams(Object.values(LockState)), + }, + // TODO: add links + ]); + + return ; +}; + +export const DetailsRuleLocks = ({ id }: { id: string }) => { + const { gridApi, onGridReady, streamingHook, startStreaming, stopStreaming } = useTableStreaming(); + + useEffect(() => { + if (gridApi) { + const url = '/api/feature/list-rule-replica-lock-states?' + new URLSearchParams({ id }); + startStreaming(url); + } + }, [gridApi]); + + return ; +}; diff --git a/src/component-library/pages/Rule/details/DetailsRuleMeta.tsx b/src/component-library/pages/Rule/details/DetailsRuleMeta.tsx new file mode 100644 index 000000000..c7fac459e --- /dev/null +++ b/src/component-library/pages/Rule/details/DetailsRuleMeta.tsx @@ -0,0 +1,102 @@ +import { Divider } from '@/component-library/atoms/misc/Divider'; +import { KeyValueRow } from '@/component-library/features/key-value/KeyValueRow'; +import { Field } from '@/component-library/atoms/misc/Field'; +import { formatDate } from '@/component-library/features/utils/text-formatters'; +import { KeyValueWrapper } from '@/component-library/features/key-value/KeyValueWrapper'; +import { DIDTypeBadge } from '@/component-library/features/badges/DID/DIDTypeBadge'; +import Checkbox from '@/component-library/atoms/form/Checkbox'; +import { RuleMetaViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { NullBadge } from '@/component-library/features/badges/NullBadge'; +import { RuleStateBadge } from '@/component-library/features/badges/Rule/RuleStateBadge'; +import { RuleGroupingBadge } from '@/component-library/features/badges/Rule/RuleGroupingBadge'; +import { RuleNotificationBadge } from '@/component-library/features/badges/Rule/RuleNotificationBadge'; + +export const DetailsRuleMeta = ({ meta }: { meta: RuleMetaViewModel }) => { + const getExpiredField = () => { + if (meta.expires_at) { + return formatDate(meta.expires_at); + } else { + return ; + } + }; + + return ( + +
+ + + + + + {meta.scope}:{meta.name} + + + + + {meta.rse_expression} + + + {meta.account} + + + + + + {meta.locks_ok_cnt} + + + {meta.locks_replicating_cnt} + + + {meta.locks_stuck_cnt} + + + + + + {formatDate(meta.created_at)} + + + {formatDate(meta.updated_at)} + + {getExpiredField()} + + + + + {meta.copies} + + + + + + + + + {meta.priority} + + + {meta.activity} + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; diff --git a/src/component-library/pages/legacy/DID/DIDMetaView.tsx b/src/component-library/pages/legacy/DID/DIDMetaView.tsx deleted file mode 100644 index 411e85c90..000000000 --- a/src/component-library/pages/legacy/DID/DIDMetaView.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { twMerge } from 'tailwind-merge'; -import { DIDMetaViewModel } from '@/lib/infrastructure/data/view-model/did'; -import { FileSize } from '@/component-library/atoms/legacy/text/content/FileSize/FileSize'; -import { DIDTypeTag } from '@/component-library/features/legacy/Tags/DIDTypeTag'; -import { BoolTag } from '@/component-library/features/legacy/Tags/BoolTag'; -import { AvailabilityTag } from '@/component-library/features/legacy/Tags/AvailabilityTag'; -var format = require('date-format'); - -export const DIDMetaView = (props: { data: DIDMetaViewModel; show: boolean; horizontal?: boolean }) => { - const Titletd: React.FC = ({ ...props }) => { - const { className, ...otherprops } = props; - return ( - - {props.children} - - ); - }; - const Contenttd: React.FC = ({ ...props }) => { - const { className, ...otherprops } = props; - return ( - - {props.children} - - ); - }; - const meta = props.data; - // suspense: data is not loaded yet - if (props.data.status === 'pending') { - return ( -
- Loading DID Metadata -
- ); - } - // base case: data is loaded - return ( -
- - - - Scope - {meta.scope} - - - Name - {meta.name} - - -
- - - - Size - - - - - - GUID - {meta.guid as string} - - - Adler32 - {meta.adler32 as string} - - - MD5 - {meta.md5 as string} - - -
- - - - Created At - {format('yyyy-MM-dd', new Date(meta.created_at))} - - - Updated At - {format('yyyy-MM-dd', new Date(meta.updated_at))} - - -
- - - - DID Type - - - - Account - {meta.account} - - - Is Open - - - - Monotonic - - - -
- -
- -
- -
- - - - Obsolete - - - - Hidden - - - - Suppressed - - - - Purge Replicas - - - - Availability - - - -
- -
- -
- -
- -
- -
-
- ); -}; diff --git a/src/component-library/pages/legacy/DID/PageDID.stories.tsx b/src/component-library/pages/legacy/DID/PageDID.stories.tsx deleted file mode 100644 index 6d4b77952..000000000 --- a/src/component-library/pages/legacy/DID/PageDID.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { PageDID as PD } from './PageDID'; - -import { - fixtureDIDMetaViewModel, - mockUseComDOM, - fixtureDIDRulesViewModel, - fixtureDIDViewModel, - fixtureDIDDatasetReplicasViewModel, - fixtureFilereplicaStateViewModel, - fixtureFilereplicaStateDViewModel, - fixtureDIDKeyValuePairsDataViewModel, -} from '@/test/fixtures/table-fixtures'; - -export default { - title: 'Components/Pages/DID', - component: PD, -} as Meta; - -const Template: StoryFn = args => ; -export const PageDID = Template.bind({}); -PageDID.args = { - didMeta: fixtureDIDMetaViewModel(), - fromDidList: 'yosearch', - // Parent DIDs [FILE] - didParentsComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDViewModel())), - // DID Metadata - didKeyValuePairsData: fixtureDIDKeyValuePairsDataViewModel(), - // Filereplicas - didFileReplicasComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureFilereplicaStateViewModel())), - didFileReplicasDOnChange: (scope: string, name: string) => { - console.log(scope, name, 'queried by FileReplicasDOnChange'); - }, - didRulesComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDRulesViewModel())), - // Contents - didContentsComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDViewModel())), - didDatasetReplicasComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDDatasetReplicasViewModel())), -}; diff --git a/src/component-library/pages/legacy/DID/PageDID.tsx b/src/component-library/pages/legacy/DID/PageDID.tsx deleted file mode 100644 index d79ca8554..000000000 --- a/src/component-library/pages/legacy/DID/PageDID.tsx +++ /dev/null @@ -1,199 +0,0 @@ -// components -import { Tabs } from '../../../atoms/legacy/Tabs/Tabs'; -import { DIDTypeTag } from '@/component-library/features/legacy/Tags/DIDTypeTag'; -import { SubPage } from '../../../atoms/legacy/helpers/SubPage/SubPage'; -import { Heading } from '@/component-library/pages/legacy/Helpers/Heading'; -import { Body } from '@/component-library/pages/legacy/Helpers/Body'; - -// misc packages, react -import { twMerge } from 'tailwind-merge'; -import React, { useEffect, useState } from 'react'; - -// DTO etc -import { DIDType } from '@/lib/core/entity/rucio'; -import { PageDIDMetadata } from './PageDIDMetadata'; -import { PageDIDFilereplicas } from './PageDIDFileReplicas'; -import { PageDIDFilereplicasD } from './PageDIDFileReplicasD'; -import { PageDIDRules } from './PageDIDRules'; -import { PageDIDByType } from './PageDIDByType'; -import { PageDIDDatasetReplicas } from './PageDIDDatasetReplicas'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { - DIDDatasetReplicasViewModel, - DIDKeyValuePairsDataViewModel, - DIDMetaViewModel, - DIDRulesViewModel, - DIDViewModel, - FilereplicaStateDViewModel, - FileReplicaStateViewModel, -} from '@/lib/infrastructure/data/view-model/did'; -import { HTTPRequest } from '@/lib/sdk/http'; -import { DIDMetaView } from '@/component-library/pages/legacy/DID/DIDMetaView'; -import { InfoField } from '@/component-library/features/fields/InfoField'; - -export interface PageDIDPageProps { - didMeta: DIDMetaViewModel; - fromDidList?: string; // if coming from DIDList, this will be the DIDList's query - // Parent DIDs [FILE] - didParentsComDOM: UseComDOM; - // Metadata [BOTH] - didKeyValuePairsData: DIDKeyValuePairsDataViewModel; - // File Replica States [FILE] - didFileReplicasComDOM: UseComDOM; - // File Replica States [DATASET] simply uses Contents ComDOM - // But we supply an onchange function that is called when the contents comdom is selected - didFileReplicasDOnChange: (scope: string, name: string) => void; // This function changes the file replicas comdom's request to the selected file - // Rule State [DATASET] - didRulesComDOM: UseComDOM; - // Contents [COLLECTION] - didContentsComDOM: UseComDOM; - // Dataset Replica States [DATASET] - didDatasetReplicasComDOM: UseComDOM; -} - -export const PageDID = (props: PageDIDPageProps) => { - const didtype = props.didMeta.did_type; - const [subpageIndex, setSubpageIndex] = useState(0); - const showPageBools: Record boolean> = { - 'subpage-metadata': () => { - if (didtype === DIDType.FILE) { - return subpageIndex === 2; - } else if (didtype === DIDType.DATASET) { - return subpageIndex === 3; - } else if (didtype === DIDType.CONTAINER) { - return subpageIndex === 2; - } else { - return false; - } - }, - 'subpage-contents': () => { - return didtype === DIDType.CONTAINER && subpageIndex === 0; - }, - 'subpage-parent-dids': () => { - return didtype === DIDType.FILE && subpageIndex === 1; - }, - 'subpage-rules': () => { - if (didtype === DIDType.DATASET) { - return subpageIndex === 0; - } else if (didtype === DIDType.CONTAINER) { - return subpageIndex === 1; - } else { - return false; - } - }, - 'subpage-dataset-replicas': () => { - return didtype === DIDType.DATASET && subpageIndex === 1; - }, - 'subpage-file-replica-states': () => { - return didtype === DIDType.FILE && subpageIndex === 0; - }, - 'subpage-file-replica-states-d': () => { - return didtype === DIDType.DATASET && subpageIndex === 2; - }, - }; - return ( -
- - This page is currently in development and has not been optimized yet. We are working on improvements, so stay tuned! - - } - > -
- -
-
- - - { - setSubpageIndex(id); - }} - /> - { - if (props.didRulesComDOM.query.data.all.length === 0) { - props.didRulesComDOM.start(); - } - }} - id="subpage-rules" - > - - - { - if (props.didDatasetReplicasComDOM.query.data.all.length === 0) { - props.didDatasetReplicasComDOM.start(); - } - }} - id="subpage-dataset-replicas" - > - - - { - if (props.didFileReplicasComDOM.query.data.all.length === 0) { - props.didFileReplicasComDOM.start(); - } - }} - id="subpage-file-replica-states" - > - - - { - if (props.didContentsComDOM.query.data.all.length === 0) { - props.didContentsComDOM.start(); - } - }} - id="subpage-file-replica-states-d" - > - - - { - if (props.didParentsComDOM.query.data.all.length === 0) { - props.didParentsComDOM.start(); - } - }} - id="subpage-parent-dids" - > - - - - - - { - if (props.didContentsComDOM.query.data.all.length === 0) { - props.didContentsComDOM.start(); - } - }} - id="subpage-contents" - > - - - -
- ); -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDByType.stories.tsx b/src/component-library/pages/legacy/DID/PageDIDByType.stories.tsx deleted file mode 100644 index 0f98c2d70..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDByType.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureDIDViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; -import { PageDIDByType as P } from './PageDIDByType'; - -export default { - title: 'Components/Pages/DID', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageDIDByType = Template.bind({}); -PageDIDByType.args = { - showDIDType: true, - comdom: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDViewModel())), -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDByType.tsx b/src/component-library/pages/legacy/DID/PageDIDByType.tsx deleted file mode 100644 index 8d84adee4..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDByType.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { twMerge } from 'tailwind-merge'; -import { createColumnHelper } from '@tanstack/react-table'; - -import { P } from '../../../atoms/legacy/text/content/P/P'; -import { DIDTypeTag } from '@/component-library/features/legacy/Tags/DIDTypeTag'; -import { DIDType } from '@/lib/core/entity/rucio'; -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { TableFilterDiscrete } from '@/component-library/features/legacy/StreamedTables/TableFilterDiscrete'; -import { HiDotsHorizontal } from 'react-icons/hi'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { DIDViewModel } from '@/lib/infrastructure/data/view-model/did'; -import { TableInternalLink } from '@/component-library/features/legacy/StreamedTables/TableInternalLink'; - -export const PageDIDByType = (props: { comdom: UseComDOM; showDIDType?: boolean }) => { - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor(row => `${row.scope}:${row.name}`, { - id: 'did', - header: info => { - return ; - }, - cell: info => { - return ( - {info.getValue()} - ); - }, - }), - columnHelper.accessor('did_type', { - id: 'did_type', - header: info => { - return ( - - name="DID Type" - keys={[DIDType.CONTAINER, DIDType.DATASET, DIDType.FILE]} - renderFunc={state => - state === undefined ? ( - - ) : ( - - ) - } - column={info.column} - /> - ); - }, - cell: info => , - meta: { - style: 'w-6 sm:w-8 md:w-36', - }, - }), - ]; - - return tablecomdom={props.comdom} tablecolumns={tablecolumns} tablestyling={{}} />; -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDDatasetReplicas.stories.tsx b/src/component-library/pages/legacy/DID/PageDIDDatasetReplicas.stories.tsx deleted file mode 100644 index c91ed857f..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDDatasetReplicas.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureDIDDatasetReplicasViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; -import { PageDIDDatasetReplicas as P } from './PageDIDDatasetReplicas'; - -export default { - title: 'Components/Pages/DID', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageDIDDatasetReplicas = Template.bind({}); -PageDIDDatasetReplicas.args = { - comdom: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDDatasetReplicasViewModel())), -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDDatasetReplicas.tsx b/src/component-library/pages/legacy/DID/PageDIDDatasetReplicas.tsx deleted file mode 100644 index 74c574f78..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDDatasetReplicas.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { twMerge } from 'tailwind-merge'; -import { createColumnHelper } from '@tanstack/react-table'; -import { useEffect, useState } from 'react'; - -import { DateTag } from '@/component-library/features/legacy/Tags/DateTag'; -import { FileSize } from '../../../atoms/legacy/text/content/FileSize/FileSize'; -import { ReplicaStateTag } from '@/component-library/features/legacy/Tags/ReplicaStateTag'; -import { ReplicaState } from '@/lib/core/entity/rucio'; -import { RSETag } from '@/component-library/features/legacy/Tags/RSETag'; -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { TableSortUpDown } from '@/component-library/features/legacy/StreamedTables/TableSortUpDown'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { DIDDatasetReplicasViewModel } from '@/lib/infrastructure/data/view-model/did'; - -export const PageDIDDatasetReplicas = (props: { comdom: UseComDOM }) => { - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor('rse', { - id: 'rse', - header: info => { - return ; - }, - cell: info => { - return ( - - - - {info.getValue()} - - - {info.row.original.availability ? '' : } - - ); - }, - meta: { - style: '', - filter: true, - }, - }), - columnHelper.accessor('rseblocked', { meta: { style: '' } }), - columnHelper.accessor('availability', { - // formerly known as `state` - id: 'availability', - cell: info => { - return ( - - ); - }, - meta: { - style: 'cursor-pointer md:w-44 pl-2', - }, - }), - columnHelper.accessor('available_files', { - id: 'available_files', - header: info => { - return ; - }, - cell: info => { - return ( - - {info.row.original.available_files} - - ); - }, - meta: { - style: 'cursor-pointer w-40 2xl:w-44 pt-2', - }, - }), - columnHelper.accessor('available_bytes', { - id: 'available_bytes', - header: info => { - return ; - }, - cell: info => { - return ( - - - - ); - }, - meta: { - style: 'cursor-pointer w-40 2xl:w-44 pt-2', - }, - }), - columnHelper.accessor('creation_date', { - id: 'creation_date', - header: info => { - return ; - }, - cell: info => { - return ( - - - - ); - }, - meta: { - style: 'cursor-pointer w-40 2xl:w-44 pt-2', - }, - }), - columnHelper.accessor('last_accessed', { - id: 'last_accessed', - header: info => { - return ; - }, - cell: info => { - return ( - - - - ); - }, - meta: { - style: 'cursor-pointer w-40 2xl:w-44 pt-2', - }, - }), - ]; - - // handle window resize - const [windowSize, setWindowSize] = useState([1920, 1080]); - useEffect(() => { - setWindowSize([window.innerWidth, window.innerHeight]); - - const handleWindowResize = () => { - setWindowSize([window.innerWidth, window.innerHeight]); - }; - - window.addEventListener('resize', handleWindowResize); - - return () => { - window.removeEventListener('resize', handleWindowResize); - }; - }, []); - const isLg = () => windowSize[0] > 1024; // 1024px is the breakpoint for lg => is minimum lg sized - - return ( - - tablecomdom={props.comdom} - tablecolumns={tablecolumns} - tablestyling={{ - visibility: { - rse: true, - availability: false, - available_files: isLg(), - available_bytes: isLg(), - creation_date: isLg(), - last_accessed: isLg(), - rseblocked: false, - }, - }} - tableselecting={{ - handleChange: (data: DIDDatasetReplicasViewModel[]) => {}, - enableRowSelection: !isLg(), - breakOut: { - breakoutVisibility: !isLg(), - keys: { - available_files: 'Available Files', - available_bytes: 'Available Bytes', - creation_date: 'Creation Date', - last_accessed: 'Last Accessed', - }, - }, - }} - /> - ); -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDFileReplicas.stories.tsx b/src/component-library/pages/legacy/DID/PageDIDFileReplicas.stories.tsx deleted file mode 100644 index 22ebb7211..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDFileReplicas.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { PageDIDFilereplicas as PDF } from './PageDIDFileReplicas'; -import { ReplicaState } from '@/lib/core/entity/rucio'; -import { fixtureFilereplicaStateViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; - -export default { - title: 'Components/Pages/DID', - component: PDF, -} as Meta; - -const Template: StoryFn = args => ; - -export const PageDIDFilereplicas = Template.bind({}); -PageDIDFilereplicas.args = { - comdom: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureFilereplicaStateViewModel())), -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDFileReplicas.tsx b/src/component-library/pages/legacy/DID/PageDIDFileReplicas.tsx deleted file mode 100644 index 5a63c1413..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDFileReplicas.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// components -import { TableFilterDiscrete } from '@/component-library/features/legacy/StreamedTables/TableFilterDiscrete'; - -// misc packages, react -import { createColumnHelper } from '@tanstack/react-table'; -import { twMerge } from 'tailwind-merge'; -import { HiDotsHorizontal } from 'react-icons/hi'; - -// Viewmodels etc -import { ReplicaState } from '@/lib/core/entity/rucio'; -import { ReplicaStateTag } from '@/component-library/features/legacy/Tags/ReplicaStateTag'; -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { TableInternalLink } from '@/component-library/features/legacy/StreamedTables/TableInternalLink'; -import { FileReplicaStateViewModel } from '@/lib/infrastructure/data/view-model/did'; -import { TableStyling } from '@/component-library/features/legacy/StreamedTables/types'; - -export const PageDIDFilereplicas = (props: { comdom: UseComDOM; tablestyling?: TableStyling }) => { - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor('rse', { - id: 'rse', - cell: info => { - // perhaps use this as a basis for links in tables - return {info.getValue()}; - }, - header: info => { - return ; - }, - filterFn: 'includesString', - }), - columnHelper.accessor('state', { - id: 'state', - cell: info => { - return ; - }, - header: info => { - return ( - - name="File Replica State" - keys={Object.values(ReplicaState)} - renderFunc={key => - key === undefined ? ( - - ) : ( - - ) - } - column={info.column} - /> - ); - }, - filterFn: 'equalsString', - meta: { - style: 'w-28 md:w-56', - }, - }), - ]; - return ( - tablecomdom={props.comdom} tablecolumns={tablecolumns} tablestyling={props.tablestyling ?? {}} /> - ); -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDFileReplicasD.stories.tsx b/src/component-library/pages/legacy/DID/PageDIDFileReplicasD.stories.tsx deleted file mode 100644 index ffffd200f..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDFileReplicasD.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { PageDIDFilereplicasD as PDFD } from './PageDIDFileReplicasD'; -import { - fixtureFilereplicaStateViewModel, - fixtureFilereplicaStateDViewModel, - mockUseComDOM, - fixtureDIDViewModel, -} from '@/test/fixtures/table-fixtures'; -import { useEffect, useState } from 'react'; - -export default { - title: 'Components/Pages/DID', - component: PDFD, -} as Meta; - -const Template: StoryFn = args => ; - -export const PageDIDFilereplicasD = Template.bind({}); -PageDIDFilereplicasD.args = { - datasetComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDViewModel())), - replicaComDOM: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureFilereplicaStateViewModel())), - onChangeFileSelection: (scope: string, name: string) => console.log('onChangeFileSelection', scope, name), -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDFileReplicasD.tsx b/src/component-library/pages/legacy/DID/PageDIDFileReplicasD.tsx deleted file mode 100644 index d691e0c21..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDFileReplicasD.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// components -import { P } from '../../../atoms/legacy/text/content/P/P'; -import { PageDIDFilereplicas } from './PageDIDFileReplicas'; - -// misc packages, react -import { createColumnHelper } from '@tanstack/react-table'; -import { twMerge } from 'tailwind-merge'; - -// Viewmodels etc -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { DIDViewModel, FileReplicaStateViewModel } from '@/lib/infrastructure/data/view-model/did'; -import { TableInternalLink } from '@/component-library/features/legacy/StreamedTables/TableInternalLink'; - -export const PageDIDFilereplicasD = (props: { - datasetComDOM: UseComDOM; // the files in the dataset - replicaComDOM: UseComDOM; // replicas of the selected file - onChangeFileSelection: (scope: string, name: string) => void; -}) => { - const { datasetComDOM, replicaComDOM, onChangeFileSelection } = props; - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor(row => `${row.scope}:${row.name}`, { - id: 'did', - header: info => { - return ; - }, - cell: info => { - return ( - {info.getValue()} - ); - }, - }), - ]; - - return ( -

- Select a file and view the states of its replicas. -
- - tablecomdom={datasetComDOM} - tablecolumns={tablecolumns} - tablestyling={{ - tableFooterStack: true, - }} - tableselecting={{ - handleChange: (data: DIDViewModel[]) => { - if (data.length === 0) return; - onChangeFileSelection(data[0].scope, data[0].name); - }, - enableRowSelection: true, - enableMultiRowSelection: false, - }} - /> - -
-
- ); -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDMetadata.stories.tsx b/src/component-library/pages/legacy/DID/PageDIDMetadata.stories.tsx deleted file mode 100644 index 449377b03..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDMetadata.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureDIDKeyValuePairsDataViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; -import { PageDIDMetadata as P } from './PageDIDMetadata'; - -export default { - title: 'Components/Pages/DID', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageDIDMetadata = Template.bind({}); -PageDIDMetadata.args = { - tabledata: fixtureDIDKeyValuePairsDataViewModel(), -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDMetadata.tsx b/src/component-library/pages/legacy/DID/PageDIDMetadata.tsx deleted file mode 100644 index daeebf290..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDMetadata.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// components -import { H3 } from '../../../atoms/legacy/text/headings/H3/H3'; -import { BoolTag } from '@/component-library/features/legacy/Tags/BoolTag'; -import { AvailabilityTag } from '@/component-library/features/legacy/Tags/AvailabilityTag'; -import { DIDTypeTag } from '@/component-library/features/legacy/Tags/DIDTypeTag'; -import { DIDType, DIDKeyValuePair } from '@/lib/core/entity/rucio'; -import { NullTag } from '@/component-library/features/legacy/Tags/NullTag'; - -// misc packages, react -import { createColumnHelper } from '@tanstack/react-table'; -import { twMerge } from 'tailwind-merge'; - -// Viewmodels etc -import { DIDAvailability } from '@/lib/core/entity/rucio'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { DIDKeyValuePairsDataViewModel } from '@/lib/infrastructure/data/view-model/did'; -import { NormalTable } from '@/component-library/features/legacy/StreamedTables/NormalTable'; - -export const PageDIDMetadata = (props: { - tabledata: DIDKeyValuePairsDataViewModel; // remember that this is ONLY the custom metadata -}) => { - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor('key', { - id: 'key', - cell: info => { - return {info.getValue()}; - }, - header: info => { - return ; - }, - }), - columnHelper.accessor('value', { - id: 'value', - cell: info => { - const val = info.getValue(); - if (typeof val === 'boolean') { - return ; - } - if (val === null) { - return ; - } - if (Object.keys(DIDAvailability).includes(val as string)) { - return ; - } - if (Object.keys(DIDType).includes(val as DIDType)) { - return ; - } else { - return {val as string}; - } - }, - header: info => { - return

Value

; - }, - }), - ]; - - if (props.tabledata.status === 'pending') { - return ( -
- Loading DID Metadata -
- ); - } - return tabledata={props.tabledata.data || []} tablecolumns={tablecolumns} />; -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDRules.stories.tsx b/src/component-library/pages/legacy/DID/PageDIDRules.stories.tsx deleted file mode 100644 index fdee24668..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDRules.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { PageDIDRules as P } from './PageDIDRules'; -import { fixtureDIDRulesViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; - -export default { - title: 'Components/Pages/DID', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageDIDRules = Template.bind({}); -PageDIDRules.args = { - comdom: mockUseComDOM(Array.from({ length: 100 }, (_, i) => fixtureDIDRulesViewModel())), -}; diff --git a/src/component-library/pages/legacy/DID/PageDIDRules.tsx b/src/component-library/pages/legacy/DID/PageDIDRules.tsx deleted file mode 100644 index e796efa41..000000000 --- a/src/component-library/pages/legacy/DID/PageDIDRules.tsx +++ /dev/null @@ -1,136 +0,0 @@ -// components -import { H3 } from '../../../atoms/legacy/text/headings/H3/H3'; -import { HiDotsHorizontal } from 'react-icons/hi'; -import { createColumnHelper } from '@tanstack/react-table'; -import { useEffect, useState } from 'react'; -import { twMerge } from 'tailwind-merge'; -import { RuleStateTag } from '@/component-library/features/legacy/Tags/RuleStateTag'; -import { DateTag } from '@/component-library/features/legacy/Tags/DateTag'; -import { RuleState } from '@/lib/core/entity/rucio'; -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable'; -import { TableFilterDiscrete } from '@/component-library/features/legacy/StreamedTables/TableFilterDiscrete'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { TableSortUpDown } from '@/component-library/features/legacy/StreamedTables/TableSortUpDown.stories'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { DIDRulesViewModel } from '@/lib/infrastructure/data/view-model/did'; - -export const PageDIDRules = (props: { comdom: UseComDOM }) => { - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor('id', { - id: 'id', - }), - columnHelper.accessor('name', { - id: 'Rule', - cell: info => { - return ( - - {info.getValue()} - 1024 ? 'hidden' : ''} /> - - ); - }, - header: info => { - return ; - }, - meta: { - style: 'pl-1', - }, - }), - columnHelper.accessor('state', { - id: 'state', - cell: info => { - return ; - }, - header: info => { - return ( - - name="Rule State" - keys={Object.values(RuleState)} - renderFunc={key => - key === undefined ? ( - - ) : ( - - ) - } - column={info.column} - /> - ); - }, - meta: { - style: 'w-28 md:w-44 cursor-pointer select-none', - }, - }), - columnHelper.accessor('account', { - id: 'Account', - cell: info => { - return

{info.getValue()}

; - }, - header: info => { - return ; - }, - meta: { - style: 'pl-1', - }, - }), - columnHelper.accessor('subscription', { - id: 'subscription', - cell: info => { - return

{info.getValue()?.name ?? ''}

; - }, - header: info => { - return

Subscription

; - }, - meta: { - style: '', - }, - }), - columnHelper.accessor('last_modified', { - id: 'last_modified', - cell: info => { - return ; - }, - header: info => { - return ; - }, - meta: { - style: 'w-48', - }, - }), - ]; - - // handle window resize - const [windowSize, setWindowSize] = useState([1920, 1080]); - - useEffect(() => { - setWindowSize([window.innerWidth, window.innerHeight]); - - const handleWindowResize = () => { - setWindowSize([window.innerWidth, window.innerHeight]); - }; - - window.addEventListener('resize', handleWindowResize); - - return () => { - window.removeEventListener('resize', handleWindowResize); - }; - }, []); - - return ( - - tablecomdom={props.comdom} - tablecolumns={tablecolumns} - tablestyling={{ - visibility: { - id: false, - Rule: true, - state: windowSize[0] > 1024, - Account: true, - subscription: windowSize[0] > 1024, - last_modified: windowSize[0] > 640, - }, - }} - /> - ); -}; diff --git a/src/component-library/pages/legacy/RSE/PageRSE.stories.tsx b/src/component-library/pages/legacy/RSE/PageRSE.stories.tsx deleted file mode 100644 index 6db350365..000000000 --- a/src/component-library/pages/legacy/RSE/PageRSE.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { RSEBlockState } from '@/lib/core/entity/rucio'; -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureRSEViewModel, fixtureRSEProtocolViewModel, fixtureRSEAttributeViewModel } from '@/test/fixtures/table-fixtures'; -import { PageRSE as P } from './PageRSE'; - -export default { - title: 'Components/Pages/RSE', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageRSE = Template.bind({}); -PageRSE.args = { - rse: fixtureRSEViewModel(), - rseblockstate: 7 as RSEBlockState, // 7 = all blocked - protocols: fixtureRSEProtocolViewModel(), - attributes: fixtureRSEAttributeViewModel(), - fromrselist: true, -}; diff --git a/src/component-library/pages/legacy/RSE/PageRSE.tsx b/src/component-library/pages/legacy/RSE/PageRSE.tsx deleted file mode 100644 index 8b9f7eef1..000000000 --- a/src/component-library/pages/legacy/RSE/PageRSE.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { RSEBlockState } from '@/lib/core/entity/rucio'; -import { twMerge } from 'tailwind-merge'; -import { Generaltable } from '../../../atoms/legacy/helpers/Metatable/Metatable'; -import { Titleth, Contenttd } from '../../../atoms/legacy/helpers/Metatable/Metatable'; -import { BoolTag } from '@/component-library/features/legacy/Tags/BoolTag'; -import { RSETypeTag } from '@/component-library/features/legacy/Tags/RSETypeTag'; -import { RSETag } from '@/component-library/features/legacy/Tags/RSETag'; -import { RSEAttributeViewModel, RSEProtocolViewModel, RSEViewModel } from '@/lib/infrastructure/data/view-model/rse'; -import { PageRSEProtocols } from './PageRSEProtocols'; -import { PageRSEAttributes } from './PageRSEAttributes'; -import { H2 } from '../../../atoms/legacy/text/headings/H2/H2'; -import { Body } from '@/component-library/pages/legacy/Helpers/Body'; -import { Heading } from '@/component-library/pages/legacy/Helpers/Heading'; -import { InfoField } from '@/component-library/features/fields/InfoField'; -import React from 'react'; - -type PageRSEProps = { - rse: RSEViewModel; - rseblockstate: RSEBlockState; - protocols: RSEProtocolViewModel; - attributes: RSEAttributeViewModel; - fromrselist?: boolean; -}; - -export const PageRSE = (props: PageRSEProps) => { - return ( -

- - This page is currently in development. We are working on improvements, so stay tuned! - - -
- - - Name - {props.rse.name} - - - RSE Type - - - - - - Availability - - - - - - - - Volatile - - - - - - Deterministic - - - - - - Staging Area - - - - - -
-
- -
-

RSE Protocols

- -
-
-

RSE Attributes

- -
- -
- ); -}; diff --git a/src/component-library/pages/legacy/RSE/PageRSEAttributes.stories.tsx b/src/component-library/pages/legacy/RSE/PageRSEAttributes.stories.tsx deleted file mode 100644 index 05141a291..000000000 --- a/src/component-library/pages/legacy/RSE/PageRSEAttributes.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureRSEAttributeViewModel } from '@/test/fixtures/table-fixtures'; -import { PageRSEAttributes as P } from './PageRSEAttributes'; - -export default { - title: 'Components/Pages/RSE', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageRSEAttributes = Template.bind({}); -PageRSEAttributes.args = { - attributes: fixtureRSEAttributeViewModel().attributes, -}; diff --git a/src/component-library/pages/legacy/RSE/PageRSEAttributes.tsx b/src/component-library/pages/legacy/RSE/PageRSEAttributes.tsx deleted file mode 100644 index c1be17f10..000000000 --- a/src/component-library/pages/legacy/RSE/PageRSEAttributes.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { createColumnHelper } from '@tanstack/react-table'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { P } from '../../../atoms/legacy/text/content/P/P'; -import { H3 } from '../../../atoms/legacy/text/headings/H3/H3'; -import { BoolTag } from '@/component-library/features/legacy/Tags/BoolTag'; -import { NullTag } from '@/component-library/features/legacy/Tags/NullTag'; -import { RSEAttribute } from '@/lib/core/entity/rucio'; -import { NormalTable } from '@/component-library/features/legacy/StreamedTables/NormalTable'; - -export const PageRSEAttributes = (props: { attributes: RSEAttribute[] }) => { - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor('key', { - id: 'key', - header: info => , - cell: info =>

{info.getValue()}

, - }), - columnHelper.accessor('value', { - id: 'value', - header: info =>

Value

, - cell: info => { - const val = info.getValue(); - if (typeof val === 'boolean') { - return ; - } else if (val === null) { - return ; - } else { - return

{val}

; - } - }, - }), - ]; - return tabledata={props.attributes} tablecolumns={tablecolumns} />; -}; diff --git a/src/component-library/pages/legacy/RSE/PageRSEProtocols.stories.tsx b/src/component-library/pages/legacy/RSE/PageRSEProtocols.stories.tsx deleted file mode 100644 index f2e418a3b..000000000 --- a/src/component-library/pages/legacy/RSE/PageRSEProtocols.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureRSEProtocolViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; -import { PageRSEProtocols as P } from './PageRSEProtocols'; - -export default { - title: 'Components/Pages/RSE', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageRSEProtocols = Template.bind({}); -PageRSEProtocols.args = { - tableData: fixtureRSEProtocolViewModel(), -}; diff --git a/src/component-library/pages/legacy/RSE/PageRSEProtocols.tsx b/src/component-library/pages/legacy/RSE/PageRSEProtocols.tsx deleted file mode 100644 index 1de45ec8b..000000000 --- a/src/component-library/pages/legacy/RSE/PageRSEProtocols.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { createColumnHelper } from '@tanstack/react-table'; -import { P } from '../../../atoms/legacy/text/content/P/P'; -import { twMerge } from 'tailwind-merge'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable.stories'; -import { TableSortUpDown } from '@/component-library/features/legacy/StreamedTables/TableSortUpDown'; -import { H3 } from '../../../atoms/legacy/text/headings/H3/H3'; -import { RSEProtocolViewModel } from '@/lib/infrastructure/data/view-model/rse'; -import { RSEProtocol } from '@/lib/core/entity/rucio'; -import { NormalTable } from '@/component-library/features/legacy/StreamedTables/NormalTable'; -import { TableStyling } from '@/component-library/features/legacy/StreamedTables/types'; - -export const PageRSEProtocols = (props: { - // TODO: data will not be streamed, but loaded in one go - tableData: RSEProtocolViewModel; -}) => { - const shortstyle = { style: 'w-20' }; - const shortstyleblue = { style: 'w-20 bg-extra-indigo-500' }; - const shortstylepink = { style: 'w-20 bg-extra-rose-500' }; - const columnHelper = createColumnHelper(); - const tablecolumns: any[] = [ - columnHelper.accessor('scheme', { - id: 'scheme', - header: info =>

Scheme

, - cell: info =>

{info.getValue()}

, - meta: { style: 'w-24' }, - }), - columnHelper.accessor('hostname', { - id: 'hostname', - header: info =>

Hostname

, - cell: info =>

{info.getValue()}

, - }), - columnHelper.accessor('port', { - id: 'port', - header: info =>

Port

, - cell: info =>

{info.getValue()}

, - meta: { style: 'w-24' }, - }), - columnHelper.accessor('prefix', { - id: 'prefix', - header: info =>

Prefix

, - cell: info =>

{info.getValue()}

, - }), - columnHelper.accessor('priorities_lan.read', { - id: 'priorities_lan.read', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstyleblue, - }), - columnHelper.accessor('priorities_lan.write', { - id: 'priorities_lan.write', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstyleblue, - }), - columnHelper.accessor('priorities_lan.delete', { - id: 'priorities_lan.delete', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstyleblue, - }), - columnHelper.accessor('priorities_wan.read', { - id: 'priorities_wan.read', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstylepink, - }), - columnHelper.accessor('priorities_wan.write', { - id: 'priorities_wan.write', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstylepink, - }), - columnHelper.accessor('priorities_wan.delete', { - id: 'priorities_wan.delete', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstylepink, - }), - // columnHelper.accessor("priorities_wan.tpc", { - // id: "priorities_lan.tpc", - // header: info => , - // cell: info =>

{info.getValue()}

, - // meta: shortstylepink, - // }), - columnHelper.accessor('priorities_wan.tpcwrite', { - id: 'priorities_wan.tpcwrite', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstylepink, - }), - columnHelper.accessor('priorities_wan.tpcread', { - id: 'priorities_wan.tpcread', - header: info => , - cell: info =>

{info.getValue()}

, - meta: shortstylepink, - }), - ]; - return ( - - tabledata={props.tableData.protocols || []} - tablecolumns={tablecolumns} - tablestyling={ - { - pageSize: 5, - } as TableStyling - } - /> - ); -}; diff --git a/src/component-library/pages/legacy/Rule/PageRule.stories.tsx b/src/component-library/pages/legacy/Rule/PageRule.stories.tsx deleted file mode 100644 index bb438b8b9..000000000 --- a/src/component-library/pages/legacy/Rule/PageRule.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { StoryFn, Meta } from '@storybook/react'; -import { fixtureRulePageLockEntryViewModel, fixtureRuleMetaViewModel, mockUseComDOM } from '@/test/fixtures/table-fixtures'; -import { PageRule as P } from './PageRule'; -import { RulePageLockEntryViewModel } from '@/lib/infrastructure/data/view-model/rule'; - -export default { - title: 'Components/Pages/Rule', - component: P, -} as Meta; - -const Template: StoryFn = args =>

; - -export const PageRule = Template.bind({}); -PageRule.args = { - ruleMeta: fixtureRuleMetaViewModel(), - ruleLocks: mockUseComDOM(Array.from({ length: 100 }, () => fixtureRulePageLockEntryViewModel())), - ruleBoostFunc: () => { - console.log('boosted rule'); - }, - ruleBoostShow: true, -}; diff --git a/src/component-library/pages/legacy/Rule/PageRule.tsx b/src/component-library/pages/legacy/Rule/PageRule.tsx deleted file mode 100644 index a42fff695..000000000 --- a/src/component-library/pages/legacy/Rule/PageRule.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { twMerge } from 'tailwind-merge'; -var format = require('date-format'); - -import { LockState } from '@/lib/core/entity/rucio'; -import { Tabs } from '../../../atoms/legacy/Tabs/Tabs'; -import { SubPage } from '../../../atoms/legacy/helpers/SubPage/SubPage'; -import { H3 } from '../../../atoms/legacy/text/headings/H3/H3'; -import { P } from '../../../atoms/legacy/text/content/P/P'; -import { BoolTag } from '@/component-library/features/legacy/Tags/BoolTag'; -import { DIDTypeTag } from '@/component-library/features/legacy/Tags/DIDTypeTag'; -import { RuleStateTag } from '@/component-library/features/legacy/Tags/RuleStateTag'; -import { LockStateTag } from '@/component-library/features/legacy/Tags/LockStateTag'; -import { RuleNotificationTag } from '@/component-library/features/legacy/Tags/RuleNotificationTag'; -import { StreamedTable } from '@/component-library/features/legacy/StreamedTables/StreamedTable'; -import { createColumnHelper } from '@tanstack/react-table'; -import { HiDotsHorizontal, HiExternalLink } from 'react-icons/hi'; -import { TableExternalLink } from '@/component-library/features/legacy/StreamedTables/TableExternalLink'; -import { TableFilterDiscrete } from '@/component-library/features/legacy/StreamedTables/TableFilterDiscrete'; -import { TableFilterString } from '@/component-library/features/legacy/StreamedTables/TableFilterString'; -import { Titleth, Contenttd, Generaltable } from '../../../atoms/legacy/helpers/Metatable/Metatable'; -import { UseComDOM } from '@/lib/infrastructure/hooks/useComDOM'; -import { Heading } from '@/component-library/pages/legacy/Helpers/Heading'; -import { Body } from '@/component-library/pages/legacy/Helpers/Body'; -import { RuleMetaViewModel, RulePageLockEntryViewModel } from '@/lib/infrastructure/data/view-model/rule'; -import { Button } from '@/component-library/atoms/legacy/Button/Button'; -import { HiRocketLaunch } from 'react-icons/hi2'; -import { InfoField } from '@/component-library/features/fields/InfoField'; - -export interface PageRulePageProps { - ruleMeta: RuleMetaViewModel; - ruleLocks: UseComDOM; - ruleBoostFunc: () => void; - ruleBoostShow: boolean; -} - -export const PageRule = (props: PageRulePageProps) => { - const [subpageIndex, setSubpageIndex] = useState(0); - - useEffect(() => { - if (subpageIndex === 1 && props.ruleLocks.query.data.all.length === 0) { - // Opened locks tab, but no data yet => start load - console.log(props.ruleLocks.query.data); - props.ruleLocks.start(); - } - }, [subpageIndex]); - - const meta = props.ruleMeta; - - const columnHelper = createColumnHelper(); - const tablecolumns = [ - columnHelper.accessor(row => `${row.scope}:${row.name}`, { - id: 'did', - header: info => { - return ; - }, - cell: info => { - return

{info.getValue()}

; - }, - }), - columnHelper.accessor('rse', { - id: 'rse', - header: info => { - return ; - }, - cell: info => { - return

{info.getValue()}

; - }, - }), - columnHelper.accessor('state', { - id: 'state', - cell: info => , - header: info => { - return ( - - name="Lock" - keys={Object.values(LockState)} - renderFunc={state => - state === undefined ? ( - - ) : ( - - ) - } - column={info.column} - /> - ); - }, - meta: { - style: 'w-6 sm:w-8 md:w-32', - }, - }), - columnHelper.display({ - id: 'links', - header: info => { - return ( - -

Links

-
- ); - }, - cell: info => { - return ( - - - - - ); - }, - meta: { - style: 'w-32', - }, - }), - ]; - - const [windowSize, setWindowSize] = useState([1920, 1080]); - - useEffect(() => { - setWindowSize([window.innerWidth, window.innerHeight]); - - const handleWindowResize = () => { - setWindowSize([window.innerWidth, window.innerHeight]); - }; - - window.addEventListener('resize', handleWindowResize); - - return () => { - window.removeEventListener('resize', handleWindowResize); - }; - }, []); - - return ( -
- - - This page is currently using mock data for demonstration purposes. We are actively working on implementing real data soon! - - - -
-
-
- - { - setSubpageIndex(active); - }} - /> - -
- - - Scope - {meta.scope} - - - Name - {meta.name} - - - - - Created At - {format('yyyy-MM-dd', new Date(meta.created_at))} - - - Updated At - {format('yyyy-MM-dd', new Date(meta.updated_at))} - - - Expires At - - { - format('yyyy-MM-dd', new Date(meta.expires_at)) - // add ability to extend lifetime here => or maybe not?? i think this might be bad UX - } - - - - - - Locks OK - -

{meta.locks_ok_cnt}

-
- - - Locks Replicating - -

{meta.locks_replicating_cnt}

-
- - - Locks Stuck - -

{meta.locks_stuck_cnt}

-
- -
- - - Purge Replicas - {} - - - Split Container - {} - - - Ignore Account Limit - {} - - - Ignore Availability - {} - - - Locked - {} - - - - - Copies - {meta.copies} - - - ID - {meta.id} - - - DID Type - - - - - - Grouping - - - - - - Priority - {meta.priority} - - - - - RSE Expression - {meta.rse_expression} - - - Notification - - - - - - - - Account - {meta.account} - - - Activity - {meta.activity} - - - - - State - - - - - -
-
- - 768, - links: windowSize[0] > 768, - }, - }} - /> - - -
- ); -}; diff --git a/src/lib/core/dto/rse-dto.ts b/src/lib/core/dto/rse-dto.ts index 36b3b2677..ba7f901f1 100644 --- a/src/lib/core/dto/rse-dto.ts +++ b/src/lib/core/dto/rse-dto.ts @@ -1,5 +1,6 @@ import { BaseDTO, BaseStreamableDTO } from '@/lib/sdk/dto'; -import { RSE, RSEAttribute, RSEProtocol, RSEType } from '@/lib/core/entity/rucio'; +import { RSE, RSEAttribute, RSEDetails, RSEProtocol, RSEType } from '@/lib/core/entity/rucio'; +import { undefined } from 'zod'; /** * The Data Transfer Object for the ListRSEsEndpoint which contains the stream @@ -9,7 +10,23 @@ export interface ListRSEsDTO extends BaseStreamableDTO {} /** * Data Transfer Object for GET RSE Endpoint */ -export interface RSEDTO extends BaseDTO, RSE {} +export interface RSEDetailsDTO extends BaseDTO, RSEDetails {} + +export const getEmptyRSEDetailsDTO = (): RSEDetailsDTO => { + return { + availability_delete: false, + availability_read: false, + availability_write: false, + deterministic: false, + id: '', + name: '', + protocols: [], + rse_type: RSEType.UNKNOWN, + staging_area: false, + status: 'error', + volatile: false, + }; +}; /** * Data Transfer Object for GET RSE Protocols Endpoint @@ -34,15 +51,3 @@ export interface RSEUsageDTO extends BaseDTO { files: number; updated_at: string; } - -export function getEmptyRSEDTO(): RSEDTO { - return { - status: 'error', - id: '', - name: '', - rse_type: RSEType.UNKNOWN, - volatile: false, - staging_area: false, - deterministic: false, - }; -} diff --git a/src/lib/core/dto/rule-dto.ts b/src/lib/core/dto/rule-dto.ts index 516074387..7afb59318 100644 --- a/src/lib/core/dto/rule-dto.ts +++ b/src/lib/core/dto/rule-dto.ts @@ -1,11 +1,13 @@ import { BaseDTO, BaseStreamableDTO } from '@/lib/sdk/dto'; -import { LockState, Rule } from '../entity/rucio'; +import { LockState, Rule, RuleMeta } from '../entity/rucio'; /** * The Data Transfer Object for the ListRulesEndpoint which contains the stream */ export interface RuleDTO extends BaseDTO, Rule {} +export interface RuleMetaDTO extends BaseDTO, RuleMeta {} + /** * Data Transfer Object for Rule Replica Locks */ @@ -16,6 +18,8 @@ export interface RuleReplicaLockStateDTO extends BaseDTO { state: LockState; } +export interface ListLocksDTO extends BaseStreamableDTO {} + export interface ListRulesDTO extends BaseStreamableDTO {} export interface CreateRuleDTO extends BaseDTO { diff --git a/src/lib/core/entity/rucio.ts b/src/lib/core/entity/rucio.ts index b5e43dc86..841f0ded2 100644 --- a/src/lib/core/entity/rucio.ts +++ b/src/lib/core/entity/rucio.ts @@ -133,6 +133,7 @@ export type RSEAccountUsage = { bytes_limit: number; }; +// TODO: complete with all the fields // copied from deployed rucio UI export type RuleMeta = { account: string; @@ -140,8 +141,8 @@ export type RuleMeta = { copies: number; created_at: DateISO; did_type: DIDType; - expires_at: DateISO; - grouping: DIDType; + expires_at: DateISO | null; + grouping: RuleGrouping; id: string; ignore_account_limit: boolean; ignore_availability: boolean; @@ -284,6 +285,42 @@ export type RSEProtocol = { created_at?: DateISO; // TODO: rucio does not provide this }; +export type RSEDetailsProtocol = { + domains: { + lan: { + read: number; + write: number; + delete: number; + }; + wan: { + read: number; + write: number; + delete: number; + third_party_copy_read: number; + third_party_copy_write: number; + }; + }; + scheme: string; + hostname: string; + port: number; + prefix: string; + impl: string; + extended_attributes?: Record; +}; + +export type RSEDetails = { + id: string; + name: string; + rse_type: RSEType; + volatile: boolean; + deterministic: boolean; + staging_area: boolean; + availability_delete: boolean; + availability_read: boolean; + availability_write: boolean; + protocols: RSEDetailsProtocol[]; +}; + export type RSEAttribute = { key: string; value: string | DateISO | number | boolean | null; @@ -303,6 +340,12 @@ export enum LockState { UNKNOWN = 'U', } +export enum RuleGrouping { + ALL = 'A', + DATASET = 'D', + NONE = 'N', +} + // rucio.db.sqla.constants::RuleNotification export enum RuleNotification { Yes = 'Y', // notify when the rules state becomes OK diff --git a/src/lib/core/port/primary/get-rule-ports.ts b/src/lib/core/port/primary/get-rule-ports.ts new file mode 100644 index 000000000..6bedd626c --- /dev/null +++ b/src/lib/core/port/primary/get-rule-ports.ts @@ -0,0 +1,12 @@ +import { BaseAuthenticatedInputPort, BaseOutputPort } from '@/lib/sdk/primary-ports'; +import { GetRuleError, GetRuleRequest, GetRuleResponse } from '@/lib/core/usecase-models/get-rule-usecase-models'; + +/** + * @interface GetRuleInputPort representing the GetRule usecase. + */ +export interface GetRuleInputPort extends BaseAuthenticatedInputPort {} + +/** + * @interface GetRuleOutputPort representing the GetRule presenter. + */ +export interface GetRuleOutputPort extends BaseOutputPort {} diff --git a/src/lib/core/port/primary/list-rule-replica-lock-states-ports.ts b/src/lib/core/port/primary/list-rule-replica-lock-states-ports.ts new file mode 100644 index 000000000..63773f4e1 --- /dev/null +++ b/src/lib/core/port/primary/list-rule-replica-lock-states-ports.ts @@ -0,0 +1,18 @@ +import { BaseAuthenticatedInputPort, BaseStreamingOutputPort } from '@/lib/sdk/primary-ports'; +import { + ListRuleReplicaLockStatesResponse, + ListRuleReplicaLockStatesRequest, + ListRuleReplicaLockStatesError, +} from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import { ListRuleReplicaLockStatesViewModel } from '@/lib/infrastructure/data/view-model/rule'; +/** + * @interface ListRuleReplicaLockStatesInputPort that abstracts the usecase. + */ +export interface ListRuleReplicaLockStatesInputPort extends BaseAuthenticatedInputPort {} + +// TODO: Add viewmodel +/** + * @interface ListRuleReplicaLockStatesOutputPort that abtrsacts the presenter + */ +export interface ListRuleReplicaLockStatesOutputPort + extends BaseStreamingOutputPort {} diff --git a/src/lib/core/port/secondary/rse-gateway-output-port.ts b/src/lib/core/port/secondary/rse-gateway-output-port.ts index b02402cee..eb564a111 100644 --- a/src/lib/core/port/secondary/rse-gateway-output-port.ts +++ b/src/lib/core/port/secondary/rse-gateway-output-port.ts @@ -1,4 +1,4 @@ -import { ListRSEsDTO, RSEAttributeDTO, RSEDTO, RSEProtocolDTO, RSEUsageDTO } from '@/lib/core/dto/rse-dto'; +import { ListRSEsDTO, RSEAttributeDTO, RSEDetailsDTO, RSEProtocolDTO, RSEUsageDTO } from '@/lib/core/dto/rse-dto'; export default interface RSEGatewayOutputPort { /** @@ -6,7 +6,7 @@ export default interface RSEGatewayOutputPort { * @param rucioAuthToken A valid Rucio Auth Token. * @param rseName The RSE to get. */ - getRSE(rucioAuthToken: string, rseName: string): Promise; + getRSE(rucioAuthToken: string, rseName: string): Promise; /** * Lists all supported protocols for a given RSE. diff --git a/src/lib/core/port/secondary/rule-gateway-output-port.ts b/src/lib/core/port/secondary/rule-gateway-output-port.ts index d8d3af0b6..5080c4a76 100644 --- a/src/lib/core/port/secondary/rule-gateway-output-port.ts +++ b/src/lib/core/port/secondary/rule-gateway-output-port.ts @@ -1,5 +1,5 @@ import { BaseStreamableDTO } from '@/lib/sdk/dto'; -import { CreateRuleDTO, ListRulesDTO, RuleDTO } from '../../dto/rule-dto'; +import { CreateRuleDTO, ListRulesDTO, RuleDTO, RuleMetaDTO } from '../../dto/rule-dto'; import { ListRulesFilter } from '@/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils'; import { RuleCreationParameters } from '@/lib/core/entity/rucio'; @@ -9,7 +9,7 @@ export default interface RuleGatewayOutputPort { * @param rucioAuthToken A valid Rucio Auth Token. * @param ruleId The rule to get. */ - getRule(rucioAuthToken: string, ruleId: string): Promise; + getRule(rucioAuthToken: string, ruleId: string): Promise; /** * Lists all rules for a given account. diff --git a/src/lib/core/use-case/get-rse-usecase.ts b/src/lib/core/use-case/get-rse-usecase.ts index 60adb4d80..8a7c3f304 100644 --- a/src/lib/core/use-case/get-rse-usecase.ts +++ b/src/lib/core/use-case/get-rse-usecase.ts @@ -5,12 +5,12 @@ import { AuthenticatedRequestModel } from '@/lib/sdk/usecase-models'; import { GetRSEError, GetRSERequest, GetRSEResponse } from '@/lib/core/usecase-models/get-rse-usecase-models'; import { GetRSEInputPort, type GetRSEOutputPort } from '@/lib/core/port/primary/get-rse-ports'; -import { RSEDTO } from '@/lib/core/dto/rse-dto'; +import { RSEDetailsDTO } from '@/lib/core/dto/rse-dto'; import type RSEGatewayOutputPort from '@/lib/core/port/secondary/rse-gateway-output-port'; @injectable() export default class GetRSEUseCase - extends BaseSingleEndpointUseCase, GetRSEResponse, GetRSEError, RSEDTO> + extends BaseSingleEndpointUseCase, GetRSEResponse, GetRSEError, RSEDetailsDTO> implements GetRSEInputPort { constructor(protected readonly presenter: GetRSEOutputPort, private readonly gateway: RSEGatewayOutputPort) { @@ -21,12 +21,12 @@ export default class GetRSEUseCase return undefined; } - async makeGatewayRequest(requestModel: AuthenticatedRequestModel): Promise { + async makeGatewayRequest(requestModel: AuthenticatedRequestModel): Promise { const { rucioAuthToken, rseName } = requestModel; - const dto: RSEDTO = await this.gateway.getRSE(rucioAuthToken, rseName); + const dto: RSEDetailsDTO = await this.gateway.getRSE(rucioAuthToken, rseName); return dto; } - handleGatewayError(error: RSEDTO): GetRSEError { + handleGatewayError(error: RSEDetailsDTO): GetRSEError { return { status: 'error', message: error.errorMessage ? error.errorMessage : 'Gateway Error', @@ -35,7 +35,7 @@ export default class GetRSEUseCase } as GetRSEError; } - processDTO(dto: RSEDTO): { data: GetRSEResponse | GetRSEError; status: 'success' | 'error' } { + processDTO(dto: RSEDetailsDTO): { data: GetRSEResponse | GetRSEError; status: 'success' | 'error' } { const responseModel: GetRSEResponse = { ...dto, status: 'success', diff --git a/src/lib/core/use-case/get-rule-usecase.ts b/src/lib/core/use-case/get-rule-usecase.ts new file mode 100644 index 000000000..c8291c02e --- /dev/null +++ b/src/lib/core/use-case/get-rule-usecase.ts @@ -0,0 +1,49 @@ +import { injectable } from 'inversify'; +import { BaseSingleEndpointUseCase } from '@/lib/sdk/usecase'; +import { AuthenticatedRequestModel } from '@/lib/sdk/usecase-models'; + +import { GetRuleError, GetRuleRequest, GetRuleResponse } from '@/lib/core/usecase-models/get-rule-usecase-models'; +import { GetRuleInputPort, type GetRuleOutputPort } from '@/lib/core/port/primary/get-rule-ports'; + +import { RuleMetaDTO } from '@/lib/core/dto/rule-dto'; +import type RuleGatewayOutputPort from '@/lib/core/port/secondary/rule-gateway-output-port'; + +@injectable() +export default class GetRuleUseCase + extends BaseSingleEndpointUseCase, GetRuleResponse, GetRuleError, RuleMetaDTO> + implements GetRuleInputPort +{ + constructor(protected readonly presenter: GetRuleOutputPort, private readonly gateway: RuleGatewayOutputPort) { + super(presenter); + } + + validateRequestModel(requestModel: AuthenticatedRequestModel): GetRuleError | undefined { + return undefined; + } + + async makeGatewayRequest(requestModel: AuthenticatedRequestModel): Promise { + const { rucioAuthToken, id } = requestModel; + const dto: RuleMetaDTO = await this.gateway.getRule(rucioAuthToken, id); + return dto; + } + handleGatewayError(error: RuleMetaDTO): GetRuleError { + return { + status: 'error', + message: error.errorMessage ? error.errorMessage : 'Gateway Error', + name: `Gateway Error`, + code: error.errorCode, + } as GetRuleError; + } + + processDTO(dto: RuleMetaDTO): { data: GetRuleResponse | GetRuleError; status: 'success' | 'error' } { + const responseModel: GetRuleResponse = { + ...dto, + status: 'success', + }; + + return { + status: 'success', + data: responseModel, + }; + } +} diff --git a/src/lib/core/use-case/list-account-rse-quotas-usecase.ts b/src/lib/core/use-case/list-account-rse-quotas-usecase.ts index 6fab72777..724dd47cc 100644 --- a/src/lib/core/use-case/list-account-rse-quotas-usecase.ts +++ b/src/lib/core/use-case/list-account-rse-quotas-usecase.ts @@ -5,7 +5,7 @@ import { collectStreamedData } from '@/lib/sdk/utils'; import { injectable } from 'inversify'; import { Transform, Readable, PassThrough } from 'stream'; import { AccountRSELimitDTO, AccountRSEUsageDTO } from '../dto/account-dto'; -import { ListRSEsDTO, RSEDTO } from '../dto/rse-dto'; +import { ListRSEsDTO, RSEDetailsDTO } from '../dto/rse-dto'; import { DIDLong } from '../entity/rucio'; import { TAccountRSEUsageAndLimits, TRSESummaryRow } from '../entity/rule-summary'; import type { ListAccountRSEQuotasInputPort, ListAccountRSEQuotasOutputPort } from '../port/primary/list-account-rse-quotas-ports'; @@ -24,7 +24,7 @@ export default class ListAccountRSEQuotasUseCase AuthenticatedRequestModel, ListAccountRSEQuotasResponse, ListAccountRSEQuotasError, - RSEDTO, + RSEDetailsDTO, RSEAccountUsageLimitViewModel > implements ListAccountRSEQuotasInputPort @@ -133,7 +133,7 @@ export default class ListAccountRSEQuotasUseCase } } - processStreamedData(rse: RSEDTO): { data: ListAccountRSEQuotasResponse | ListAccountRSEQuotasError; status: 'success' | 'error' } { + processStreamedData(rse: RSEDetailsDTO): { data: ListAccountRSEQuotasResponse | ListAccountRSEQuotasError; status: 'success' | 'error' } { const quotaInfo: (TRSESummaryRow & { status: 'success' }) | BaseErrorResponseModel = getQuotaInfo( rse, this.account, diff --git a/src/lib/core/use-case/list-all-rses-usecase.ts b/src/lib/core/use-case/list-all-rses-usecase.ts index 25bdfe7d7..9ffc5bbcd 100644 --- a/src/lib/core/use-case/list-all-rses-usecase.ts +++ b/src/lib/core/use-case/list-all-rses-usecase.ts @@ -6,8 +6,7 @@ import { ListAllRSEsError, ListAllRSEsRequest, ListAllRSEsResponse } from '@/lib import { ListAllRSEsInputPort, type ListAllRSEsOutputPort } from '@/lib/core/port/primary/list-all-rses-ports'; import { RSEViewModel } from '@/lib/infrastructure/data/view-model/rse'; -import { ListRSEsDTO } from '@/lib/core/dto/rse-dto'; -import { RSEDTO } from '@/lib/core/dto/rse-dto'; +import { ListRSEsDTO, RSEDetailsDTO } from '@/lib/core/dto/rse-dto'; import type RSEGatewayOutputPort from '@/lib/core/port/secondary/rse-gateway-output-port'; @injectable() @@ -17,7 +16,7 @@ export default class ListAllRSEsUseCase ListAllRSEsResponse, ListAllRSEsError, ListRSEsDTO, - RSEDTO, + RSEDetailsDTO, RSEViewModel > implements ListAllRSEsInputPort @@ -51,7 +50,7 @@ export default class ListAllRSEsUseCase } as ListAllRSEsError; } - processStreamedData(dto: RSEDTO): { data: ListAllRSEsResponse | ListAllRSEsError; status: 'success' | 'error' } { + processStreamedData(dto: RSEDetailsDTO): { data: ListAllRSEsResponse | ListAllRSEsError; status: 'success' | 'error' } { if (dto.status === 'error') { const errorModel: ListAllRSEsError = { status: 'error', diff --git a/src/lib/core/use-case/list-rses/list-rses-usecase.ts b/src/lib/core/use-case/list-rses/list-rses-usecase.ts index a7e501c51..a65d7f9ed 100644 --- a/src/lib/core/use-case/list-rses/list-rses-usecase.ts +++ b/src/lib/core/use-case/list-rses/list-rses-usecase.ts @@ -7,7 +7,7 @@ import { RSEViewModel } from '@/lib/infrastructure/data/view-model/rse'; import type RSEGatewayOutputPort from '@/lib/core/port/secondary/rse-gateway-output-port'; import { ListRSEsDTO } from '@/lib/core/dto/rse-dto'; -import { RSEDTO } from '@/lib/core/dto/rse-dto'; +import { RSEDetailsDTO } from '@/lib/core/dto/rse-dto'; import { ListRSEsInputPort, type ListRSEsOutputPort } from '@/lib/core/port/primary/list-rses-ports'; @@ -20,7 +20,7 @@ export default class ListRSEsUseCase ListRSEsResponse, ListRSEsError, ListRSEsDTO, - RSEDTO, + RSEDetailsDTO, RSEViewModel > implements ListRSEsInputPort @@ -69,7 +69,7 @@ export default class ListRSEsUseCase } as ListRSEsError; } - processStreamedData(dto: RSEDTO): { data: ListRSEsResponse | ListRSEsError; status: 'success' | 'error' } { + processStreamedData(dto: RSEDetailsDTO): { data: ListRSEsResponse | ListRSEsError; status: 'success' | 'error' } { if (dto.status === 'error') { const errorModel: ListRSEsError = { status: 'error', diff --git a/src/lib/core/use-case/list-rses/pipeline-element-get-rse-pipeline-element.ts b/src/lib/core/use-case/list-rses/pipeline-element-get-rse-pipeline-element.ts index eadaebba0..07ac02b5c 100644 --- a/src/lib/core/use-case/list-rses/pipeline-element-get-rse-pipeline-element.ts +++ b/src/lib/core/use-case/list-rses/pipeline-element-get-rse-pipeline-element.ts @@ -1,7 +1,7 @@ import { BaseStreamingPostProcessingPipelineElement } from '@/lib/sdk/postprocessing-pipeline-elements'; import { AuthenticatedRequestModel } from '@/lib/sdk/usecase-models'; -import { RSEDTO, getEmptyRSEDTO } from '@/lib/core/dto/rse-dto'; +import { getEmptyRSEDetailsDTO, RSEDetailsDTO } from '@/lib/core/dto/rse-dto'; import RSEGatewayOutputPort from '@/lib/core/port/secondary/rse-gateway-output-port'; import { ListRSEsError, ListRSEsRequest, ListRSEsResponse } from '@/lib/core/usecase-models/list-rses-usecase-models'; @@ -9,27 +9,27 @@ export default class GetRSEPipelineElement extends BaseStreamingPostProcessingPi ListRSEsRequest, ListRSEsResponse, ListRSEsError, - RSEDTO + RSEDetailsDTO > { constructor(private gateway: RSEGatewayOutputPort) { super(); } - async makeGatewayRequest(requestModel: AuthenticatedRequestModel, responseModel: ListRSEsResponse): Promise { + async makeGatewayRequest(requestModel: AuthenticatedRequestModel, responseModel: ListRSEsResponse): Promise { try { const { rucioAuthToken } = requestModel; const rseName = responseModel.name; if (!rseName) { - const errorDTO: RSEDTO = getEmptyRSEDTO(); + const errorDTO: RSEDetailsDTO = getEmptyRSEDetailsDTO(); errorDTO.status = 'error'; errorDTO.errorCode = 400; errorDTO.errorName = 'Invalid Request'; errorDTO.errorMessage = 'RSE Name not found in response model'; return errorDTO; } - const dto: RSEDTO = await this.gateway.getRSE(rucioAuthToken, rseName); + const dto: RSEDetailsDTO = await this.gateway.getRSE(rucioAuthToken, rseName); return dto; } catch (error: any) { - const errorDTO: RSEDTO = getEmptyRSEDTO(); + const errorDTO: RSEDetailsDTO = getEmptyRSEDetailsDTO(); errorDTO.status = 'error'; errorDTO.errorCode = 500; errorDTO.errorName = 'Gateway Error'; @@ -38,7 +38,7 @@ export default class GetRSEPipelineElement extends BaseStreamingPostProcessingPi } } - handleGatewayError(dto: RSEDTO): ListRSEsError { + handleGatewayError(dto: RSEDetailsDTO): ListRSEsError { const errorModel: ListRSEsError = { status: 'error', name: dto.name, @@ -48,7 +48,7 @@ export default class GetRSEPipelineElement extends BaseStreamingPostProcessingPi return errorModel; } - transformResponseModel(responseModel: ListRSEsResponse, dto: RSEDTO): ListRSEsResponse { + transformResponseModel(responseModel: ListRSEsResponse, dto: RSEDetailsDTO): ListRSEsResponse { responseModel.id = dto.id; responseModel.deterministic = dto.deterministic; responseModel.rse_type = dto.rse_type; diff --git a/src/lib/core/use-case/list-rule-replica-lock-states-usecase.ts b/src/lib/core/use-case/list-rule-replica-lock-states-usecase.ts new file mode 100644 index 000000000..82a5415e8 --- /dev/null +++ b/src/lib/core/use-case/list-rule-replica-lock-states-usecase.ts @@ -0,0 +1,88 @@ +import { injectable } from 'inversify'; +import { BaseSingleEndpointStreamingUseCase } from '@/lib/sdk/usecase'; +import { AuthenticatedRequestModel } from '@/lib/sdk/usecase-models'; + +import { + ListRuleReplicaLockStatesError, + ListRuleReplicaLockStatesRequest, + ListRuleReplicaLockStatesResponse, +} from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import { + ListRuleReplicaLockStatesInputPort, + type ListRuleReplicaLockStatesOutputPort, +} from '@/lib/core/port/primary/list-rule-replica-lock-states-ports'; +import { ListRuleReplicaLockStatesViewModel } from '@/lib/infrastructure/data/view-model/rule'; + +import { RuleReplicaLockStateDTO, ListLocksDTO } from '@/lib/core/dto/rule-dto'; +import type RuleGatewayOutputPort from '@/lib/core/port/secondary/rule-gateway-output-port'; + +@injectable() +export default class ListRuleReplicaLockStatesUseCase + extends BaseSingleEndpointStreamingUseCase< + AuthenticatedRequestModel, + ListRuleReplicaLockStatesResponse, + ListRuleReplicaLockStatesError, + ListLocksDTO, + RuleReplicaLockStateDTO, + ListRuleReplicaLockStatesViewModel + > + implements ListRuleReplicaLockStatesInputPort +{ + constructor(protected readonly presenter: ListRuleReplicaLockStatesOutputPort, private readonly gateway: RuleGatewayOutputPort) { + super(presenter); + } + + validateRequestModel(requestModel: AuthenticatedRequestModel): ListRuleReplicaLockStatesError | undefined { + return undefined; + } + + async makeGatewayRequest(requestModel: AuthenticatedRequestModel): Promise { + const { rucioAuthToken, id } = requestModel; + return await this.gateway.listRuleReplicaLockStates(rucioAuthToken, id); + } + + handleGatewayError(error: RuleReplicaLockStateDTO): ListRuleReplicaLockStatesError { + return { + status: 'error', + error: `Gateway retuned with ${error.errorCode}: ${error.errorMessage}`, + message: error.errorMessage ? error.errorMessage : 'Gateway Error', + name: `Gateway Error`, + code: error.errorCode, + } as ListRuleReplicaLockStatesError; + } + + processStreamedData(dto: RuleReplicaLockStateDTO): { + data: ListRuleReplicaLockStatesResponse | ListRuleReplicaLockStatesError; + status: 'success' | 'error'; + } { + if (dto.status === 'error') { + const errorModel: ListRuleReplicaLockStatesError = { + status: 'error', + code: dto.errorCode || 500, + message: dto.errorMessage || 'Could not fetch or process the fetched data', + name: dto.errorName || 'Gateway Error', + }; + return { + status: 'error', + data: errorModel, + }; + } + + const responseModel: ListRuleReplicaLockStatesResponse = { + ...dto, + status: 'success', + ddm_link: '', + fts_link: '', + }; + return { + status: 'success', + data: responseModel, + }; + } + + intializeRequest( + request: AuthenticatedRequestModel>, + ): Promise { + return Promise.resolve(undefined); + } +} diff --git a/src/lib/core/usecase-models/get-rse-usecase-models.ts b/src/lib/core/usecase-models/get-rse-usecase-models.ts index f82eb95c3..092dc809f 100644 --- a/src/lib/core/usecase-models/get-rse-usecase-models.ts +++ b/src/lib/core/usecase-models/get-rse-usecase-models.ts @@ -1,5 +1,5 @@ import { BaseErrorResponseModel, BaseResponseModel } from '@/lib/sdk/usecase-models'; -import { RSE } from '@/lib/core/entity/rucio'; +import { RSE, RSEDetails } from '@/lib/core/entity/rucio'; /** * @interface GetRSERequest represents the RequestModel for get_rse usecase */ @@ -10,7 +10,7 @@ export interface GetRSERequest { /** * @interface GetRSEResponse represents the ResponseModel for get_rse usecase */ -export interface GetRSEResponse extends RSE, BaseResponseModel {} +export interface GetRSEResponse extends RSEDetails, BaseResponseModel {} /** * @interface GetRSEError represents the ErrorModel for get_rse usecase diff --git a/src/lib/core/usecase-models/get-rule-usecase-models.ts b/src/lib/core/usecase-models/get-rule-usecase-models.ts new file mode 100644 index 000000000..7cf44e098 --- /dev/null +++ b/src/lib/core/usecase-models/get-rule-usecase-models.ts @@ -0,0 +1,18 @@ +import { BaseErrorResponseModel, BaseResponseModel } from '@/lib/sdk/usecase-models'; +import { RuleMeta } from '@/lib/core/entity/rucio'; +/** + * @interface GetRuleRequest represents the RequestModel for get_rule usecase + */ +export interface GetRuleRequest { + id: string; +} + +/** + * @interface GetRuleResponse represents the ResponseModel for get_rule usecase + */ +export interface GetRuleResponse extends BaseResponseModel, RuleMeta {} + +/** + * @interface GetRuleError represents the ErrorModel for get_rule usecase + */ +export interface GetRuleError extends BaseErrorResponseModel {} diff --git a/src/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models.ts b/src/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models.ts new file mode 100644 index 000000000..9fda647f8 --- /dev/null +++ b/src/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models.ts @@ -0,0 +1,18 @@ +import { BaseErrorResponseModel, BaseResponseModel } from '@/lib/sdk/usecase-models'; +import { RulePageLockEntry } from '@/lib/core/entity/rucio'; +/** + * @interface ListRuleReplicaLockStatesRequest represents the RequestModel for list_rule_replica_lock_states usecase + */ +export interface ListRuleReplicaLockStatesRequest { + id: string; +} + +/** + * @interface ListRuleReplicaLockStatesResponse represents the ResponseModel for list_rule_replica_lock_states usecase + */ +export interface ListRuleReplicaLockStatesResponse extends BaseResponseModel, RulePageLockEntry {} + +/** + * @interface ListRuleReplicaLockStatesError represents the ErrorModel for list_rule_replica_lock_states usecase + */ +export interface ListRuleReplicaLockStatesError extends BaseErrorResponseModel {} diff --git a/src/lib/core/utils/create-rule-utils.ts b/src/lib/core/utils/create-rule-utils.ts index 7b4f36f32..248b78a1d 100644 --- a/src/lib/core/utils/create-rule-utils.ts +++ b/src/lib/core/utils/create-rule-utils.ts @@ -1,6 +1,6 @@ import { BaseErrorResponseModel } from '@/lib/sdk/usecase-models'; import { AccountRSELimitDTO, AccountRSEUsageDTO } from '../dto/account-dto'; -import { RSEDTO } from '../dto/rse-dto'; +import { RSEDetailsDTO } from '../dto/rse-dto'; import { DIDLong, DIDType, RSEAccountUsage } from '../entity/rucio'; import { TAccountRSEUsageAndLimits, TDIDSummaryRow, TRSESummaryRow } from '../entity/rule-summary'; import { BaseDTO } from '@/lib/sdk/dto'; @@ -83,7 +83,7 @@ export const createAccountRSEUsageAndLimitMap = ( * If the account has no limits specified, a BaseErrorResponseModel is returned. */ export const getQuotaInfo = ( - rse: RSEDTO, + rse: RSEDetailsDTO, account: string, accountRSEUsageAndLimits: TAccountRSEUsageAndLimits, totalDIDBytesRequested: number, diff --git a/src/lib/infrastructure/controller/get-rule-controller.ts b/src/lib/infrastructure/controller/get-rule-controller.ts new file mode 100644 index 000000000..ede8060c4 --- /dev/null +++ b/src/lib/infrastructure/controller/get-rule-controller.ts @@ -0,0 +1,27 @@ +import { injectable, inject } from 'inversify'; +import { NextApiResponse } from 'next'; + +import { AuthenticatedRequestModel } from '@/lib/sdk/usecase-models'; +import { BaseController, TAuthenticatedControllerParameters } from '@/lib/sdk/controller'; +import { GetRuleRequest } from '@/lib/core/usecase-models/get-rule-usecase-models'; +import { GetRuleInputPort } from '@/lib/core/port/primary/get-rule-ports'; +import USECASE_FACTORY from '@/lib/infrastructure/ioc/ioc-symbols-usecase-factory'; + +export type GetRuleControllerParameters = TAuthenticatedControllerParameters & { + id: string; +}; + +@injectable() +class GetRuleController extends BaseController> { + constructor(@inject(USECASE_FACTORY.GET_RULE) getRuleUseCaseFactory: (response: NextApiResponse) => GetRuleInputPort) { + super(getRuleUseCaseFactory); + } + prepareRequestModel(parameters: GetRuleControllerParameters): AuthenticatedRequestModel { + return { + rucioAuthToken: parameters.rucioAuthToken, + id: parameters.id, + }; + } +} + +export default GetRuleController; diff --git a/src/lib/infrastructure/controller/list-rule-replica-lock-states-controller.ts b/src/lib/infrastructure/controller/list-rule-replica-lock-states-controller.ts new file mode 100644 index 000000000..65101cebf --- /dev/null +++ b/src/lib/infrastructure/controller/list-rule-replica-lock-states-controller.ts @@ -0,0 +1,33 @@ +import { injectable, inject } from 'inversify'; +import { NextApiResponse } from 'next'; + +import { AuthenticatedRequestModel } from '@/lib/sdk/usecase-models'; +import { BaseController, TAuthenticatedControllerParameters } from '@/lib/sdk/controller'; +import { ListRuleReplicaLockStatesRequest } from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import { ListRuleReplicaLockStatesInputPort } from '@/lib/core/port/primary/list-rule-replica-lock-states-ports'; +import USECASE_FACTORY from '@/lib/infrastructure/ioc/ioc-symbols-usecase-factory'; + +export type ListRuleReplicaLockStatesControllerParameters = TAuthenticatedControllerParameters & { + id: string; +}; + +@injectable() +class ListRuleReplicaLockStatesController extends BaseController< + ListRuleReplicaLockStatesControllerParameters, + AuthenticatedRequestModel +> { + constructor( + @inject(USECASE_FACTORY.LIST_RULE_REPLICA_LOCK_STATES) + listRuleReplicaLockStatesUseCaseFactory: (response: NextApiResponse) => ListRuleReplicaLockStatesInputPort, + ) { + super(listRuleReplicaLockStatesUseCaseFactory); + } + prepareRequestModel(parameters: ListRuleReplicaLockStatesControllerParameters): AuthenticatedRequestModel { + return { + rucioAuthToken: parameters.rucioAuthToken, + id: parameters.id, + }; + } +} + +export default ListRuleReplicaLockStatesController; diff --git a/src/lib/infrastructure/data/view-model/rse.ts b/src/lib/infrastructure/data/view-model/rse.ts index fa815852f..b663e5db3 100644 --- a/src/lib/infrastructure/data/view-model/rse.ts +++ b/src/lib/infrastructure/data/view-model/rse.ts @@ -1,8 +1,10 @@ import { BaseViewModel } from '@/lib/sdk/view-models'; -import { RSE, RSEAttribute, RSEAccountUsage, RSEProtocol, RSEType } from '@/lib/core/entity/rucio'; +import { RSE, RSEAttribute, RSEAccountUsage, RSEProtocol, RSEType, RSEDetails } from '@/lib/core/entity/rucio'; export interface RSEViewModel extends RSE, BaseViewModel {} +export interface RSEDetailsViewModel extends RSEDetails, BaseViewModel {} + export interface RSEProtocolViewModel extends BaseViewModel { protocols: RSEProtocol[]; } @@ -53,6 +55,22 @@ export function generateEmptyRSEViewModel(): RSEViewModel { } as RSEViewModel; } +export function generateEmptyRSEDetailsViewModel(): RSEDetailsViewModel { + return { + status: 'error', + id: '', + name: '', + rse_type: RSEType.UNKNOWN, + volatile: false, + deterministic: false, + staging_area: false, + protocols: [], + availability_delete: false, + availability_read: false, + availability_write: false, + } as RSEDetailsViewModel; +} + export function generateEmptyRSEProtocolViewModel(): RSEProtocolViewModel { return { status: 'error', diff --git a/src/lib/infrastructure/data/view-model/rule.ts b/src/lib/infrastructure/data/view-model/rule.ts index 20737cfc7..9c740ecd6 100644 --- a/src/lib/infrastructure/data/view-model/rule.ts +++ b/src/lib/infrastructure/data/view-model/rule.ts @@ -1,4 +1,4 @@ -import { Rule, RuleMeta, RulePageLockEntry, RuleState } from '@/lib/core/entity/rucio'; +import { DIDType, LockState, Rule, RuleGrouping, RuleMeta, RuleNotification, RulePageLockEntry, RuleState } from '@/lib/core/entity/rucio'; import { BaseViewModel } from '@/lib/sdk/view-models'; import { RSEAccountUsageLimitViewModel } from '@/lib/infrastructure/data/view-model/rse'; import { ListDIDsViewModel } from '@/lib/infrastructure/data/view-model/list-did'; @@ -82,3 +82,47 @@ export const getEmptyCreateRuleViewModel = (): CreateRuleViewModel => { rule_ids: [], }; }; + +export interface GetRuleViewModel extends BaseViewModel, RuleMeta {} +export const getEmptyGetRuleViewModel = (): GetRuleViewModel => { + return { + status: 'error', + account: '', + activity: '', + copies: 0, + created_at: '', + did_type: DIDType.UNKNOWN, + expires_at: null, + grouping: RuleGrouping.NONE, + id: '', + ignore_account_limit: false, + ignore_availability: false, + locked: false, + locks_ok_cnt: 0, + locks_replicating_cnt: 0, + locks_stuck_cnt: 0, + name: '', + notification: RuleNotification.No, + priority: 0, + purge_replicas: false, + rse_expression: '', + scope: '', + split_container: false, + state: RuleState.UNKNOWN, + updated_at: '', + }; +}; + +export interface ListRuleReplicaLockStatesViewModel extends BaseViewModel, RulePageLockEntry {} + +export const getEmptyListRuleReplicaLockStatesViewModel = (): ListRuleReplicaLockStatesViewModel => { + return { + ddm_link: '', + fts_link: '', + name: '', + rse: '', + scope: '', + state: LockState.UNKNOWN, + status: 'error', + }; +}; diff --git a/src/lib/infrastructure/gateway/rse-gateway/endpoints/get-rse-endpoint.ts b/src/lib/infrastructure/gateway/rse-gateway/endpoints/get-rse-endpoint.ts index 1844d1d9b..8f75d9a3c 100644 --- a/src/lib/infrastructure/gateway/rse-gateway/endpoints/get-rse-endpoint.ts +++ b/src/lib/infrastructure/gateway/rse-gateway/endpoints/get-rse-endpoint.ts @@ -1,10 +1,10 @@ -import { RSEDTO } from '@/lib/core/dto/rse-dto'; -import { RSEType } from '@/lib/core/entity/rucio'; +import { RSEDetailsDTO } from '@/lib/core/dto/rse-dto'; +import { RSEDetails, RSEType } from '@/lib/core/entity/rucio'; import { BaseEndpoint } from '@/lib/sdk/gateway-endpoints'; import { HTTPRequest } from '@/lib/sdk/http'; import { Response } from 'node-fetch'; -export default class GetRSEEndpoint extends BaseEndpoint { +export default class GetRSEEndpoint extends BaseEndpoint { constructor(private rucioAuthToken: string, private rseName: string) { super(); } @@ -32,11 +32,15 @@ export default class GetRSEEndpoint extends BaseEndpoint { /** * @implements */ - async reportErrors(statusCode: number, response: Response): Promise { - const dto: RSEDTO = { + async reportErrors(statusCode: number, response: Response): Promise { + const dto: RSEDetailsDTO = { status: 'error', errorCode: statusCode, errorType: 'gateway_endpoint_error', + protocols: [], + availability_write: false, + availability_delete: false, + availability_read: false, id: '', name: this.rseName, rse_type: RSEType.UNKNOWN, @@ -60,15 +64,8 @@ export default class GetRSEEndpoint extends BaseEndpoint { /** * @implements */ - createDTO(data: any): RSEDTO { - data = data as { - id: string; - rse: string; - rse_type: string; - volatile: boolean; - deterministic: boolean; - staging_area: boolean; - }; + createDTO(data: any): RSEDetailsDTO { + data = data as RSEDetails; switch (data.rse_type.toUpperCase()) { case 'DISK': @@ -81,7 +78,7 @@ export default class GetRSEEndpoint extends BaseEndpoint { data.rse_type = RSEType.UNKNOWN; break; } - const dto: RSEDTO = { + const dto: RSEDetailsDTO = { status: 'success', id: data.id, name: data.rse, @@ -89,6 +86,10 @@ export default class GetRSEEndpoint extends BaseEndpoint { volatile: data.volatile, deterministic: data.deterministic, staging_area: data.staging_area, + protocols: data.protocols, + availability_write: data.availability_write, + availability_read: data.availability_read, + availability_delete: data.availability_delete, }; return dto; } diff --git a/src/lib/infrastructure/gateway/rse-gateway/endpoints/list-rses-endpoint.ts b/src/lib/infrastructure/gateway/rse-gateway/endpoints/list-rses-endpoint.ts index 9f8a272f3..d6a7cf467 100644 --- a/src/lib/infrastructure/gateway/rse-gateway/endpoints/list-rses-endpoint.ts +++ b/src/lib/infrastructure/gateway/rse-gateway/endpoints/list-rses-endpoint.ts @@ -1,10 +1,10 @@ -import { ListRSEsDTO, RSEDTO } from '@/lib/core/dto/rse-dto'; +import { ListRSEsDTO, RSEDetailsDTO } from '@/lib/core/dto/rse-dto'; import { BaseStreamableEndpoint } from '@/lib/sdk/gateway-endpoints'; import { HTTPRequest } from '@/lib/sdk/http'; import { Response } from 'node-fetch'; import { convertToRSEDTO, TRucioRSE } from '../rse-gateway-utils'; -export default class ListRSEsEndpoint extends BaseStreamableEndpoint { +export default class ListRSEsEndpoint extends BaseStreamableEndpoint { constructor(private readonly rucioAuthToken: string, private readonly rseExpression: string) { super(true); } @@ -53,9 +53,9 @@ export default class ListRSEsEndpoint extends BaseStreamableEndpoint { + async getRSE(rucioAuthToken: string, rseName: string): Promise { const endpoint = new GetRSEEndpoint(rucioAuthToken, rseName); const dto = await endpoint.fetch(); return dto; diff --git a/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts b/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts index 5e6a7c83c..fadf8c286 100644 --- a/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts +++ b/src/lib/infrastructure/gateway/rule-gateway/endpoints/get-rule-endpoints.ts @@ -1,10 +1,10 @@ -import { RuleDTO } from '@/lib/core/dto/rule-dto'; +import { RuleMetaDTO } from '@/lib/core/dto/rule-dto'; import { BaseEndpoint } from '@/lib/sdk/gateway-endpoints'; import { HTTPRequest } from '@/lib/sdk/http'; -import { convertToRuleDTO, getEmptyRuleDTO, TRucioRule } from '../rule-gateway-utils'; +import { convertToRuleMetaDTO, getEmptyRuleMetaDTO, TRucioRule } from '../rule-gateway-utils'; import { Response } from 'node-fetch'; -export default class GetRuleEndpoint extends BaseEndpoint { +export default class GetRuleEndpoint extends BaseEndpoint { constructor(private readonly rucioAuthToken: string, private readonly ruleId: string) { super(); } @@ -31,10 +31,10 @@ export default class GetRuleEndpoint extends BaseEndpoint { * @param response The reponse containing error data * @returns */ - async reportErrors(statusCode: number, response: Response): Promise { + async reportErrors(statusCode: number, response: Response): Promise { const data = await response.json(); - const errorDTO: RuleDTO = { - ...getEmptyRuleDTO(), + const errorDTO: RuleMetaDTO = { + ...getEmptyRuleMetaDTO(), status: 'error', errorMessage: data, errorCode: statusCode, @@ -49,8 +49,8 @@ export default class GetRuleEndpoint extends BaseEndpoint { * @param response The individual RSE object streamed from Rucio * @returns The RSEDTO object */ - createDTO(data: TRucioRule): RuleDTO { - const dto: RuleDTO = convertToRuleDTO(data); + createDTO(data: TRucioRule): RuleMetaDTO { + const dto: RuleMetaDTO = convertToRuleMetaDTO(data); return dto; } } diff --git a/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts b/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts index 79b78a68e..5ceb53864 100644 --- a/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts +++ b/src/lib/infrastructure/gateway/rule-gateway/endpoints/list-rule-replica-lock-states-endpoint.ts @@ -1,10 +1,9 @@ -import { RuleReplicaLockStateDTO } from '@/lib/core/dto/rule-dto'; -import { BaseStreamableDTO } from '@/lib/sdk/dto'; +import { ListLocksDTO, RuleReplicaLockStateDTO } from '@/lib/core/dto/rule-dto'; import { BaseStreamableEndpoint } from '@/lib/sdk/gateway-endpoints'; import { HTTPRequest } from '@/lib/sdk/http'; import { Response } from 'node-fetch'; import { convertToRuleReplicaLockDTO, TRucioRuleReplicaLock } from '../rule-gateway-utils'; -export default class ListRuleReplicaLockStatesEndpoint extends BaseStreamableEndpoint { +export default class ListRuleReplicaLockStatesEndpoint extends BaseStreamableEndpoint { constructor(private readonly rucioAuthToken: string, private readonly ruleId: string) { super(true); } @@ -31,9 +30,9 @@ export default class ListRuleReplicaLockStatesEndpoint extends BaseStreamableEnd * @param response The reponse containing error data * @returns */ - async reportErrors(statusCode: number, response: Response): Promise { + async reportErrors(statusCode: number, response: Response): Promise { const data = await response.json(); - const errorDTO: BaseStreamableDTO = { + const errorDTO: ListLocksDTO = { status: 'error', errorMessage: data, errorCode: statusCode, diff --git a/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts index f7ad0fc54..b107b21ff 100644 --- a/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts +++ b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway-utils.ts @@ -1,5 +1,5 @@ -import { RuleDTO, RuleReplicaLockStateDTO } from '@/lib/core/dto/rule-dto'; -import { DateISO, LockState, RuleState } from '@/lib/core/entity/rucio'; +import { RuleDTO, RuleMetaDTO, RuleReplicaLockStateDTO } from '@/lib/core/dto/rule-dto'; +import { DIDType, LockState, RuleGrouping, RuleNotification, RuleState } from '@/lib/core/entity/rucio'; export type TRucioRule = { error: null | string; @@ -100,6 +100,75 @@ export function convertToRuleDTO(rule: TRucioRule): RuleDTO { }; } +function getDIDType(type: string): DIDType { + const cleanType = type.trim().toUpperCase(); + switch (cleanType) { + case 'FILE': + return DIDType.FILE; + case 'DATASET': + return DIDType.DATASET; + case 'CONTAINER': + return DIDType.CONTAINER; + default: + return DIDType.UNKNOWN; + } +} + +function getRuleNotification(notification: string): RuleNotification { + const cleanNotification = notification.trim().toUpperCase(); + switch (cleanNotification) { + case 'YES': + return RuleNotification.Yes; + case 'CLOSE': + return RuleNotification.Close; + case 'PROGRESS': + return RuleNotification.Progress; + default: + return RuleNotification.No; + } +} + +function getRuleGrouping(grouping: string): RuleGrouping { + const cleanGrouping = grouping.trim().toUpperCase(); + switch (cleanGrouping) { + case 'ALL': + return RuleGrouping.ALL; + case 'DATASET': + return RuleGrouping.DATASET; + default: + return RuleGrouping.NONE; + } +} + +export function convertToRuleMetaDTO(rule: TRucioRule): RuleMetaDTO { + return { + status: 'success', + account: rule.account, + activity: rule.activity, + copies: rule.copies, + created_at: rule.created_at, + did_type: getDIDType(rule.did_type), + expires_at: rule.expires_at, + grouping: getRuleGrouping(rule.grouping), + id: rule.id, + ignore_account_limit: rule.ignore_account_limit, + ignore_availability: rule.ignore_availability, + locked: rule.locked, + locks_ok_cnt: rule.locks_ok_cnt, + locks_replicating_cnt: rule.locks_replicating_cnt, + locks_stuck_cnt: rule.locks_stuck_cnt, + name: rule.name, + notification: getRuleNotification(rule.notification), + priority: rule.priority, + purge_replicas: rule.purge_replicas, + rse_expression: rule.rse_expression, + scope: rule.scope, + split_container: rule.split_container, + state: getRuleState(rule.state), + updated_at: rule.updated_at, + }; +} + export function convertToRuleReplicaLockDTO(ruleReplicaLockState: TRucioRuleReplicaLock): RuleReplicaLockStateDTO { return { status: 'success', @@ -109,6 +178,7 @@ export function convertToRuleReplicaLockDTO(ruleReplicaLockState: TRucioRuleRepl state: getReplicaLockState(ruleReplicaLockState.state), }; } + export function getEmptyRuleDTO(): RuleDTO { return { status: 'error', @@ -126,6 +196,35 @@ export function getEmptyRuleDTO(): RuleDTO { }; } +export function getEmptyRuleMetaDTO(): RuleMetaDTO { + return { + status: 'error', + account: '', + activity: '', + copies: 0, + created_at: '', + did_type: DIDType.UNKNOWN, + expires_at: '', + grouping: RuleGrouping.NONE, + id: '', + ignore_account_limit: false, + ignore_availability: false, + locked: false, + locks_ok_cnt: 0, + locks_replicating_cnt: 0, + locks_stuck_cnt: 0, + name: '', + notification: RuleNotification.No, + priority: 0, + purge_replicas: false, + rse_expression: '', + scope: '', + split_container: false, + state: RuleState.UNKNOWN, + updated_at: '', + }; +} + export function getEmptyRuleReplicaLockDTO(): RuleReplicaLockStateDTO { return { status: 'error', diff --git a/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts index 4b7d06e06..e019ff856 100644 --- a/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts +++ b/src/lib/infrastructure/gateway/rule-gateway/rule-gateway.ts @@ -1,4 +1,4 @@ -import { CreateRuleDTO, ListRulesDTO, RuleDTO } from '@/lib/core/dto/rule-dto'; +import { CreateRuleDTO, ListLocksDTO, ListRulesDTO, RuleMetaDTO } from '@/lib/core/dto/rule-dto'; import RuleGatewayOutputPort from '@/lib/core/port/secondary/rule-gateway-output-port'; import { BaseStreamableDTO } from '@/lib/sdk/dto'; import { injectable } from 'inversify'; @@ -11,7 +11,7 @@ import CreateRuleEndpoint from '@/lib/infrastructure/gateway/rule-gateway/endpoi @injectable() export default class RuleGateway implements RuleGatewayOutputPort { - async getRule(rucioAuthToken: string, ruleId: string): Promise { + async getRule(rucioAuthToken: string, ruleId: string): Promise { const endpoint = new GetRuleEndpoint(rucioAuthToken, ruleId); const dto = await endpoint.fetch(); return dto; @@ -39,7 +39,7 @@ export default class RuleGateway implements RuleGatewayOutputPort { } } - async listRuleReplicaLockStates(rucioAuthToken: string, ruleId: string): Promise { + async listRuleReplicaLockStates(rucioAuthToken: string, ruleId: string): Promise { try { const endpoint = new ListRuleReplicaLockStatesEndpoint(rucioAuthToken, ruleId); const errorDTO: BaseStreamableDTO | undefined = await endpoint.fetch(); diff --git a/src/lib/infrastructure/ioc/container-config.ts b/src/lib/infrastructure/ioc/container-config.ts index a13bb7bc5..7af3a3d4c 100644 --- a/src/lib/infrastructure/ioc/container-config.ts +++ b/src/lib/infrastructure/ioc/container-config.ts @@ -69,6 +69,8 @@ import CreateRuleFeature from '@/lib/infrastructure/ioc/features/create-rule-fea import AddDIDFeature from '@/lib/infrastructure/ioc/features/add-did-feature'; import AttachDIDsFeature from '@/lib/infrastructure/ioc/features/attach-dids-feature'; import SetDIDStatusFeature from '@/lib/infrastructure/ioc/features/set-did-status-feature'; +import GetRuleFeature from '@/lib/infrastructure/ioc/features/get-rule-feature'; +import ListRuleReplicaLockStatesFeature from '@/lib/infrastructure/ioc/features/list-rule-replica-lock-states-feature'; /** * IoC Container configuration for the application. @@ -102,6 +104,7 @@ loadFeaturesSync(appContainer, [ new ListDatasetReplicasFeature(appContainer), new ListFileReplicasFeature(appContainer), new ListSubscriptionsFeature(appContainer), + new GetRuleFeature(appContainer), ]); // Features: Page RSE @@ -131,6 +134,7 @@ loadFeaturesSync(appContainer, [new ListSubscriptionRuleStatesFeature(appContain // Features: List Rules loadFeaturesSync(appContainer, [new ListRulesFeature(appContainer)]); +loadFeaturesSync(appContainer, [new ListRuleReplicaLockStatesFeature(appContainer)]); // Features: Dashboard loadFeaturesSync(appContainer, [new ListAccountRSEUsageFeature(appContainer)]); diff --git a/src/lib/infrastructure/ioc/features/get-rule-feature.ts b/src/lib/infrastructure/ioc/features/get-rule-feature.ts new file mode 100644 index 000000000..ea60f1c05 --- /dev/null +++ b/src/lib/infrastructure/ioc/features/get-rule-feature.ts @@ -0,0 +1,35 @@ +import RuleGatewayOutputPort from '@/lib/core/port/secondary/rule-gateway-output-port'; +import { GetRuleError, GetRuleRequest, GetRuleResponse } from '@/lib/core/usecase-models/get-rule-usecase-models'; +import { GetRuleControllerParameters } from '@/lib/infrastructure/controller/get-rule-controller'; +import GetRuleController from '@/lib/infrastructure/controller/get-rule-controller'; +import { GetRuleViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { BaseFeature, IOCSymbols } from '@/lib/sdk/ioc-helpers'; +import GATEWAYS from '@/lib/infrastructure/ioc/ioc-symbols-gateway'; +import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'; +import INPUT_PORT from '@/lib/infrastructure/ioc/ioc-symbols-input-port'; +import USECASE_FACTORY from '@/lib/infrastructure/ioc/ioc-symbols-usecase-factory'; +import { Container } from 'inversify'; + +import GetRuleUseCase from '@/lib/core/use-case/get-rule-usecase'; + +import GetRulePresenter from '@/lib/infrastructure/presenter/get-rule-presenter'; + +export default class GetRuleFeature extends BaseFeature< + GetRuleControllerParameters, + GetRuleRequest, + GetRuleResponse, + GetRuleError, + GetRuleViewModel +> { + constructor(appContainer: Container) { + const rucioRuleGateway = appContainer.get(GATEWAYS.RULE); + + const symbols: IOCSymbols = { + CONTROLLER: CONTROLLERS.GET_RULE, + USECASE_FACTORY: USECASE_FACTORY.GET_RULE, + INPUT_PORT: INPUT_PORT.GET_RULE, + }; + const useCaseConstructorArgs = [rucioRuleGateway]; + super('GetRule', GetRuleController, GetRuleUseCase, useCaseConstructorArgs, GetRulePresenter, false, symbols); + } +} diff --git a/src/lib/infrastructure/ioc/features/list-rule-replica-lock-states-feature.ts b/src/lib/infrastructure/ioc/features/list-rule-replica-lock-states-feature.ts new file mode 100644 index 000000000..25e4b37d4 --- /dev/null +++ b/src/lib/infrastructure/ioc/features/list-rule-replica-lock-states-feature.ts @@ -0,0 +1,47 @@ +import RuleGatewayOutputPort from '@/lib/core/port/secondary/rule-gateway-output-port'; +import { + ListRuleReplicaLockStatesError, + ListRuleReplicaLockStatesRequest, + ListRuleReplicaLockStatesResponse, +} from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import { ListRuleReplicaLockStatesControllerParameters } from '@/lib/infrastructure/controller/list-rule-replica-lock-states-controller'; +import ListRuleReplicaLockStatesController from '@/lib/infrastructure/controller/list-rule-replica-lock-states-controller'; +import { ListRuleReplicaLockStatesViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { BaseStreamableFeature, IOCSymbols } from '@/lib/sdk/ioc-helpers'; +import GATEWAYS from '@/lib/infrastructure/ioc/ioc-symbols-gateway'; +import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'; +import INPUT_PORT from '@/lib/infrastructure/ioc/ioc-symbols-input-port'; +import USECASE_FACTORY from '@/lib/infrastructure/ioc/ioc-symbols-usecase-factory'; +import { Container } from 'inversify'; + +import ListRuleReplicaLockStatesUseCase from '@/lib/core/use-case/list-rule-replica-lock-states-usecase'; + +import ListRuleReplicaLockStatesPresenter from '@/lib/infrastructure/presenter/list-rule-replica-lock-states-presenter'; + +export default class ListRuleReplicaLockStatesFeature extends BaseStreamableFeature< + ListRuleReplicaLockStatesControllerParameters, + ListRuleReplicaLockStatesRequest, + ListRuleReplicaLockStatesResponse, + ListRuleReplicaLockStatesError, + ListRuleReplicaLockStatesViewModel +> { + constructor(appContainer: Container) { + const rucioRuleGateway = appContainer.get(GATEWAYS.RULE); + + const symbols: IOCSymbols = { + CONTROLLER: CONTROLLERS.LIST_RULE_REPLICA_LOCK_STATES, + USECASE_FACTORY: USECASE_FACTORY.LIST_RULE_REPLICA_LOCK_STATES, + INPUT_PORT: INPUT_PORT.LIST_RULE_REPLICA_LOCK_STATES, + }; + const useCaseConstructorArgs = [rucioRuleGateway]; + super( + 'ListRuleReplicaLockStates', + ListRuleReplicaLockStatesController, + ListRuleReplicaLockStatesUseCase, + useCaseConstructorArgs, + ListRuleReplicaLockStatesPresenter, + false, + symbols, + ); + } +} diff --git a/src/lib/infrastructure/ioc/ioc-symbols-controllers.ts b/src/lib/infrastructure/ioc/ioc-symbols-controllers.ts index 5e513e83b..18ebe3e33 100644 --- a/src/lib/infrastructure/ioc/ioc-symbols-controllers.ts +++ b/src/lib/infrastructure/ioc/ioc-symbols-controllers.ts @@ -36,6 +36,8 @@ const CONTROLLERS = { ADD_DID: Symbol.for('AddDIDController'), ATTACH_DIDS: Symbol.for('AttachDIDsController'), SET_DID_STATUS: Symbol.for('SetDIDStatusController'), + GET_RULE: Symbol.for('GetRuleController'), + LIST_RULE_REPLICA_LOCK_STATES: Symbol.for('ListRuleReplicaLockStatesController'), }; export default CONTROLLERS; diff --git a/src/lib/infrastructure/ioc/ioc-symbols-input-port.ts b/src/lib/infrastructure/ioc/ioc-symbols-input-port.ts index 12645e1dc..88e948f42 100644 --- a/src/lib/infrastructure/ioc/ioc-symbols-input-port.ts +++ b/src/lib/infrastructure/ioc/ioc-symbols-input-port.ts @@ -35,6 +35,8 @@ const INPUT_PORT = { ADD_DID: Symbol.for('AddDIDInputPort'), ATTACH_DIDS: Symbol.for('AttachDIDsInputPort'), SET_DID_STATUS: Symbol.for('SetDIDStatusInputPort'), + GET_RULE: Symbol.for('GetRuleInputPort'), + LIST_RULE_REPLICA_LOCK_STATES: Symbol.for('ListRuleReplicaLockStatesInputPort'), }; export default INPUT_PORT; diff --git a/src/lib/infrastructure/ioc/ioc-symbols-usecase-factory.ts b/src/lib/infrastructure/ioc/ioc-symbols-usecase-factory.ts index 0773a0ef6..3192ae3da 100644 --- a/src/lib/infrastructure/ioc/ioc-symbols-usecase-factory.ts +++ b/src/lib/infrastructure/ioc/ioc-symbols-usecase-factory.ts @@ -36,6 +36,8 @@ const USECASE_FACTORY = { ADD_DID: Symbol.for('Factory'), ATTACH_DIDS: Symbol.for('Factory'), SET_DID_STATUS: Symbol.for('Factory'), + GET_RULE: Symbol.for('Factory'), + LIST_RULE_REPLICA_LOCK_STATES: Symbol.for('Factory'), }; export default USECASE_FACTORY; diff --git a/src/lib/infrastructure/presenter/get-rse-presenter.ts b/src/lib/infrastructure/presenter/get-rse-presenter.ts index 7bf402029..b2322a41d 100644 --- a/src/lib/infrastructure/presenter/get-rse-presenter.ts +++ b/src/lib/infrastructure/presenter/get-rse-presenter.ts @@ -1,10 +1,10 @@ import { BasePresenter } from '@/lib/sdk/presenter'; import { GetRSEError, GetRSEResponse } from '@/lib/core/usecase-models/get-rse-usecase-models'; -import { generateEmptyRSEViewModel, RSEViewModel } from '@/lib/infrastructure/data/view-model/rse'; +import { generateEmptyRSEDetailsViewModel, RSEDetailsViewModel } from '@/lib/infrastructure/data/view-model/rse'; -export default class GetRSEPresenter extends BasePresenter { - convertResponseModelToViewModel(responseModel: GetRSEResponse): { viewModel: RSEViewModel; status: number } { - const viewModel: RSEViewModel = { +export default class GetRSEPresenter extends BasePresenter { + convertResponseModelToViewModel(responseModel: GetRSEResponse): { viewModel: RSEDetailsViewModel; status: number } { + const viewModel: RSEDetailsViewModel = { ...responseModel, }; return { @@ -13,8 +13,8 @@ export default class GetRSEPresenter extends BasePresenter { + convertResponseModelToViewModel(responseModel: GetRuleResponse): { viewModel: GetRuleViewModel; status: number } { + const viewModel: GetRuleViewModel = { + ...responseModel, + }; + return { + status: 200, + viewModel: viewModel, + }; + } + + convertErrorModelToViewModel(errorModel: GetRuleError): { viewModel: GetRuleViewModel; status: number } { + const viewModel: GetRuleViewModel = getEmptyGetRuleViewModel(); + const message = errorModel.message || errorModel.name; + viewModel.message = message; + const errorCode = errorModel.code || 500; + return { + status: errorCode, + viewModel: viewModel, + }; + } +} diff --git a/src/lib/infrastructure/presenter/list-rule-replica-lock-states-presenter.ts b/src/lib/infrastructure/presenter/list-rule-replica-lock-states-presenter.ts new file mode 100644 index 000000000..20bee13a9 --- /dev/null +++ b/src/lib/infrastructure/presenter/list-rule-replica-lock-states-presenter.ts @@ -0,0 +1,51 @@ +import { + ListRuleReplicaLockStatesError, + ListRuleReplicaLockStatesResponse, +} from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import { NextApiResponse } from 'next'; +import { ListRuleReplicaLockStatesViewModel, getEmptyListRuleReplicaLockStatesViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { BaseStreamingPresenter } from '@/lib/sdk/presenter'; +import { ListRuleReplicaLockStatesOutputPort } from '@/lib/core/port/primary/list-rule-replica-lock-states-ports'; + +export default class ListRuleReplicaLockStatesPresenter + extends BaseStreamingPresenter + implements ListRuleReplicaLockStatesOutputPort +{ + response: NextApiResponse; + + constructor(response: NextApiResponse) { + super(response); + this.response = response; + } + + streamResponseModelToViewModel(responseModel: ListRuleReplicaLockStatesResponse): ListRuleReplicaLockStatesViewModel { + const viewModel: ListRuleReplicaLockStatesViewModel = { + ...responseModel, + }; + return viewModel; + } + + streamErrorModelToViewModel(error: ListRuleReplicaLockStatesError): ListRuleReplicaLockStatesViewModel { + const errorViewModel: ListRuleReplicaLockStatesViewModel = getEmptyListRuleReplicaLockStatesViewModel(); + errorViewModel.status = 'error'; + errorViewModel.message = `${error.name}: ${error.message}`; + return errorViewModel; + } + + /** + * Converts an error model to an error view model. + * @param errorModel The error model to convert. + * @returns The error view model that represents the error model. + */ + convertErrorModelToViewModel(errorModel: ListRuleReplicaLockStatesError): { viewModel: ListRuleReplicaLockStatesViewModel; status: number } { + const viewModel: ListRuleReplicaLockStatesViewModel = getEmptyListRuleReplicaLockStatesViewModel(); + const message = errorModel.message ? errorModel.message.toString() : errorModel.name; + const name = errorModel.name ? errorModel.name : ''; + viewModel.message = message; + const errorCode = errorModel.code ? errorModel.code : 500; + return { + status: errorCode, + viewModel: viewModel, + }; + } +} diff --git a/src/pages/api/feature/get-rule.ts b/src/pages/api/feature/get-rule.ts new file mode 100644 index 000000000..c3b883d55 --- /dev/null +++ b/src/pages/api/feature/get-rule.ts @@ -0,0 +1,34 @@ +import { SessionUser } from '@/lib/core/entity/auth-models'; + +import { withAuthenticatedSessionRoute } from '@/lib/infrastructure/auth/session-utils'; +import { GetRuleControllerParameters } from '@/lib/infrastructure/controller/get-rule-controller'; +import appContainer from '@/lib/infrastructure/ioc/container-config'; +import { GetRuleRequest } from '@/lib/core/usecase-models/get-rule-usecase-models'; +import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'; +import { BaseController } from '@/lib/sdk/controller'; +import { NextApiRequest, NextApiResponse } from 'next'; + +async function getRule(req: NextApiRequest, res: NextApiResponse, rucioAuthToken: string, sessionUser?: SessionUser) { + if (req.method !== 'GET') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + const { id } = req.query; + + if (!id || typeof id !== 'string') { + res.status(400).json({ error: 'Rule ID should be specified' }); + return; + } + + const controllerParameters: GetRuleControllerParameters = { + response: res, + rucioAuthToken: rucioAuthToken, + id, + }; + + const controller = appContainer.get>(CONTROLLERS.GET_RULE); + await controller.execute(controllerParameters); +} + +export default withAuthenticatedSessionRoute(getRule); diff --git a/src/pages/api/feature/list-rule-replica-lock-states.ts b/src/pages/api/feature/list-rule-replica-lock-states.ts new file mode 100644 index 000000000..02724b689 --- /dev/null +++ b/src/pages/api/feature/list-rule-replica-lock-states.ts @@ -0,0 +1,36 @@ +import { SessionUser } from '@/lib/core/entity/auth-models'; + +import { withAuthenticatedSessionRoute } from '@/lib/infrastructure/auth/session-utils'; +import { ListRuleReplicaLockStatesControllerParameters } from '@/lib/infrastructure/controller/list-rule-replica-lock-states-controller'; +import appContainer from '@/lib/infrastructure/ioc/container-config'; +import { ListRuleReplicaLockStatesRequest } from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'; +import { BaseController } from '@/lib/sdk/controller'; +import { NextApiRequest, NextApiResponse } from 'next'; + +async function listRuleReplicaLockStates(req: NextApiRequest, res: NextApiResponse, rucioAuthToken: string, sessionUser?: SessionUser) { + if (req.method !== 'GET') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + const { id } = req.query; + + if (!id || typeof id !== 'string') { + res.status(400).json({ error: 'Rule ID should be specified' }); + return; + } + + const controllerParameters: ListRuleReplicaLockStatesControllerParameters = { + response: res, + rucioAuthToken: rucioAuthToken, + id, + }; + + const controller = appContainer.get>( + CONTROLLERS.LIST_RULE_REPLICA_LOCK_STATES, + ); + await controller.execute(controllerParameters); +} + +export default withAuthenticatedSessionRoute(listRuleReplicaLockStates); diff --git a/test/api/rule/get-rule.test.ts b/test/api/rule/get-rule.test.ts new file mode 100644 index 000000000..feceaa58d --- /dev/null +++ b/test/api/rule/get-rule.test.ts @@ -0,0 +1,105 @@ +import appContainer from '@/lib/infrastructure/ioc/container-config'; +import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'; +import { BaseController } from '@/lib/sdk/controller'; +import { NextApiResponse } from 'next'; +import { createHttpMocks } from 'test/fixtures/http-fixtures'; +import MockRucioServerFactory, { MockEndpoint } from 'test/fixtures/rucio-server'; +import { DIDType, RuleGrouping, RuleNotification, RuleState } from '@/lib/core/entity/rucio'; +import { GetRuleViewModel } from '@/lib/infrastructure/data/view-model/rule'; +import { GetRuleRequest } from '@/lib/core/usecase-models/get-rule-usecase-models'; +import { GetRuleControllerParameters } from '@/lib/infrastructure/controller/get-rule-controller'; + +describe('GET Rule API Test', () => { + const expectedViewModel: GetRuleViewModel = { + status: 'success', + locks_stuck_cnt: 0, + ignore_account_limit: false, + ignore_availability: false, + rse_expression: 'XRD3', + created_at: 'Mon, 27 Nov 2023 17:57:44 UTC', + account: 'root', + copies: 1, + activity: 'User Subscriptions', + priority: 3, + updated_at: 'Mon, 27 Nov 2023 17:57:44 UTC', + scope: 'test', + expires_at: null, + grouping: RuleGrouping.DATASET, + name: 'container', + notification: RuleNotification.No, + did_type: DIDType.CONTAINER, + locked: false, + state: RuleState.REPLICATING, + locks_ok_cnt: 0, + purge_replicas: false, + id: '817b3030097446a38b3b842bf528e112', + locks_replicating_cnt: 4, + split_container: false, + }; + + beforeEach(() => { + fetchMock.doMock(); + const getRuleEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/rules/817b3030097446a38b3b842bf528e112`, + method: 'GET', + endsWith: '817b3030097446a38b3b842bf528e112', + response: { + status: 200, + headers: { + 'Content-Type': 'application/x-json-stream', + }, + body: JSON.stringify({ + error: null, + locks_stuck_cnt: 0, + ignore_availability: false, + meta: null, + subscription_id: null, + rse_expression: 'XRD3', + source_replica_expression: null, + ignore_account_limit: false, + created_at: 'Mon, 27 Nov 2023 17:57:44 UTC', + account: 'root', + copies: 1, + activity: 'User Subscriptions', + priority: 3, + updated_at: 'Mon, 27 Nov 2023 17:57:44 UTC', + scope: 'test', + expires_at: null, + grouping: 'DATASET', + name: 'container', + weight: null, + notification: 'NO', + comments: null, + did_type: 'CONTAINER', + locked: false, + stuck_at: null, + child_rule_id: null, + state: 'REPLICATING', + locks_ok_cnt: 0, + purge_replicas: false, + eol_at: null, + id: '817b3030097446a38b3b842bf528e112', + locks_replicating_cnt: 4, + split_container: false, + }), + }, + }; + MockRucioServerFactory.createMockRucioServer(true, [getRuleEndpoint]); + }); + afterEach(() => { + fetchMock.dontMock(); + }); + + test('it should get details for a rule', async () => { + const { req, res, session } = await createHttpMocks('/api/feature/get-rule?=817b3030097446a38b3b842bf528e112', 'GET', {}); + const getRuleController = appContainer.get>(CONTROLLERS.GET_RULE); + const getRuleControllerParams: GetRuleControllerParameters = { + rucioAuthToken: MockRucioServerFactory.VALID_RUCIO_TOKEN, + response: res as unknown as NextApiResponse, + id: '817b3030097446a38b3b842bf528e112', + }; + await getRuleController.execute(getRuleControllerParams); + const data = await res._getJSONData(); + expect(data).toEqual(expectedViewModel); + }); +}); diff --git a/test/api/rule/list-rule-replica-lock-states.test.ts b/test/api/rule/list-rule-replica-lock-states.test.ts new file mode 100644 index 000000000..d76d7f4dc --- /dev/null +++ b/test/api/rule/list-rule-replica-lock-states.test.ts @@ -0,0 +1,83 @@ +import appContainer from '@/lib/infrastructure/ioc/container-config'; +import { ListRulesRequest } from '@/lib/core/usecase-models/list-rules-usecase-models'; +import { ListRulesControllerParameters } from '@/lib/infrastructure/controller/list-rules-controller'; +import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'; + +import { NextApiResponse } from 'next'; +import { MockHttpStreamableResponseFactory } from 'test/fixtures/http-fixtures'; +import { Readable } from 'stream'; +import MockRucioServerFactory, { MockEndpoint } from '../../fixtures/rucio-server'; +import { BaseController } from '@/lib/sdk/controller'; +import ListRuleReplicaLockStates from '@/pages/api/feature/list-rule-replica-lock-states'; +import { ListRuleReplicaLockStatesRequest } from '@/lib/core/usecase-models/list-rule-replica-lock-states-usecase-models'; +import { ListRuleReplicaLockStatesControllerParameters } from '@/lib/infrastructure/controller/list-rule-replica-lock-states-controller'; + +describe('Feature: ListRules', () => { + beforeEach(() => { + fetchMock.doMock(); + const mockLock = { + scope: 'test', + name: 'file', + rse_id: 'da739c56d6df4a199badc7b1cf53c13c', + rse: 'MOCK', + state: 'REPLICATING', + rule_id: 'c0301e6bce06448e807503e608255130', + }; + + const listLocksMockEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/rules/c0301e6bce06448e807503e608255130/locks`, + method: 'GET', + response: { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: Readable.from([JSON.stringify(mockLock), JSON.stringify(mockLock)].join('\n')), + }, + }; + MockRucioServerFactory.createMockRucioServer(true, [listLocksMockEndpoint]); + }); + + afterEach(() => { + fetchMock.dontMock(); + }); + + it('should return a view model for a valid request to ListRules feature', async () => { + const res = MockHttpStreamableResponseFactory.getMockResponse(); + + const listLocksController = appContainer.get>( + CONTROLLERS.LIST_RULE_REPLICA_LOCK_STATES, + ); + const listRulesControllerParams: ListRuleReplicaLockStatesControllerParameters = { + rucioAuthToken: MockRucioServerFactory.VALID_RUCIO_TOKEN, + id: 'c0301e6bce06448e807503e608255130', + response: res as unknown as NextApiResponse, + }; + + await listLocksController.execute(listRulesControllerParams); + + const receivedData: any[] = []; + const onData = (data: any) => { + receivedData.push(JSON.parse(data)); + }; + + // TODO: decompose + const done = new Promise((resolve, reject) => { + res.on('data', onData); + res.on('end', () => { + res.off('data', onData); + resolve(); + }); + res.on('error', err => { + res.off('data', onData); + reject(err); + }); + }); + + await done; + expect(receivedData.length).toEqual(2); + expect(receivedData[0].status).toEqual('success'); + expect(receivedData[0].name).toEqual('file'); + expect(receivedData[1].rse).toEqual('MOCK'); + }); +}); diff --git a/test/fixtures/table-fixtures.ts b/test/fixtures/table-fixtures.ts index be76af2b8..71ff4f51b 100644 --- a/test/fixtures/table-fixtures.ts +++ b/test/fixtures/table-fixtures.ts @@ -9,6 +9,7 @@ import { RSEBlockState, RSEProtocol, RSEType, + RuleGrouping, RuleNotification, RuleState, SubscriptionState, @@ -168,7 +169,7 @@ export function fixtureRuleMetaViewModel(): RuleMetaViewModel { created_at: faker.date.past().toISOString(), did_type: faker.helpers.arrayElement([DIDType.CONTAINER, DIDType.DATASET, DIDType.FILE]), expires_at: faker.date.future().toISOString(), - grouping: faker.helpers.arrayElement([DIDType.CONTAINER, DIDType.DATASET, DIDType.FILE]), + grouping: faker.helpers.arrayElement([RuleGrouping.ALL, RuleGrouping.DATASET, RuleGrouping.NONE]), id: faker.string.uuid(), ignore_account_limit: faker.datatype.boolean(), ignore_availability: faker.datatype.boolean(), diff --git a/test/gateway/rule/rule-gateway-get-rule.test.ts b/test/gateway/rule/rule-gateway-get-rule.test.ts index 92b3b0631..68cc914df 100644 --- a/test/gateway/rule/rule-gateway-get-rule.test.ts +++ b/test/gateway/rule/rule-gateway-get-rule.test.ts @@ -1,14 +1,39 @@ -import { RuleDTO, RuleReplicaLockStateDTO } from '@/lib/core/dto/rule-dto'; -import { LockState, RuleState } from '@/lib/core/entity/rucio'; +import { RuleMetaDTO } from '@/lib/core/dto/rule-dto'; +import { DIDType, RuleGrouping, RuleNotification, RuleState } from '@/lib/core/entity/rucio'; import RuleGatewayOutputPort from '@/lib/core/port/secondary/rule-gateway-output-port'; import appContainer from '@/lib/infrastructure/ioc/container-config'; import GATEWAYS from '@/lib/infrastructure/ioc/ioc-symbols-gateway'; -import { BaseStreamableDTO } from '@/lib/sdk/dto'; import MockRucioServerFactory, { MockEndpoint } from 'test/fixtures/rucio-server'; -import { collectStreamedData } from 'test/fixtures/stream-test-utils'; -import { Readable } from 'stream'; +// TODO: extend the test describe('Rule Gateway', () => { + const expectedDTO: RuleMetaDTO = { + status: 'success', + locks_stuck_cnt: 0, + ignore_account_limit: false, + ignore_availability: false, + rse_expression: 'XRD3', + created_at: 'Mon, 27 Nov 2023 17:57:44 UTC', + account: 'root', + copies: 1, + activity: 'User Subscriptions', + priority: 3, + updated_at: 'Mon, 27 Nov 2023 17:57:44 UTC', + scope: 'test', + expires_at: null, + grouping: RuleGrouping.DATASET, + name: 'container', + notification: RuleNotification.No, + did_type: DIDType.CONTAINER, + locked: false, + state: RuleState.REPLICATING, + locks_ok_cnt: 0, + purge_replicas: false, + id: '817b3030097446a38b3b842bf528e112', + locks_replicating_cnt: 4, + split_container: false, + }; + beforeEach(() => { fetchMock.doMock(); const getRuleEndpoint: MockEndpoint = { @@ -56,76 +81,15 @@ describe('Rule Gateway', () => { }), }, }; - - const replicaLockStates = [ - JSON.stringify({ - scope: 'test', - name: 'file1', - rse_id: 'c8b8113ddcdb4ec78e0846171e594280', - rse: 'XRD3', - state: 'REPLICATING', - rule_id: '817b3030097446a38b3b842bf528e112', - }), - JSON.stringify({ - scope: 'test', - name: 'file2', - rse_id: 'c8b8113ddcdb4ec78e0846171e594280', - rse: 'XRD3', - state: 'REPLICATING', - rule_id: '817b3030097446a38b3b842bf528e112', - }), - JSON.stringify({ - scope: 'test', - name: 'file3', - rse_id: 'c8b8113ddcdb4ec78e0846171e594280', - rse: 'XRD3', - state: 'REPLICATING', - rule_id: '817b3030097446a38b3b842bf528e112', - }), - JSON.stringify({ - scope: 'test', - name: 'file4', - rse_id: 'c8b8113ddcdb4ec78e0846171e594280', - rse: 'XRD3', - state: 'REPLICATING', - rule_id: '817b3030097446a38b3b842bf528e112', - }), - ]; - - const listRuleReplicaLocksEndpoint: MockEndpoint = { - url: `${MockRucioServerFactory.RUCIO_HOST}/rules/817b3030097446a38b3b842bf528e112/locks`, - method: 'GET', - endsWith: '817b3030097446a38b3b842bf528e112/locks', - response: { - status: 200, - headers: { - 'Content-Type': 'application/x-json-stream', - }, - body: Readable.from(replicaLockStates.join('\n')), - }, - }; - MockRucioServerFactory.createMockRucioServer(true, [getRuleEndpoint, listRuleReplicaLocksEndpoint]); + MockRucioServerFactory.createMockRucioServer(true, [getRuleEndpoint]); }); afterEach(() => { fetchMock.dontMock(); }); - it('Should fetch details of a rule', async () => { + it('Should fetch details for a rule', async () => { const ruleGateway: RuleGatewayOutputPort = appContainer.get(GATEWAYS.RULE); - const listRuleLockStatesDTO: BaseStreamableDTO = await ruleGateway.listRuleReplicaLockStates( - MockRucioServerFactory.VALID_RUCIO_TOKEN, - '817b3030097446a38b3b842bf528e112', - ); - expect(listRuleLockStatesDTO.status).toEqual('success'); - - const ruleStream = listRuleLockStatesDTO.stream; - if (ruleStream == null || ruleStream == undefined) { - fail('Rule stream is null or undefined'); - } - - const recievedData = await collectStreamedData(ruleStream); - expect(recievedData.length).toEqual(4); - expect(recievedData[0].name).toEqual('file1'); - expect(recievedData[0].state).toEqual(LockState.REPLICATING); + const ruleMetaDTO: RuleMetaDTO = await ruleGateway.getRule(MockRucioServerFactory.VALID_RUCIO_TOKEN, '817b3030097446a38b3b842bf528e112'); + expect(ruleMetaDTO).toEqual(expectedDTO); }); });