diff --git a/ui/.gitignore b/ui/.gitignore index 6bc3041b..46ee295d 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -40,3 +40,6 @@ yarn-error.log* # typescript *.tsbuildinfo +*:Zone.Identifier +*.diff + diff --git a/ui/.prettierrc.json b/ui/.prettierrc.json index 2197f514..019849bd 100644 --- a/ui/.prettierrc.json +++ b/ui/.prettierrc.json @@ -6,7 +6,7 @@ "printWidth": 120, "useTabs": false, "bracketSpacing": true, - "jsxBracketSameLine": false, + "bracketSameLine": false, "arrowParens": "always", "endOfLine": "lf", "singleAttributePerLine": true diff --git a/ui/src/app/datasets/[bucket]/[name]/components/CollectionDetails.tsx b/ui/src/app/datasets/[bucket]/[name]/components/CollectionDetails.tsx index dde3fe12..4d4881f0 100644 --- a/ui/src/app/datasets/[bucket]/[name]/components/CollectionDetails.tsx +++ b/ui/src/app/datasets/[bucket]/[name]/components/CollectionDetails.tsx @@ -13,10 +13,8 @@ //limitations under the License. //SPDX-License-Identifier: Apache-2.0 -import { useEffect, useRef, useState } from "react"; import Link from "next/link"; -import { useWindowSize } from "usehooks-ts"; import { OutlinedIcon } from "~/components/Icon"; import { Colors, Tag } from "~/components/Tag"; @@ -31,105 +29,100 @@ interface DatasetDetailsProps { export const CollectionDetails = ({ dataset }: DatasetDetailsProps) => { const toolParamUpdater = useToolParamUpdater(); - const containerRef = useRef(null); - const [height, setHeight] = useState(0); - const windowSize = useWindowSize(); - - useEffect(() => { - if (containerRef?.current) { - setHeight(windowSize.height - containerRef.current.getBoundingClientRect().top - 12); - } - }, [windowSize.height]); return ( -
-
+
-
-
{dataset.name}
-
-
-
ID
-
{dataset.id}
-
Bucket
-
- - {dataset.bucket} - -
- {dataset.created_by && ( - <> -
Created By
-
{dataset.created_by}
- +

+ {dataset.name} +

+
+
+
ID
+
{dataset.id}
+
Bucket
+
+ + {dataset.bucket} + +
+ {dataset.created_by && ( + <> +
Created By
+
{dataset.created_by}
+ + )} +
Created Date
+
{convertToReadableTimezone(dataset.created_date)}
+
Labels
+
+ {Object.entries(dataset.labels).length > 0 ? ( +
+ {Object.entries(dataset.labels).map(([key, value], index) => ( + + {key}: {String(value)} + + ))} +
+ ) : ( +

None

)} -
Created Date
-
{convertToReadableTimezone(dataset.created_date)}
-
Labels
-
- {Object.entries(dataset.labels).length > 0 ? ( -
- {Object.entries(dataset.labels).map(([key, value], index) => ( - - {key}: {String(value)} - - ))} -
- ) : ( -

None

- )} -
-
-
-
+
+
+
+ + + - - -
+ + Rename Collection +
-
+
); }; diff --git a/ui/src/app/datasets/[bucket]/[name]/components/CollectionOverview.tsx b/ui/src/app/datasets/[bucket]/[name]/components/CollectionOverview.tsx index 3c9832a3..c2ac5231 100644 --- a/ui/src/app/datasets/[bucket]/[name]/components/CollectionOverview.tsx +++ b/ui/src/app/datasets/[bucket]/[name]/components/CollectionOverview.tsx @@ -15,11 +15,12 @@ //SPDX-License-Identifier: Apache-2.0 "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; -import { DatasetTag } from "~/components/Tag"; +import PageHeader from "~/components/PageHeader"; +import { Colors, Tag } from "~/components/Tag"; import { type DataInfoResponse, type DatasetTypesSchema } from "~/models"; import { CollectionDetails } from "./CollectionDetails"; @@ -37,7 +38,6 @@ export default function CollectionOverview({ const searchParams = useSearchParams(); const toolParamUpdater = useToolParamUpdater(); const [tool, setTool] = useState(undefined); - const headerRef = useRef(null); useEffect(() => { setTool(searchParams.get(PARAM_KEYS.tool) as ToolType | undefined); @@ -45,17 +45,13 @@ export default function CollectionOverview({ return ( <> -
- {dataset.type} -

+ +

{dataset.bucket}/{dataset.name} -

-
-
-
+

+ Collection + +
diff --git a/ui/src/app/datasets/[bucket]/[name]/components/CollectionVersionsTable.tsx b/ui/src/app/datasets/[bucket]/[name]/components/CollectionVersionsTable.tsx index e659bd48..6a90f9ba 100644 --- a/ui/src/app/datasets/[bucket]/[name]/components/CollectionVersionsTable.tsx +++ b/ui/src/app/datasets/[bucket]/[name]/components/CollectionVersionsTable.tsx @@ -105,6 +105,7 @@ export const CollectionVersionsTable: React.FC<{ getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), + getRowId: (row) => row.version, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore onColumnFiltersChange: setColumnFilters, @@ -123,7 +124,6 @@ export const CollectionVersionsTable: React.FC<{ { const toolParamUpdater = useToolParamUpdater(); return ( -
-
{dataset.name}
-
-
-
ID
-
{dataset.id}
-
Bucket
-
- - {dataset.bucket} - -
- {dataset.created_by && ( - <> -
Created By
-
{dataset.created_by}
- - )} -
Created Date
-
{convertToReadableTimezone(dataset.created_date)}
- {dataset.hash_location_size && ( - <> -
Storage Size
-
{convertBytes(dataset.hash_location_size)}
- +
+

+ {dataset.name} +

+
+
ID
+
{dataset.id}
+
Bucket
+
+ + {dataset.bucket} + +
+ {dataset.created_by && ( + <> +
Created By
+
{dataset.created_by}
+ + )} +
Created Date
+
{convertToReadableTimezone(dataset.created_date)}
+ {dataset.hash_location_size && ( + <> +
Storage Size
+
{convertBytes(dataset.hash_location_size)}
+ + )} +
Labels
+
+ {Object.entries(dataset.labels).length > 0 ? ( +
+ {Object.entries(dataset.labels).map(([key, value], index) => ( + + {key}: {String(value)} + + ))} +
+ ) : ( +

None

)} -
Labels
-
- {Object.entries(dataset.labels).length > 0 ? ( -
- {Object.entries(dataset.labels).map(([key, value], index) => ( - - {key}: {String(value)} - - ))} -
- ) : ( -

None

- )} -
-
-
+ +
{ Rename Dataset
-
+ ); }; diff --git a/ui/src/app/datasets/[bucket]/[name]/components/DatasetOverview.tsx b/ui/src/app/datasets/[bucket]/[name]/components/DatasetOverview.tsx index 0391d51d..db079d45 100644 --- a/ui/src/app/datasets/[bucket]/[name]/components/DatasetOverview.tsx +++ b/ui/src/app/datasets/[bucket]/[name]/components/DatasetOverview.tsx @@ -15,17 +15,18 @@ //SPDX-License-Identifier: Apache-2.0 "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { useWindowSize } from "usehooks-ts"; import FileBrowser, { type FilePreviewModalProps } from "~/app/datasets/components/FileBrowser"; import FullPageModal from "~/components/FullPageModal"; -import { FilledIcon, OutlinedIcon } from "~/components/Icon"; +import { OutlinedIcon } from "~/components/Icon"; +import { IconButton } from "~/components/IconButton"; import { PageError } from "~/components/PageError"; -import { DatasetTag } from "~/components/Tag"; +import PageHeader from "~/components/PageHeader"; +import { Colors, Tag } from "~/components/Tag"; import { type DataInfoDatasetEntry, type DataInfoResponse, type DatasetTypesSchema } from "~/models"; import { api } from "~/trpc/react"; @@ -51,10 +52,6 @@ export default function DatasetOverview({ const [tool, setTool] = useState(undefined); const [openFileData, setOpenFileData] = useState(undefined); const toolParamUpdater = useToolParamUpdater(); - const headerRef = useRef(null); - const [height, setHeight] = useState(0); - const windowSize = useWindowSize(); - const containerRef = useRef(null); const { data: selectedVersionData, @@ -74,12 +71,6 @@ export default function DatasetOverview({ }, ); - useEffect(() => { - if (containerRef?.current) { - setHeight(windowSize.height - containerRef.current.getBoundingClientRect().top); - } - }, [windowSize.height, openFileData, selectedVersionData]); - useEffect(() => { if (selectedVersion) { const selectedVersionIndex = dataset.versions.findIndex((version) => version.version === selectedVersion); @@ -101,17 +92,8 @@ export default function DatasetOverview({ return ( <> -
- - {dataset.type} - -
+ +
{previousVersion ? ( )} -

- {dataset.bucket}/{dataset.name}: {selectedVersion ?? "latest"} -

+

+ {dataset.bucket}/{dataset.name} +

{nextVersion ? ( )}
- -
+ icon="layers" + text="Versions" + /> + {errorSelectedVersionData && ( -
+
) : ( -
-
- - -
+
+ +
)}
@@ -215,7 +187,8 @@ export default function DatasetOverview({ toolParamUpdater({ showVersions: false })} - headerChildren={

Versions

} + headerChildren={

Versions

} + aria-labelledby="versions-header" > -
Version {datasetVersion.version}
-
-
-
Status
-
{datasetVersion.status}
-
Created By
-
{formatForWrapping(datasetVersion.created_by)}
-
Created Date
-
{convertToReadableTimezone(datasetVersion.created_date)}
-
Last Used
-
{convertToReadableTimezone(datasetVersion.last_used)}
-
Size
-
{convertBytes(datasetVersion.size)}
-
Retention Policy
-
{Math.floor(datasetVersion.retention_policy / (24 * 60 * 60))} days
-
Tags
-
-
- {datasetVersion.tags.map((tag, index) => ( - +

+ Version {datasetVersion.version} +

+
+
Status
+
{datasetVersion.status}
+
Created By
+
{formatForWrapping(datasetVersion.created_by)}
+
Created Date
+
{convertToReadableTimezone(datasetVersion.created_date)}
+
Last Used
+
{convertToReadableTimezone(datasetVersion.last_used)}
+
Size
+
{convertBytes(datasetVersion.size)}
+
Retention Policy
+
{Math.floor(datasetVersion.retention_policy / (24 * 60 * 60))} days
+
Tags
+
+
+ {datasetVersion.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+
Related Collections
+
+
+ {datasetVersion.collections.length > 0 ? ( + datasetVersion.collections.map((collection) => ( + - {tag} - - ))} -
-
-
Related Collections
-
-
- {datasetVersion.collections.length > 0 ? ( - datasetVersion.collections.map((collection) => ( - - {collection} - - )) - ) : ( -

None

- )} -
-
-
-
+ {collection} + + )) + ) : ( +

None

+ )} +
+ +
-
+ ); }; diff --git a/ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionsTable.tsx b/ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionsTable.tsx index c01b831d..e77a92de 100644 --- a/ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionsTable.tsx +++ b/ui/src/app/datasets/[bucket]/[name]/components/DatasetVersionsTable.tsx @@ -164,6 +164,7 @@ export const DatasetVersionsTable = ({ dataset, selectedVersion, visible }: Data getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), + getRowId: (row) => row.version, enableMultiSort: true, enableSortingRemoval: false, state: { sorting }, @@ -176,18 +177,15 @@ export const DatasetVersionsTable = ({ dataset, selectedVersion, visible }: Data }); return ( -
- + - - -
+ totalRows={dataset.versions.length} + /> + ); }; diff --git a/ui/src/app/datasets/[bucket]/[name]/components/DeleteCollection.tsx b/ui/src/app/datasets/[bucket]/[name]/components/DeleteCollection.tsx index cfd15da4..61293122 100644 --- a/ui/src/app/datasets/[bucket]/[name]/components/DeleteCollection.tsx +++ b/ui/src/app/datasets/[bucket]/[name]/components/DeleteCollection.tsx @@ -65,9 +65,10 @@ export const DeleteCollection = ({ dataset }: { dataset: DataInfoResponse -

Are you sure you want to delete this collection?

- {error && {error}} - {showSuccess && Collection deleted successfully} +

Are you sure you want to delete this collection?

+ + {(error ?? showSuccess) ? "Collection deleted successfully" : ""} +
{!showSuccess && ( -
+ + + setShowActions(true)} + aria-controls="pool-actions" + aria-expanded={showActions} + aria-haspopup="true" + /> + +
setShowActions(false)} - containerRef={headerRef} - top={headerRef.current?.offsetHeight ?? 0} - dimBackground={false} className="border-t-0" - bodyClassName="p-3" + bodyClassName="p-global" > -
-
-
+
{isLoadingPools && !localAllPools && }
-
+
-
+ + + + +
{ setShowFilters(false); }} aria-label="Pools Filter" - dimBackground={false} - className="z-40 border-t-0 w-100" + className="border-t-0 w-80" > -
-
-
-
-

Gauges

-
-
- -
+
+
) => { @@ -337,8 +337,8 @@ const CredentialForm = ({ }; return ( -
-
+
+
{ const auth = useAuth(); return ( -
+
Name
{auth.claims?.name}
Email
@@ -36,6 +36,7 @@ const ProfileSettings = ({ profile }: ProfileSettingsProps) => {
Email Notifications
{
Slack Notifications
{children}; + return children; } diff --git a/ui/src/app/profile/page.tsx b/ui/src/app/profile/page.tsx index d330f7c2..6bfcc164 100644 --- a/ui/src/app/profile/page.tsx +++ b/ui/src/app/profile/page.tsx @@ -22,6 +22,7 @@ import { useSearchParams } from "next/navigation"; import FullPageModal from "~/components/FullPageModal"; import { OutlinedIcon } from "~/components/Icon"; import { PageError } from "~/components/PageError"; +import PageHeader from "~/components/PageHeader"; import { Spinner } from "~/components/Spinner"; import { TextInput } from "~/components/TextInput"; import { type CredentialListItem, type ProfileResponse } from "~/models"; @@ -107,10 +108,14 @@ export default function ProfileSettingsPage() { return ( <> -
-
-
-

Profile

+ +
+
+
+

Profile

-
-
+ +
-
-
+
-

Credentials

-
+

Credentials

+
{ toolParamUpdater({ tool: ToolType.CreateCredential }); }} + title="Add New Credential" > - Add New Credential + Add New Credential
-
+
{toolHeading}} + headerChildren={

{toolHeading}

} + aria-labelledby="profile-settings-header" size="none" open={!!tool} onClose={() => { diff --git a/ui/src/app/resources/[name]/page.tsx b/ui/src/app/resources/[name]/page.tsx index 6aa20822..b50420d3 100644 --- a/ui/src/app/resources/[name]/page.tsx +++ b/ui/src/app/resources/[name]/page.tsx @@ -18,7 +18,8 @@ import { useEffect } from "react"; import Link from "next/link"; -import { GenericHeader } from "~/components/Header"; +import { FilledIcon } from "~/components/Icon"; +import PageHeader from "~/components/PageHeader"; import { getTaskHistoryUrl } from "~/components/TaskHistoryBanner"; import { env } from "~/env.mjs"; @@ -37,20 +38,22 @@ export default function ResourceOverviewPage({ params }: ResourcesSlugParams) { return ( <> - + +

{params.name}

- Task History + + + Task History + -
-
- -
+ + ); } diff --git a/ui/src/app/resources/components/AggregatePanels.tsx b/ui/src/app/resources/components/AggregatePanels.tsx index 73f422f2..20b352f5 100644 --- a/ui/src/app/resources/components/AggregatePanels.tsx +++ b/ui/src/app/resources/components/AggregatePanels.tsx @@ -78,8 +78,9 @@ export const AggregatePanels: React.FC {gauges}
diff --git a/ui/src/app/resources/components/PlatformDetails.tsx b/ui/src/app/resources/components/PlatformDetails.tsx index 750ecf60..e4714d92 100644 --- a/ui/src/app/resources/components/PlatformDetails.tsx +++ b/ui/src/app/resources/components/PlatformDetails.tsx @@ -39,16 +39,22 @@ export const PlatformDetails = ({ children, }: PlatformDetailsProps) => { return ( -
-
-

{name}

+
+

+ {name} Platform -

+ {showLoadError ? ( {children} -
+

Host Network Allowed: {hostNetwork ? "True" : "False"}

@@ -65,10 +71,10 @@ export const PlatformDetails = ({ Privileged Mode Allowed: {privileged ? "True" : "False"}

-
+

Default Mounts @@ -76,19 +82,17 @@ export const PlatformDetails = ({ {defaultMounts?.length ? (
    - {defaultMounts?.map((mount) => ( -
  • {mount}
  • - ))} + {defaultMounts?.map((mount) =>
  • {mount}
  • )}
) : ( -

None

+

None

)}

Allowed Mounts @@ -96,19 +100,17 @@ export const PlatformDetails = ({ {allowedMounts?.length ? (
    - {allowedMounts?.map((mount) => ( -
  • {mount}
  • - ))} + {allowedMounts?.map((mount) =>
  • {mount}
  • )}
) : ( -

None

+

None

)}

)} -
+ ); }; diff --git a/ui/src/app/resources/components/ResourceDetails.tsx b/ui/src/app/resources/components/ResourceDetails.tsx index 35889fe5..6fcb7539 100644 --- a/ui/src/app/resources/components/ResourceDetails.tsx +++ b/ui/src/app/resources/components/ResourceDetails.tsx @@ -203,20 +203,20 @@ export const ResourceDetails = ({ allowedMounts={platformConfig.allowed_mounts} > {fields && ( -
-
+
+
{Math.floor(convertResourceValueStr(fields.storage?.toString() ?? "0"))}

Storage [Gi]

-
+
{Math.floor(convertResourceValueStr(fields.memory?.toString() ?? "0"))}

Memory [Gi]

-
+
{parseFloat(fields.cpu?.toString() ?? "0")}

CPU [#]

-
+
{parseFloat(fields.gpu?.toString() ?? "0")}

GPU [#]

@@ -250,9 +250,9 @@ export const ResourceDetails = ({ return ( <> {children} -
+
{showPoolPlatformList} -
+
-
+
Resource Type -
+
-
+
-

Resources

-
-
-
- { - updateUrl({ isShowingUsed: true }); - localStorage.setItem(SHOW_USED_KEY, "true"); - }} - > - Used - - { - updateUrl({ isShowingUsed: false }); - localStorage.setItem(SHOW_USED_KEY, "false"); - }} - > - Free - -
- -
+ + updateUrl({ showGauges: !showGauges })} + icon="speed" + text="Gauges" + /> + + + +
{ setShowFilters(false); }} aria-label="Resources Filter" - dimBackground={false} className="z-40 border-t-0 w-100" > -
-
{showGauges && ( -
-
-

Gauges

- -
-
+ )} -
- -
+
{selectedResource.node} ) } + aria-label={selectedResource?.node ?? "Node Details"} open={!!selectedResource} onClose={() => { updateUrl({ selectedResource: null }); diff --git a/ui/src/app/tasks/components/StatusFilter.tsx b/ui/src/app/tasks/components/StatusFilter.tsx new file mode 100644 index 00000000..0b1e9c8f --- /dev/null +++ b/ui/src/app/tasks/components/StatusFilter.tsx @@ -0,0 +1,91 @@ +//SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at + +//http://www.apache.org/licenses/LICENSE-2.0 + +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +//SPDX-License-Identifier: Apache-2.0 +import { CheckboxWithLabel } from "~/components/Checkbox"; +import { StatusFilter as StatusFilterCommon, StatusFilterType } from "~/components/StatusFilter"; +import { TaskStatusValues, type TaskStatusType } from "~/models"; + +export const getMapFromStatusArray = (statusArray: string[]) => { + return new Map(TaskStatusValues.map((value) => [value, statusArray.includes(value.toString())])); +}; + +export const getTaskStatusArray = ( + statusFilterType?: StatusFilterType, + statusMap?: Map, +): string[] => { + if (statusFilterType === StatusFilterType.ALL) { + return [...TaskStatusValues]; + } + + if (statusFilterType === StatusFilterType.CURRENT) { + return ["SUBMITTING", "SCHEDULING", "WAITING", "PROCESSING", "INITIALIZING", "RUNNING"]; + } + + if (statusFilterType === StatusFilterType.COMPLETED) { + return ["COMPLETED"]; + } + + if (statusFilterType === StatusFilterType.FAILED) { + return TaskStatusValues.filter((status) => status.startsWith("FAILED")).concat(["RESCHEDULED"]); + } + + if (!statusFilterType || !statusMap) { + return []; + } + + return Array.from(statusMap.entries()) + .filter(([_, enabled]) => enabled) + .map(([status]) => status); +}; + +export const StatusFilter = ({ + statusMap, + setStatusMap, + className, + statusFilterType, + setStatusFilterType, +}: { + statusMap: Map; + setStatusMap: (map: Map) => void; + className?: string; + statusFilterType?: StatusFilterType; + setStatusFilterType: (statusFilterType: StatusFilterType) => void; +}) => { + return ( +
+ + {statusFilterType === StatusFilterType.CUSTOM && + TaskStatusValues.map((name) => { + const checked = Boolean(statusMap.get(name)); + return ( + { + const newMap = new Map(statusMap); + newMap.set(name, Boolean(event.target.checked)); + setStatusMap(newMap); + }} + /> + ); + })} +
+ ); +}; diff --git a/ui/src/app/tasks/components/TasksFilters.tsx b/ui/src/app/tasks/components/TasksFilters.tsx index efa6c7ce..dd97620f 100644 --- a/ui/src/app/tasks/components/TasksFilters.tsx +++ b/ui/src/app/tasks/components/TasksFilters.tsx @@ -21,20 +21,22 @@ import { OutlinedIcon } from "~/components/Icon"; import { InlineBanner } from "~/components/InlineBanner"; import { MultiselectWithAll } from "~/components/MultiselectWithAll"; import { Spinner } from "~/components/Spinner"; -import { StatusFilter } from "~/components/StatusFilter"; +import { StatusFilterType } from "~/components/StatusFilter"; import { TextInput } from "~/components/TextInput"; import { UserFilter, UserFilterType } from "~/components/UserFilter"; -import { PoolsListResponseSchema, type PriorityType, type ResourcesEntry } from "~/models"; +import { PoolsListResponseSchema, type PriorityType, type ResourcesEntry, type TaskStatusType } from "~/models"; import { api } from "~/trpc/react"; +import { getMapFromStatusArray, getTaskStatusArray, StatusFilter } from "./StatusFilter"; + export interface TasksFiltersDataProps { userType: UserFilterType; selectedUsers: string; dateRange: number; startedAfter?: string; startedBefore?: string; - allStatuses: boolean; - statuses: string; + statusFilterType?: StatusFilterType; + statuses?: string; selectedPools: string; isSelectAllPoolsChecked: boolean; priority?: PriorityType; @@ -44,12 +46,10 @@ export interface TasksFiltersDataProps { } interface TasksFiltersProps extends TasksFiltersDataProps { - statusValues: string[]; currentUserName: string; onRefresh: () => void; validateFilters: (props: TasksFiltersDataProps) => string[]; updateUrl: (params: ToolParamUpdaterProps) => void; - defaults: Record; } export const initNodes = (nodes: string, poolNodes: string[]) => { @@ -82,13 +82,12 @@ export const resourcesToNodes = (resources: ResourcesEntry[]): PoolNodes[] => { }; export const TasksFilters = ({ - statusValues, userType, selectedUsers, dateRange, startedAfter, startedBefore, - allStatuses, + statusFilterType, statuses, selectedPools, isSelectAllPoolsChecked, @@ -100,12 +99,11 @@ export const TasksFilters = ({ updateUrl, nodes, isSelectAllNodesChecked, - defaults, }: TasksFiltersProps) => { const [localDateRange, setLocalDateRange] = useState(dateRange); const [localStartedAfter, setLocalStartedAfter] = useState(undefined); const [localStartedBefore, setLocalStartedBefore] = useState(undefined); - const [localStatusMap, setLocalStatusMap] = useState>(new Map()); + const [localStatusMap, setLocalStatusMap] = useState>(new Map()); const [localPools, setLocalPools] = useState>(new Map()); const [localUsers, setLocalUsers] = useState(selectedUsers); const [localUserType, setLocalUserType] = useState(userType); @@ -115,7 +113,7 @@ export const TasksFilters = ({ const [priorityFilter, setPriorityFilter] = useState(priority); const [localNodes, setLocalNodes] = useState>(new Map()); const [localAllNodes, setLocalAllNodes] = useState(isSelectAllNodesChecked); - const [localAllStatuses, setLocalAllStatuses] = useState(allStatuses); + const [localStatusFilterType, setLocalStatusFilterType] = useState(statusFilterType); const { data: availablePools, isLoading: isLoadingPools } = api.resources.getPools.useQuery(undefined, { refetchOnWindowFocus: false, @@ -143,6 +141,17 @@ export const TasksFilters = ({ setLocalUserType(userType); }, [userType]); + useEffect(() => { + setLocalStatusFilterType(statusFilterType); + + if (statusFilterType === StatusFilterType.CUSTOM) { + const statusArray = statuses?.split(",") ?? []; + setLocalStatusMap(getMapFromStatusArray(statusArray)); + } else { + setLocalStatusMap(getMapFromStatusArray(getTaskStatusArray(statusFilterType))); + } + }, [statuses, statusFilterType]); + const poolNodes = useMemo(() => { if (localAllPools) { return availableNodes?.flatMap(({ hostname }) => hostname); @@ -161,11 +170,7 @@ export const TasksFilters = ({ setLocalStartedAfter(startedAfter); setLocalStartedBefore(startedBefore); setLocalWorkflowId(workflowId); - - setLocalStatusMap( - new Map(statusValues.map((value: string) => [value, !statuses || statuses.split(",").includes(value)])), - ); - }, [dateRange, startedAfter, startedBefore, statuses, workflowId, statusValues]); + }, [dateRange, startedAfter, startedBefore, workflowId]); useEffect(() => { const parsedData = PoolsListResponseSchema.safeParse(availablePools); @@ -192,11 +197,7 @@ export const TasksFilters = ({ const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); - const statuses = localAllStatuses - ? [] - : Array.from(localStatusMap.entries()) - .filter(([_, enabled]) => enabled) - .map(([status]) => status); + const statuses = getTaskStatusArray(localStatusFilterType, localStatusMap); const pools = Array.from(localPools.entries()) .filter(([_, enabled]) => enabled) @@ -219,8 +220,8 @@ export const TasksFilters = ({ workflowId: localWorkflowId, nodes: nodes.join(","), isSelectAllNodesChecked: localAllNodes, - allStatuses: localAllStatuses, - statuses: statuses.join(","), + statusFilterType: localStatusFilterType, + statuses: localStatusFilterType === StatusFilterType.CUSTOM ? statuses.join(",") : undefined, }); setErrors(formErrors); @@ -233,8 +234,8 @@ export const TasksFilters = ({ dateRange: localDateRange, dateAfter: localDateRange === customDateRange ? localStartedAfter : null, dateBefore: localDateRange === customDateRange ? localStartedBefore : null, - allStatuses: localAllStatuses, - status: statuses.length > 0 ? statuses.join(",") : undefined, + statusFilterType: localStatusFilterType, + status: localStatusFilterType === StatusFilterType.CUSTOM ? statuses.join(",") : null, allPools: localAllPools, pools: localAllPools ? null : pools, allUsers: localUserType === UserFilterType.ALL, @@ -249,15 +250,7 @@ export const TasksFilters = ({ }; const handleReset = () => { - setLocalAllStatuses(defaults.allStatuses ? defaults.allStatuses === "true" : true); - - if (defaults.status) { - const statuses = defaults.status.split(","); - setLocalStatusMap(new Map(statusValues.map((value) => [value, statuses.includes(value)]))); - } else { - setLocalStatusMap(new Map(statusValues.map((value) => [value, true]))); - } - + setLocalStatusFilterType(StatusFilterType.CURRENT); setLocalUserType(UserFilterType.CURRENT); setLocalUsers(currentUserName); setLocalAllPools(true); @@ -269,8 +262,8 @@ export const TasksFilters = ({ setLocalAllNodes(true); updateUrl({ - status: undefined, - allStatuses: false, + status: null, + statusFilterType: StatusFilterType.CURRENT, allPools: true, allUsers: false, users: [currentUserName], @@ -288,7 +281,7 @@ export const TasksFilters = ({ return ( -
+
Priority -
+
-
+ 0 ? "error" : "none"}> +
+ {errors.map((error, index) => ( +
{error}
+ ))} +
+
+
- -
+ + { + setShowTotalResources(!showTotalResources); + }} + aria-expanded={showTotalResources} + aria-haspopup="true" + aria-controls="total-resources" + /> + + +
setShowFilters(false)} className="w-100 border-t-0" - containerRef={headerRef} - top={headerRef.current?.offsetHeight ?? 0} - dimBackground={false} + aria-label="Tasks Filter" > {/* By only adding it if showFilters is true, it will reset to url params if closed and reopened */} {showFilters && ( )} @@ -370,14 +370,14 @@ export default function Tasks() { id="total-resources" open={showTotalResources} onClose={() => setShowTotalResources(false)} - containerRef={headerRef} - top={headerRef.current?.offsetHeight ?? 0} - header={

Total Resources

} - dimBackground={false} - className="mr-26 border-t-0" + header="Total Resources" + className="lg:mr-20 border-t-0" > -
-
+
+
Storage
{Intl.NumberFormat("en-US", { style: "decimal" }).format(totalResources.Storage)} @@ -397,11 +397,6 @@ export default function Tasks() {
-
-
Workflow Details @@ -432,7 +426,14 @@ export default function Tasks() { id="tasks-details" open={!!selectedTaskName} paused={!!selectedTaskName && !selectedTask} + returnFocusOnDeactivate={false} onClose={() => { + setSafeTimeout(() => { + // focus-trap-react does not work well with useReactTable as the ref for button inside the table that triggered the slideout is not persistent + const el = focusReturnIdRef.current ? document.getElementById(focusReturnIdRef.current) : undefined; + el?.focus(); + }, 500); + if (showWF) { updateUrl({ showWF: false }); } else { @@ -448,14 +449,11 @@ export default function Tasks() { className="workflow-details-slideout border-t-0" headerClassName="brand-header" bodyClassName="dag-details-body" - containerRef={containerRef} - heightOffset={10} > {selectedWorkflowError ? ( ) : showWF && selectedWorkflow ? ( @@ -468,7 +466,6 @@ export default function Tasks() { ) : selectedTask ? ( diff --git a/ui/src/app/taskssummary/components/TasksTable.tsx b/ui/src/app/taskssummary/components/TasksTable.tsx index b2c6365e..d2a4d21b 100644 --- a/ui/src/app/taskssummary/components/TasksTable.tsx +++ b/ui/src/app/taskssummary/components/TasksTable.tsx @@ -30,7 +30,6 @@ import { WorkflowTableRowAction } from "~/app/workflows/components/WorkflowTable import { type ToolParamUpdaterProps } from "~/app/workflows/hooks/useToolParamUpdater"; import { commonFilterFns } from "~/components/commonFilterFns"; import { TableBase } from "~/components/TableBase"; -import { TableLoader } from "~/components/TableLoader"; import { TablePagination } from "~/components/TablePagination"; import { Colors, Tag } from "~/components/Tag"; import { useTableSortLoader } from "~/hooks/useTableSortLoader"; @@ -153,10 +152,10 @@ export const TasksTable = ({ cell: ({ row }) => row.original.workflow_id ? ( ) : undefined, sortingFn: "alphanumericCaseSensitive", @@ -176,6 +175,9 @@ export const TasksTable = ({ getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), + getRowId: (row) => { + return showWF ? `${row.user}-${row.workflow_id}` : row.user; + }, state: { sorting, columnVisibility, @@ -193,29 +195,17 @@ export const TasksTable = ({ autoResetPageIndex: false, }); - if (isLoading) { - return ; - } - return ( -
- {isLoading ? ( - - ) : ( -
- - - -
- )} -
+ + + ); }; diff --git a/ui/src/app/taskssummary/layout.tsx b/ui/src/app/taskssummary/layout.tsx index 6d6aec80..9925d668 100644 --- a/ui/src/app/taskssummary/layout.tsx +++ b/ui/src/app/taskssummary/layout.tsx @@ -15,7 +15,6 @@ //SPDX-License-Identifier: Apache-2.0 import { type PropsWithChildren } from "react"; -import { Layout } from "~/components/Layout"; import { env } from "~/env.mjs"; export const metadata = { @@ -23,5 +22,5 @@ export const metadata = { }; export default function TasksLayout({ children }: PropsWithChildren) { - return {children}; + return children; } diff --git a/ui/src/app/taskssummary/page.tsx b/ui/src/app/taskssummary/page.tsx index 6f50f666..53c3dedb 100644 --- a/ui/src/app/taskssummary/page.tsx +++ b/ui/src/app/taskssummary/page.tsx @@ -21,17 +21,21 @@ import Link from "next/link"; import { useAuth } from "~/components/AuthProvider"; import { allDateRange, customDateRange } from "~/components/DateRangePicker"; -import { FilledIcon, OutlinedIcon } from "~/components/Icon"; +import { FilterButton } from "~/components/FilterButton"; +import { FilledIcon } from "~/components/Icon"; +import { IconButton } from "~/components/IconButton"; import { PageError } from "~/components/PageError"; +import PageHeader from "~/components/PageHeader"; import { SlideOut } from "~/components/SlideOut"; +import { StatusFilterType } from "~/components/StatusFilter"; import { TASK_PINNED_KEY, UrlTypes } from "~/components/StoreProvider"; import { UserFilterType } from "~/components/UserFilter"; import useSafeTimeout from "~/hooks/useSafeTimeout"; -import { type TaskSummaryStatusType, TaskSummaryStatusValues } from "~/models"; import { type TaskSummaryListItem } from "~/models/tasks-model"; import { api } from "~/trpc/react"; import { TasksTable } from "./components/TasksTable"; +import { getTaskStatusArray } from "../tasks/components/StatusFilter"; import { TasksFilters, type TasksFiltersDataProps } from "../tasks/components/TasksFilters"; import { ToolsModal } from "../workflows/components/ToolsModal"; import WorkflowDetails from "../workflows/components/WorkflowDetails"; @@ -42,7 +46,7 @@ export default function TasksSummary() { const { username } = useAuth(); const defaultState = { status: "RUNNING", - allStatuses: "false", + statusFilterType: StatusFilterType.CURRENT, dateRange: allDateRange.toString(), }; const { @@ -65,13 +69,11 @@ export default function TasksSummary() { dateRangeDates, dateAfterFilter, dateBeforeFilter, - allStatuses, + statusFilterType, statusFilter, nodes, isSelectAllNodesChecked, } = useToolParamUpdater(UrlTypes.TasksSummary, username, defaultState); - const headerRef = useRef(null); - const containerRef = useRef(null); const lastFetchTimeRef = useRef(Date.now()); const [taskPinned, setTaskPinned] = useState(false); const [activeTool, setActiveTool] = useState(tool); @@ -98,9 +100,8 @@ export default function TasksSummary() { users: userType === UserFilterType.CUSTOM ? (userFilter?.split(",") ?? []) : [], all_pools: isSelectAllPoolsChecked, pools: isSelectAllPoolsChecked ? [] : poolFilter ? poolFilter.split(",") : [], - statuses: allStatuses - ? Object.values(TaskSummaryStatusValues) - : (statusFilter?.split(",") as TaskSummaryStatusType[]), + statuses: + statusFilterType === StatusFilterType.CUSTOM ? statusFilter?.split(",") : getTaskStatusArray(statusFilterType), priority: priority, started_after: dateRangeDates?.fromDate?.toISOString(), started_before: dateRangeDates?.toDate?.toISOString(), @@ -151,7 +152,7 @@ export default function TasksSummary() { dateRange, startedAfter, startedBefore, - allStatuses, + statusFilterType, statuses, nodes, isSelectAllNodesChecked, @@ -166,7 +167,7 @@ export default function TasksSummary() { if (dateRange === customDateRange && (startedAfter === undefined || startedBefore === undefined)) { errors.push("Please select a date range"); } - if (!allStatuses && statuses.length === 0) { + if (statusFilterType === StatusFilterType.CUSTOM && !statuses?.length) { errors.push("Please select at least one status"); } if (!isSelectAllNodesChecked && nodes.length === 0) { @@ -190,7 +191,7 @@ export default function TasksSummary() { workflowId: nameFilter ?? "", nodes: nodes ?? "", isSelectAllNodesChecked: isSelectAllNodesChecked ?? true, - allStatuses: allStatuses ?? true, + statusFilterType, statuses: statusFilter ?? "", }).length > 0 ) { @@ -210,7 +211,7 @@ export default function TasksSummary() { nodes, isSelectAllNodesChecked, updateUrl, - allStatuses, + statusFilterType, statusFilter, ]); const { setSafeTimeout } = useSafeTimeout(); @@ -266,60 +267,46 @@ export default function TasksSummary() { return ( <> -
-

- {`${allStatuses ? "Current" : statusFilter}`} Tasks - {userType === UserFilterType.ALL - ? " for All Users" - : userFilter && userFilter.split(",").length === 1 - ? ` for ${userFilter}` - : ""} -

-
- - - -
+ +

+ {`${statusFilterType?.toString() ?? "Current"} Tasks + ${ + userType === UserFilterType.ALL + ? " for All Users" + : userFilter && userFilter.split(",").length === 1 + ? ` for ${userFilter}` + : "" + }`} +

+ { + setShowTotalResources(!showTotalResources); + }} + icon="memory" + text="Total Resources" + aria-expanded={showTotalResources} + aria-haspopup="true" + aria-controls="total-resources" + /> + +
+
setShowFilters(false)} className="w-100 border-t-0" - containerRef={headerRef} - top={headerRef.current?.getBoundingClientRect().top ?? 0} - dimBackground={false} + aria-label="Tasks Filter" > {/* By only adding it if showFilters is true, it will reset to url params if closed and reopened */} {showFilters && ( setShowTotalResources(false)} - containerRef={headerRef} - top={headerRef.current?.getBoundingClientRect().top ?? 0} - header={

Total Resources

} - dimBackground={false} - className="mr-30 border-t-0" + header="Total Resources" + className="mr-20 border-t-0" > -
-
+
+
Storage
{Intl.NumberFormat("en-US", { style: "decimal" }).format(totalResources.Storage)} @@ -370,11 +356,6 @@ export default function TasksSummary() {
-
-
{error ? (
Workflow Details @@ -421,14 +402,11 @@ export default function TasksSummary() { className="workflow-details-slideout border-t-0" headerClassName="brand-header" bodyClassName="dag-details-body" - containerRef={containerRef} - heightOffset={10} > {selectedWorkflowError ? ( ) : selectedWorkflow ? ( diff --git a/ui/src/app/tutorials/layout.tsx b/ui/src/app/tutorials/layout.tsx new file mode 100644 index 00000000..51039819 --- /dev/null +++ b/ui/src/app/tutorials/layout.tsx @@ -0,0 +1,26 @@ +//SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at + +//http://www.apache.org/licenses/LICENSE-2.0 + +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +//SPDX-License-Identifier: Apache-2.0 +import { type PropsWithChildren } from "react"; + +import { env } from "~/env.mjs"; + +export const metadata = { + title: `${env.NEXT_PUBLIC_APP_NAME} Tutorials`, +}; + +export default function TutorialsLayout({ children }: PropsWithChildren) { + return children; +} diff --git a/ui/src/app/page.tsx b/ui/src/app/tutorials/page.tsx similarity index 84% rename from ui/src/app/page.tsx rename to ui/src/app/tutorials/page.tsx index d4d28834..da3914a2 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/tutorials/page.tsx @@ -16,15 +16,12 @@ import { Container } from "~/components/Container"; import { HomepageCards } from "~/components/HomepageCards"; import { HomepageHero } from "~/components/HomepageHero"; -import { Layout } from "~/components/Layout"; export default function Dashboard() { return ( - - - - - - + + + + ); } diff --git a/ui/src/app/workflows/[name]/page.tsx b/ui/src/app/workflows/[name]/page.tsx index b61616d8..19bd30db 100644 --- a/ui/src/app/workflows/[name]/page.tsx +++ b/ui/src/app/workflows/[name]/page.tsx @@ -14,17 +14,21 @@ //SPDX-License-Identifier: Apache-2.0 "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { ReactFlowProvider } from "reactflow"; import { useAuth } from "~/components/AuthProvider"; +import { FilterButton } from "~/components/FilterButton"; import { FilledIcon, OutlinedIcon } from "~/components/Icon"; +import { IconButton } from "~/components/IconButton"; import { PageError } from "~/components/PageError"; +import PageHeader from "~/components/PageHeader"; import { SlideOut } from "~/components/SlideOut"; import { Spinner } from "~/components/Spinner"; import StatusBadge from "~/components/StatusBadge"; +import { StatusFilterType } from "~/components/StatusFilter"; import { TASK_PINNED_KEY, UrlTypes } from "~/components/StoreProvider"; import { ViewToggleButton } from "~/components/ViewToggleButton"; import { env } from "~/env.mjs"; @@ -43,8 +47,6 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { const { username } = useAuth(); const [taskPinned, setTaskPinned] = useState(false); const [workflowNameParts, setWorkflowNameParts] = useState<{ id: number; name: string } | undefined>(undefined); - const containerRef = useRef(null); - const headerRef = useRef(null); const { updateUrl, @@ -53,7 +55,7 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { lines, view, statusFilter, - allStatuses, + statusFilterType, nameFilter, nodes, isSelectAllNodesChecked, @@ -62,9 +64,10 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { showWF, selectedTaskName, retryId, - } = useToolParamUpdater(UrlTypes.Workflows, username, { showWF: "true", allStatuses: "true", status: "" }); + } = useToolParamUpdater(UrlTypes.Workflows, username, { showWF: "true", statusFilterType: StatusFilterType.ALL }); const selectedWorkflow = useWorkflow(params.name, true, 2); const [selectedTask, setSelectedTask] = useState(undefined); + const [selectedTaskIndex, setSelectedTaskIndex] = useState(undefined); const [activeTool, setActiveTool] = useState(undefined); const [showFilters, setShowFilters] = useState(false); @@ -76,6 +79,33 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { [selectedWorkflow.data], ); + const flatTasks = useMemo(() => { + return (selectedWorkflow.data?.groups ?? []).flatMap((group) => group.tasks); + }, [selectedWorkflow.data]); + + const forceSingleTaskView = useMemo(() => { + return flatTasks.length === 1; + }, [flatTasks]); + + useEffect(() => { + if (forceSingleTaskView) { + updateUrl({ view: ViewType.SingleTask }); + } else if (!view) { + updateUrl({ view: ViewType.List }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [forceSingleTaskView, view]); + + useEffect(() => { + if (view === ViewType.SingleTask && (!selectedTaskName || retryId === undefined)) { + const task = selectedWorkflow.data?.groups?.[0]?.tasks?.[0]; + if (task) { + updateUrl({ task: task.name, retry_id: task.retry_id }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [view, selectedTaskName, retryId, selectedWorkflow.data]); + // Initialize localStorage values after component mounts useEffect(() => { try { @@ -107,30 +137,56 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { }, [selectedWorkflow.data, tool]); useEffect(() => { - if (selectedTaskName) { - setSelectedTask( - selectedWorkflow.data?.groups - ?.find((g) => - g.tasks.some((t) => t.name === selectedTaskName && (retryId === undefined || t.retry_id === retryId)), - ) - ?.tasks.find((t) => t.name === selectedTaskName && (retryId === undefined || t.retry_id === retryId)), - ); + if (selectedTaskName && retryId !== undefined) { + const taskIndex = flatTasks.findIndex((t) => t.name === selectedTaskName && t.retry_id === retryId); + + setSelectedTask(taskIndex === undefined ? undefined : flatTasks[taskIndex]); + setSelectedTaskIndex(taskIndex); } else { setSelectedTask(undefined); + setSelectedTaskIndex(undefined); + } + }, [flatTasks, selectedTaskName, retryId]); + + const nextTask = useMemo(() => { + if (selectedTaskIndex !== undefined && selectedTaskIndex < flatTasks.length - 1) { + return flatTasks[selectedTaskIndex + 1]; + } + return undefined; + }, [selectedTaskIndex, flatTasks]); + + const previousTask = useMemo(() => { + if (selectedTaskIndex !== undefined && selectedTaskIndex > 0) { + return flatTasks[selectedTaskIndex - 1]; + } + return undefined; + }, [selectedTaskIndex, flatTasks]); + + const onNextTask = useCallback(() => { + if (nextTask) { + updateUrl({ task: nextTask.name, retry_id: nextTask.retry_id }); + } + }, [nextTask, updateUrl]); + + const onPreviousTask = useCallback(() => { + if (previousTask) { + updateUrl({ task: previousTask.name, retry_id: previousTask.retry_id }); } - }, [selectedWorkflow?.data, selectedTaskName, retryId]); + }, [previousTask, updateUrl]); const gridClass = useMemo(() => { - if (showWF && selectedTask && taskPinned) { + if (view === ViewType.SingleTask) { + return "grid grid-cols-[1fr_auto_1fr] p-global gap-1"; + } else if (showWF && selectedTask && taskPinned) { return "grid grid-cols-[auto_1fr_auto]"; } else if (showWF) { - return "grid grid-cols-[auto_1fr]"; + return "grid grid-cols-[25vw_75vw]"; } else if (taskPinned && selectedTask) { return "grid grid-cols-[1fr_auto]"; } else { return "flex flex-row"; } - }, [showWF, selectedTask, taskPinned]); + }, [showWF, selectedTask, taskPinned, view]); const verbose = useMemo(() => { const tasks = (selectedWorkflow.data?.groups ?? []).flatMap((group) => group.tasks); @@ -142,7 +198,7 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { if (!props.allNodes && props.nodes.length === 0) { errors.push("Please select at least one node"); } - if (!props.allStatuses && props.statuses.length === 0) { + if (props.statusFilterType === StatusFilterType.CUSTOM && !props.statuses?.length) { errors.push("Please select at least one status"); } return errors; @@ -157,7 +213,7 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { ); } - if (!selectedWorkflow.data) { + if (!selectedWorkflow.data || !view) { return (
-
- -
+ +
{workflowNameParts && workflowNameParts.id > 1 ? ( @@ -203,13 +242,13 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { ) : ( )} -

{params.name}

+

{params.name}

{workflowNameParts && ( @@ -220,41 +259,79 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { )}
-
-
- updateUrl({ view: ViewType.List })} + {!forceSingleTaskView && ( + <> + { + updateUrl({ showWF: !showWF }); + }} + aria-pressed={showWF} + disabled={view === ViewType.SingleTask} > - - List - - updateUrl({ view: ViewType.Graph })} + + +
- - Graph - -
- -
+ updateUrl({ view: ViewType.SingleTask, showWF: true })} + > + + + Task + + + updateUrl({ view: ViewType.List })} + > + + + List + + + updateUrl({ view: ViewType.Graph })} + > + + + Graph + + +
+ + + )} + +
{ @@ -262,43 +339,38 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { }} aria-label="Tasks Filter" className="z-20 border-t-0 w-100" - dimBackground={false} > -
-
{showWF && (
-

Workflow Details

- +

Workflow Details

+ {view !== ViewType.SingleTask && ( + + )}
)} -
-
+
+
{selectedWorkflow.data?.groups?.length > 0 ? (

No tasks found

@@ -338,7 +410,7 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { nodes={nodes} allNodes={isSelectAllNodesChecked} statuses={statusFilter} - allStatuses={allStatuses} + statusFilterType={statusFilterType} pod_ip={podIp} selectedTask={view === ViewType.List ? selectedTask : undefined} visible={view === ViewType.List} @@ -348,10 +420,9 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) {
{ setTaskPinned(pinned); @@ -359,6 +430,7 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { }} id={"task-details"} header="Task Details" + aria-label={`Task Details for ${selectedTaskName}`} open={!!selectedTask} onClose={() => { updateUrl({ task: null }); @@ -371,6 +443,11 @@ export default function WorkflowOverviewPage({ params }: WorkflowSlugParams) { )} diff --git a/ui/src/app/workflows/components/CancelWorkflow.tsx b/ui/src/app/workflows/components/CancelWorkflow.tsx index 1567ac88..30476197 100644 --- a/ui/src/app/workflows/components/CancelWorkflow.tsx +++ b/ui/src/app/workflows/components/CancelWorkflow.tsx @@ -66,7 +66,7 @@ export const CancelWorkflow = ({ return (
-
+

Are you sure you want to cancel workflow {name}?

@@ -79,6 +79,7 @@ export const CancelWorkflow = ({ disabled={showSuccess || !!error} />
- {showSuccess ? ( - Workflow canceled - ) : error ? ( - -
+ + {showSuccess ? ( + "Workflow canceled" + ) : error ? ( +

Error canceling workflow

{error}

-
- ) : null} + ) : ( + "" + )} +
{!showSuccess && !error && ( + ) : ( + )} - {task.node_name && ( - <> -
Node
-
- -
- +

{task.name}

+ {hasNext ? ( + + ) : ( + )} - {task.pod_name && ( - <> -
Pod Name
-
{task.pod_name}
- - )} - {task.pod_ip && ( - <> -
Pod IP
-
{task.pod_ip}
- - )} - {processingDuration && ( - <> -
Processing Time
-
{processingDuration}
- - )} - {schedulingDuration && ( - <> -
Scheduling Time
-
{schedulingDuration}
- - )} - {initializingDuration && ( - <> -
Initializing Time
-
{initializingDuration}
- - )} - {runningDuration && ( - <> -
Run Time
-
{runningDuration}
- - )} - {task.start_time && ( - <> -
Start
-
{convertToReadableTimezone(task.start_time)}
- - )} - {task.end_time && ( - <> -
End
-
{convertToReadableTimezone(task.end_time)}
- - )} - {task.exit_code !== null && ( - <> -
Exit Code
-
- - {task.exit_code} - -
- - )} - {extraData && - Object.entries(extraData).map(([key, value]) => ( - -
{key}
-
{value}
-
- ))} -
- {task.failure_message && ( -
- -

{task.failure_message}

-
+ + )} +
+
+
+
Status
+
+ +
+
Lead
+
{task.lead ? "True" : "False"}
+ {task.retry_id !== null && ( + <> +
Retry ID
+
{task.retry_id}
+ + )} + {task.node_name && ( + <> +
Node
+
+ +
+ + )} + {task.pod_name && ( + <> +
Pod Name
+
{task.pod_name}
+ + )} + {task.pod_ip && ( + <> +
Pod IP
+
{task.pod_ip}
+ + )} + {processingDuration && ( + <> +
Processing Time
+
{processingDuration}
+ + )} + {schedulingDuration && ( + <> +
Scheduling Time
+
{schedulingDuration}
+ + )} + {initializingDuration && ( + <> +
Initializing Time
+
{initializingDuration}
+ + )} + {runningDuration && ( + <> +
Run Time
+
{runningDuration}
+ + )} + {task.start_time && ( + <> +
Start
+
{convertToReadableTimezone(task.start_time)}
+ + )} + {task.end_time && ( + <> +
End
+
{convertToReadableTimezone(task.end_time)}
+ + )} + {task.exit_code !== null && ( + <> +
Exit Code
+
+ + {task.exit_code} + +
+ )} -
+ {extraData && + Object.entries(extraData).map(([key, value]) => ( + +
{key}
+
{value}
+
+ ))} + + {task.failure_message && ( +
+ +

{task.failure_message}

+
+ )}
- +
); }; diff --git a/ui/src/app/workflows/components/TaskTableRowAction.tsx b/ui/src/app/workflows/components/TaskTableRowAction.tsx index 92916336..23515290 100644 --- a/ui/src/app/workflows/components/TaskTableRowAction.tsx +++ b/ui/src/app/workflows/components/TaskTableRowAction.tsx @@ -13,7 +13,7 @@ //limitations under the License. //SPDX-License-Identifier: Apache-2.0 -import { useRef, useEffect } from "react"; +import { useRef } from "react"; import { Tag, TagSizes, Colors } from "~/components/Tag"; import { formatForWrapping } from "~/utils/string"; @@ -21,6 +21,7 @@ import { formatForWrapping } from "~/utils/string"; import { type ToolParamUpdaterProps } from "../hooks/useToolParamUpdater"; interface TaskTableRowActionProps { + id: string; name: string; retry_id: number | null; lead: boolean; @@ -28,10 +29,10 @@ interface TaskTableRowActionProps { verbose?: boolean; updateUrl: (params: ToolParamUpdaterProps) => void; extraParams?: Record; - disableScrollIntoView?: boolean; } export const TaskTableRowAction = ({ + id, name, retry_id, lead, @@ -39,18 +40,12 @@ export const TaskTableRowAction = ({ verbose, updateUrl, extraParams, - disableScrollIntoView = false, }: TaskTableRowActionProps) => { const buttonRef = useRef(null); - useEffect(() => { - if (buttonRef.current && selected && !disableScrollIntoView) { - buttonRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); - } - }, [selected, disableScrollIntoView]); - return ( -
-
+ {headerChildren} +
+ +
+
{children}
-
- + +
); }; diff --git a/ui/src/components/HomepageCards.tsx b/ui/src/components/HomepageCards.tsx index 60db8718..6963765d 100644 --- a/ui/src/components/HomepageCards.tsx +++ b/ui/src/components/HomepageCards.tsx @@ -21,6 +21,7 @@ import Link from "next/link"; import { useRuntimeEnv } from "~/runtime-env"; interface HomepageCardProps { + id: string; title: string; imageUrl: string; imageAlt: string; @@ -30,6 +31,7 @@ interface HomepageCardProps { } export const HomepageCard: React.FC = ({ + id, title, imageUrl, imageAlt, @@ -38,7 +40,11 @@ export const HomepageCard: React.FC = ({ workflowLink, }) => { return ( -
+
= ({ />
-
-

{title}

+
+

{title}

{body}

@@ -59,12 +65,14 @@ export const HomepageCard: React.FC = ({ href={tutorialLink} target="_blank" rel="noopener noreferrer" + aria-describedby={`${id}-title`} > View Tutorial Launch Workflow @@ -78,10 +86,20 @@ export const HomepageCards = () => { const runtimeEnv = useRuntimeEnv(); return ( -
-

Getting Started

-
+
+

+ Getting Started +

+
{ workflowLink="/workflows/submit/isaac_sim_sdg" /> { workflowLink="/workflows/submit/mnist_training" /> { workflowLink="/workflows/submit/reinforcement_learning" /> {
-

Welcome to {env.NEXT_PUBLIC_APP_NAME}

+

Welcome to {env.NEXT_PUBLIC_APP_NAME}

Run your workflows seamlessly on any cloud environment including AWS, Azure, GCP, NVIDIA Omniverse Cloud and On-premise Kubernetes clusters

-
    +
    • Build a Data factory to manage your synthetic and real data
    • Train Deep Neural Networks with experiment tracking
    • Evaluate your models and publish the results
    • diff --git a/ui/src/components/IconButton.tsx b/ui/src/components/IconButton.tsx new file mode 100644 index 00000000..d1e33f14 --- /dev/null +++ b/ui/src/components/IconButton.tsx @@ -0,0 +1,23 @@ +import { FilledIcon } from "./Icon"; + +export const IconButton = ({ + icon, + text, + alwaysShowText = false, + ...props +}: React.ButtonHTMLAttributes & { icon: string; text?: string; alwaysShowText?: boolean }) => { + return ( + + ); +}; diff --git a/ui/src/components/InlineBanner.tsx b/ui/src/components/InlineBanner.tsx index 92b268ca..d16d3ccf 100644 --- a/ui/src/components/InlineBanner.tsx +++ b/ui/src/components/InlineBanner.tsx @@ -42,10 +42,10 @@ export const InlineBanner = ({ return (
      - {statusIcon[status] && {statusIcon[status]}} + {statusIcon[status] && } {children}
      ); diff --git a/ui/src/components/JSONEditor.tsx b/ui/src/components/JSONEditor.tsx index b68cf24e..28b2ab34 100644 --- a/ui/src/components/JSONEditor.tsx +++ b/ui/src/components/JSONEditor.tsx @@ -146,8 +146,8 @@ export const JSONEditor: React.FC = ({ onSubmit={handleSaveChanges} className="w-full h-full" > -
      -
      +
      +
      {isEditMode ? (