From 56f12ddfaa896c355fa2cfb05ea3af98ffb11398 Mon Sep 17 00:00:00 2001 From: Fernando Luo Date: Thu, 12 Mar 2026 20:32:16 -0700 Subject: [PATCH 01/14] Add ended_after and ended_before to /tasks API --- src/service/core/workflow/helpers.py | 10 +++++++++- src/service/core/workflow/workflow_service.py | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/service/core/workflow/helpers.py b/src/service/core/workflow/helpers.py index 23519004e..c10fa215f 100644 --- a/src/service/core/workflow/helpers.py +++ b/src/service/core/workflow/helpers.py @@ -1,5 +1,5 @@ """ -SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-FileCopyrightText: Copyright (c) 2025-2026 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. @@ -119,6 +119,8 @@ def get_tasks(workflow_id: str | None = None, nodes: List[str] | None = None, started_after: datetime.datetime | None = None, started_before: datetime.datetime | None = None, + ended_after: datetime.datetime | None = None, + ended_before: datetime.datetime | None = None, offset: int = 0, limit: int = 20, order: connectors.ListOrder = fastapi.Query(default=connectors.ListOrder.ASC), @@ -181,6 +183,12 @@ def get_tasks(workflow_id: str | None = None, if started_before: commands.append('(tasks.start_time < %s AND tasks.start_time is not NULL)') fetch_input.append(started_before.replace(microsecond=0).isoformat()) + if ended_after: + commands.append('(tasks.end_time >= %s OR tasks.end_time IS NULL)') + fetch_input.append(ended_after.replace(microsecond=0).isoformat()) + if ended_before: + commands.append('(tasks.end_time < %s AND tasks.end_time IS NOT NULL)') + fetch_input.append(ended_before.replace(microsecond=0).isoformat()) if priority: commands.append('workflows.priority IN %s') fetch_input.append(tuple(p.value for p in priority)) diff --git a/src/service/core/workflow/workflow_service.py b/src/service/core/workflow/workflow_service.py index 747d87eac..51e743ebc 100644 --- a/src/service/core/workflow/workflow_service.py +++ b/src/service/core/workflow/workflow_service.py @@ -637,6 +637,8 @@ def list_task(workflow_id: str | None = None, nodes: List[str] | None = fastapi.Query(default = None), started_after: datetime.datetime | None = None, started_before: datetime.datetime | None = None, + ended_after: datetime.datetime | None = None, + ended_before: datetime.datetime | None = None, offset: int = 0, limit: int = 20, order: connectors.ListOrder = fastapi.Query(default=connectors.ListOrder.ASC), @@ -665,8 +667,8 @@ def list_task(workflow_id: str | None = None, if all_pools: pools = [] rows = helpers.get_tasks(workflow_id, statuses, users, pools, nodes, - started_after, started_before, offset, limit, order, summary, - aggregate_by_workflow, + started_after, started_before, ended_after, ended_before, + offset, limit, order, summary, aggregate_by_workflow, priority=priority, return_raw=True) if summary: return objects.ListTaskSummaryResponse.from_db_rows(rows) From 44356862653a23af2652a6edfb102e99d47b0475 Mon Sep 17 00:00:00 2001 From: Fernando Luo Date: Thu, 12 Mar 2026 20:36:44 -0700 Subject: [PATCH 02/14] Regenerate client api layer --- src/ui/e2e/mocks/generated-mocks.ts | 2 ++ src/ui/openapi.json | 20 ++++++++++++++++++++ src/ui/src/lib/api/generated.ts | 2 ++ 3 files changed, 24 insertions(+) diff --git a/src/ui/e2e/mocks/generated-mocks.ts b/src/ui/e2e/mocks/generated-mocks.ts index 0fc09ad23..516616572 100644 --- a/src/ui/e2e/mocks/generated-mocks.ts +++ b/src/ui/e2e/mocks/generated-mocks.ts @@ -2111,6 +2111,8 @@ all_pools?: boolean; nodes?: string[]; started_after?: string; started_before?: string; +ended_after?: string; +ended_before?: string; offset?: number; limit?: number; order?: ListOrder; diff --git a/src/ui/openapi.json b/src/ui/openapi.json index f9ffaf92c..df7633212 100644 --- a/src/ui/openapi.json +++ b/src/ui/openapi.json @@ -4985,6 +4985,26 @@ "name": "started_before", "in": "query" }, + { + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "title": "Ended After" + }, + "name": "ended_after", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "title": "Ended Before" + }, + "name": "ended_before", + "in": "query" + }, { "required": false, "schema": { diff --git a/src/ui/src/lib/api/generated.ts b/src/ui/src/lib/api/generated.ts index 2af5aa516..411985f1e 100644 --- a/src/ui/src/lib/api/generated.ts +++ b/src/ui/src/lib/api/generated.ts @@ -2118,6 +2118,8 @@ all_pools?: boolean; nodes?: string[]; started_after?: string; started_before?: string; +ended_after?: string; +ended_before?: string; offset?: number; limit?: number; order?: ListOrder; From 5f9ba74df78fad053e06ea06502d4cb41b8c4f77 Mon Sep 17 00:00:00 2001 From: Fernando Luo Date: Thu, 12 Mar 2026 20:59:51 -0700 Subject: [PATCH 03/14] Add shadcn chart and css --- src/ui/package.json | 1 + src/ui/pnpm-lock.yaml | 219 ++++++++++++++ src/ui/src/app/globals.css | 12 + src/ui/src/components/shadcn/chart.tsx | 385 +++++++++++++++++++++++++ 4 files changed, 617 insertions(+) create mode 100644 src/ui/src/components/shadcn/chart.tsx diff --git a/src/ui/package.json b/src/ui/package.json index 42f3a8c6c..33affe0c8 100644 --- a/src/ui/package.json +++ b/src/ui/package.json @@ -89,6 +89,7 @@ "react-dom": "19.2.3", "react-error-boundary": "^6.0.1", "react-hotkeys-hook": "^5.2.3", + "recharts": "^2.15.4", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "usehooks-ts": "^3.1.1", diff --git a/src/ui/pnpm-lock.yaml b/src/ui/pnpm-lock.yaml index f4b8bd662..9920c0b06 100644 --- a/src/ui/pnpm-lock.yaml +++ b/src/ui/pnpm-lock.yaml @@ -179,6 +179,9 @@ importers: react-hotkeys-hook: specifier: ^5.2.3 version: 5.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + recharts: + specifier: ^2.15.4 + version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2083,18 +2086,39 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} '@types/d3-drag@3.0.7': resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + '@types/d3-selection@3.0.11': resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/d3-transition@3.0.9': resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} @@ -2729,6 +2753,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} @@ -2745,14 +2773,38 @@ packages: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + d3-interpolate@3.0.1: resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} engines: {node: '>=12'} + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + d3-selection@3.0.0: resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} engines: {node: '>=12'} + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + d3-timer@3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} @@ -2806,6 +2858,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -2831,6 +2886,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -3054,6 +3112,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + execa@9.6.1: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} @@ -3065,6 +3126,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -3350,6 +3415,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3688,6 +3757,9 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4299,6 +4371,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -4323,6 +4398,12 @@ packages: '@types/react': optional: true + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -4333,6 +4414,12 @@ packages: '@types/react': optional: true + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} @@ -4341,6 +4428,16 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4648,6 +4745,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4843,6 +4943,9 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7013,18 +7116,36 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} + '@types/d3-color@3.1.3': {} '@types/d3-drag@3.0.7': dependencies: '@types/d3-selection': 3.0.11 + '@types/d3-ease@3.0.2': {} + '@types/d3-interpolate@3.0.4': dependencies: '@types/d3-color': 3.1.3 + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + '@types/d3-selection@3.0.11': {} + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/d3-transition@3.0.9': dependencies: '@types/d3-selection': 3.0.11 @@ -7726,6 +7847,10 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + d3-color@3.1.0: {} d3-dispatch@3.0.1: {} @@ -7737,12 +7862,36 @@ snapshots: d3-ease@3.0.1: {} + d3-format@3.1.2: {} + d3-interpolate@3.0.1: dependencies: d3-color: 3.1.0 + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-selection@3.0.0: {} + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + d3-timer@3.0.1: {} d3-transition@3.0.1(d3-selection@3.0.0): @@ -7797,6 +7946,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} deep-is@0.1.4: {} @@ -7821,6 +7972,11 @@ snapshots: dependencies: esutils: 2.0.3 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.6 + csstype: 3.2.3 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -8218,6 +8374,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@4.0.7: {} + execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -8237,6 +8395,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8522,6 +8682,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -8834,6 +8996,8 @@ snapshots: lodash.uniq@4.5.0: {} + lodash@4.17.23: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -9428,6 +9592,8 @@ snapshots: react-is@16.13.1: {} + react-is@18.3.1: {} + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.3): @@ -9449,6 +9615,14 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-smooth@4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-transition-group: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.3): dependencies: get-nonce: 1.0.1 @@ -9457,10 +9631,36 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react@19.2.3: {} readdirp@5.0.0: {} + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.23 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -9859,6 +10059,8 @@ snapshots: tapable@2.3.0: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -10068,6 +10270,23 @@ snapshots: - '@types/react' - '@types/react-dom' + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2): dependencies: esbuild: 0.27.3 diff --git a/src/ui/src/app/globals.css b/src/ui/src/app/globals.css index 0e4cdc985..c5ed2ddb9 100644 --- a/src/ui/src/app/globals.css +++ b/src/ui/src/app/globals.css @@ -249,6 +249,12 @@ --sidebar-border: oklch(92.2% 0 0deg); --sidebar-ring: oklch(70.8% 0 0deg); + /* Chart: utilization dashboard (NVIDIA-branded palette) */ + --chart-gpu: #76b900; + --chart-cpu: oklch(60% 0.15 250deg); + --chart-memory: oklch(65% 0.15 300deg); + --chart-storage: oklch(75% 0.15 85deg); + /* Brand colors */ --nvidia-green: #76b900; --nvidia-green-dark: #5a8c00; @@ -368,6 +374,12 @@ --sidebar-border: oklch(100% 0 0deg / 10%); --sidebar-ring: oklch(55.6% 0 0deg); + /* Chart: utilization dashboard (dark mode - adjusted lightness) */ + --chart-gpu: #76b900; + --chart-cpu: oklch(70% 0.15 250deg); + --chart-memory: oklch(75% 0.15 300deg); + --chart-storage: oklch(80% 0.15 85deg); + /* Brand colors - same in dark mode except bg */ --nvidia-green: #76b900; --nvidia-green-dark: #5a8c00; diff --git a/src/ui/src/components/shadcn/chart.tsx b/src/ui/src/components/shadcn/chart.tsx new file mode 100644 index 000000000..388e455fa --- /dev/null +++ b/src/ui/src/components/shadcn/chart.tsx @@ -0,0 +1,385 @@ +//SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. 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 + +"use client"; + +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "@/lib/utils"; + +const THEMES = { light: "", dark: ".dark" } as const; + +type ChartConfig = Record< + string, + { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +>; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color, + ); + + if (!colorConfig.length) { + return null; + } + + return ( + ; }; const ChartTooltip = RechartsPrimitive.Tooltip; @@ -322,3 +318,9 @@ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, useChart }; export type { ChartConfig }; + +export const Area = RechartsPrimitive.Area; +export const AreaChart = RechartsPrimitive.AreaChart; +export const CartesianGrid = RechartsPrimitive.CartesianGrid; +export const XAxis = RechartsPrimitive.XAxis; +export const YAxis = RechartsPrimitive.YAxis; diff --git a/src/ui/src/components/utilization-chart/utilization-chart.tsx b/src/ui/src/components/utilization-chart/utilization-chart.tsx index 49700bf10..f2c160117 100644 --- a/src/ui/src/components/utilization-chart/utilization-chart.tsx +++ b/src/ui/src/components/utilization-chart/utilization-chart.tsx @@ -16,10 +16,19 @@ "use client"; -import { useState, useMemo, useCallback } from "react"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { useState, useReducer, useMemo, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn/card"; -import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/shadcn/chart"; +import { + Area, + AreaChart, + CartesianGrid, + XAxis, + YAxis, + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/shadcn/chart"; import { Skeleton } from "@/components/shadcn/skeleton"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/shadcn/popover"; import { InlineErrorBoundary } from "@/components/error/inline-error-boundary"; @@ -102,14 +111,34 @@ function rangeFromPreset(key: PresetKey): { start: number; end: number } { return { start: end - ms, end }; } +type RangeState = + | { preset: PresetKey; range: { start: number; end: number }; from: ""; to: "" } + | { preset: null; range: { start: number; end: number }; from: string; to: string }; + +type RangeAction = + | { type: "preset"; key: PresetKey } + | { type: "custom"; range: { start: number; end: number }; from: string; to: string }; + +function rangeReducer(_: RangeState, action: RangeAction): RangeState { + switch (action.type) { + case "preset": + return { preset: action.key, range: rangeFromPreset(action.key), from: "", to: "" }; + case "custom": + return { preset: null, range: action.range, from: action.from, to: action.to }; + } +} + export function UtilizationChart() { - const [activePreset, setActivePreset] = useState(DEFAULT_PRESET); + const [rangeState, dispatchRange] = useReducer(rangeReducer, null, () => ({ + preset: DEFAULT_PRESET, + range: rangeFromPreset(DEFAULT_PRESET), + from: "" as const, + to: "" as const, + })); const [activeMetric, setActiveMetric] = useState("gpu"); - const [range, setRange] = useState(rangeFromPreset(DEFAULT_PRESET)); const [popoverOpen, setPopoverOpen] = useState(false); - const [customFrom, setCustomFrom] = useState(""); - const [customTo, setCustomTo] = useState(""); + const { range } = rangeState; const displayStartMs = range.start; const displayEndMs = range.end; @@ -130,22 +159,17 @@ export function UtilizationChart() { return result; }, [buckets, granularityMs]); - const isCustom = activePreset === null; + const isCustom = rangeState.preset === null; const handlePresetClick = useCallback((key: PresetKey) => { - setActivePreset(key); - setRange(rangeFromPreset(key)); - setCustomFrom(""); - setCustomTo(""); + dispatchRange({ type: "preset", key }); }, []); const handleCustomCommit = useCallback((result: DateRangePickerResult) => { const parsed = parseDateRangeValue(result.value); if (parsed && parsed.end > parsed.start) { - setActivePreset(null); - setRange({ start: parsed.start.getTime(), end: parsed.end.getTime() }); - setCustomFrom(result.value.split("..")[0] ?? ""); - setCustomTo(result.value.split("..")[1] ?? ""); + const [from = "", to = ""] = result.value.split(".."); + dispatchRange({ type: "custom", range: { start: parsed.start.getTime(), end: parsed.end.getTime() }, from, to }); setPopoverOpen(false); } }, []); @@ -167,7 +191,7 @@ export function UtilizationChart() { className={cn( "px-2.5 py-1 text-xs font-medium transition-colors", "first:rounded-l-md last:rounded-r-md", - !isCustom && activePreset === p.key + !isCustom && rangeState.preset === p.key ? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900" : "text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100", )} @@ -198,8 +222,8 @@ export function UtilizationChart() { align="start" > diff --git a/src/ui/src/features/dashboard/dashboard-content.tsx b/src/ui/src/features/dashboard/dashboard-content.tsx index 09b89fc25..ed1747fb3 100644 --- a/src/ui/src/features/dashboard/dashboard-content.tsx +++ b/src/ui/src/features/dashboard/dashboard-content.tsx @@ -28,7 +28,12 @@ import { cn } from "@/lib/utils"; import { getStatusDisplay, STATUS_STYLES } from "@/lib/workflows/workflow-constants"; import { WORKFLOW_STATUS_ICONS } from "@/lib/workflows/workflow-status-icons"; import { STATUS_PRESETS } from "@/lib/workflows/workflow-status-presets"; -import { UtilizationChart } from "@/components/utilization-chart/utilization-chart"; +import dynamic from "next/dynamic"; + +const UtilizationChart = dynamic( + () => import("@/components/utilization-chart/utilization-chart").then((m) => ({ default: m.UtilizationChart })), + { ssr: false }, +); interface DashboardContentProps { /** Server-computed 24h cutoff (ISO string) — ensures query key matches between SSR and client */ diff --git a/src/ui/src/hooks/use-utilization-data.ts b/src/ui/src/hooks/use-utilization-data.ts index 9343fa7a3..a1d5566ea 100644 --- a/src/ui/src/hooks/use-utilization-data.ts +++ b/src/ui/src/hooks/use-utilization-data.ts @@ -75,10 +75,12 @@ interface UseUtilizationDataParams { displayEndMs: number; } -export function useUtilizationData({ - displayStartMs, - displayEndMs, -}: UseUtilizationDataParams): UtilizationResult & { isLoading: boolean; granularityMs: number } { +export function useUtilizationData({ displayStartMs, displayEndMs }: UseUtilizationDataParams): UtilizationResult & { + isLoading: boolean; + granularityMs: number; + error: Error | null; + refetch: () => void; +} { const rangeMs = displayEndMs - displayStartMs; const tier: FetchTier = selectTier(rangeMs); const tierMs = TIER_MS[tier];