From 6f4da60bc6ab16c2358ef108ef62a8654a41706e Mon Sep 17 00:00:00 2001 From: Matt Yoder Date: Fri, 12 Jul 2024 16:07:50 -0400 Subject: [PATCH] feat(resource-catalog): add queue metrics component --- package-lock.json | 20 ++++- package.json | 3 +- .../api/resource-groups/1/queue-metrics.json | 31 ++++++++ src/footer.css | 1 + src/info-tip.css | 26 +++++++ src/info-tip.jsx | 22 ++++++ src/resource-catalog.jsx | 4 + src/resource-group-detail.jsx | 5 ++ src/resource-group-queue-metrics.jsx | 74 +++++++++++++++++++ 9 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 public/api/resource-groups/1/queue-metrics.json create mode 100644 src/info-tip.css create mode 100644 src/info-tip.jsx create mode 100644 src/resource-group-queue-metrics.jsx diff --git a/package-lock.json b/package-lock.json index 22a3c7e..4692a4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@glidejs/glide": "^3.6.1", "fuse.js": "^7.0.0", "preact": "^10.13.1", - "preact-iso": "^2.6.3" + "preact-iso": "^2.6.3", + "tippy.js": "^6.3.7" }, "devDependencies": { "@preact/preset-vite": "^2.5.0", @@ -1036,6 +1037,15 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@preact/preset-vite": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.5.0.tgz", @@ -3899,6 +3909,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index 6d2166d..fc8e891 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "@glidejs/glide": "^3.6.1", "fuse.js": "^7.0.0", "preact": "^10.13.1", - "preact-iso": "^2.6.3" + "preact-iso": "^2.6.3", + "tippy.js": "^6.3.7" }, "devDependencies": { "@preact/preset-vite": "^2.5.0", diff --git a/public/api/resource-groups/1/queue-metrics.json b/public/api/resource-groups/1/queue-metrics.json new file mode 100644 index 0000000..61f5fac --- /dev/null +++ b/public/api/resource-groups/1/queue-metrics.json @@ -0,0 +1,31 @@ +{ + "queueMetrics": [ + { + "resourceId": 1, + "name": "Bridges-2 Regular Memory", + "resourceRepositoryKey": 2900, + "waitTime": 5.909166, + "wallTime": 2.007403, + "expansionFactor": 5.3611, + "order": 1 + }, + { + "resourceId": 2, + "name": "Bridges-2 Extreme Memory", + "resourceRepositoryKey": 2903, + "waitTime": 5.233983, + "wallTime": 6.61043, + "expansionFactor": 1.7911, + "order": 2 + }, + { + "resourceId": 3, + "name": "Bridges-2 GPU", + "resourceRepositoryKey": 2902, + "waitTime": 19.670669, + "wallTime": 2.072165, + "expansionFactor": 10.4929, + "order": 3 + } + ] +} diff --git a/src/footer.css b/src/footer.css index a65ccf4..439d55c 100644 --- a/src/footer.css +++ b/src/footer.css @@ -109,6 +109,7 @@ a.linkedin { text-indent: -999999px; transition: opacity 0.25s ease-out; width: 40px; + z-index: 100; } .scroll-to-top.visible { opacity: 1; diff --git a/src/info-tip.css b/src/info-tip.css new file mode 100644 index 0000000..e835362 --- /dev/null +++ b/src/info-tip.css @@ -0,0 +1,26 @@ +.info-tip { + background-color: transparent; + border: 0 none transparent; +} +.tippy-box[data-theme~="access"] { + background-color: var(--teal-700); + border-radius: 0; + color: white; + font-weight: 400; + padding: 5px; +} +.tippy-box[data-theme~="access"][data-placement^="top"] > .tippy-arrow::before { + border-top-color: var(--teal-700); +} +.tippy-box[data-theme~="access"][data-placement^="bottom"] + > .tippy-arrow::before { + border-bottom-color: var(--teal-700); +} +.tippy-box[data-theme~="access"][data-placement^="left"] + > .tippy-arrow::before { + border-left-color: var(--teal-700); +} +.tippy-box[data-theme~="access"][data-placement^="right"] + > .tippy-arrow::before { + border-right-color: var(--teal-700); +} diff --git a/src/info-tip.jsx b/src/info-tip.jsx new file mode 100644 index 0000000..4e0d9f1 --- /dev/null +++ b/src/info-tip.jsx @@ -0,0 +1,22 @@ +import { useLayoutEffect, useRef } from "preact/hooks"; +import tippy from "tippy.js"; + +import Icon from "./icon"; + +export default function InfoTip({ + color = "var(--contrast-6)", + icon = "info-circle-fill", + tooltip, +}) { + const trigger = useRef(null); + useLayoutEffect(() => { + if (trigger.current) + tippy(trigger.current, { content: tooltip, theme: "access" }); + }, [trigger.current]); + + return ( + + ); +} diff --git a/src/resource-catalog.jsx b/src/resource-catalog.jsx index 3a87244..3bf364e 100644 --- a/src/resource-catalog.jsx +++ b/src/resource-catalog.jsx @@ -4,6 +4,7 @@ import baseStyle from "./base.css?inline"; import componentsStyle from "./components.css?inline"; import contentStyle from "./content.css?inline"; import iconStyle from "./icon.css?inline"; +import infoTipStyle from "./info-tip.css?inline"; import glideCoreStyle from "@glidejs/glide/dist/css/glide.core.min.css?inline"; import glideThemeStyle from "@glidejs/glide/dist/css/glide.theme.min.css?inline"; import gridStyle from "./grid.css?inline"; @@ -12,6 +13,7 @@ import resourceFiltersStyle from "./resource-filters.css?inline"; import resourceGroupStyle from "./resource-group.css?inline"; import searchStyle from "./search.css?inline"; import tagsStyle from "./tags.css?inline"; +import tippyStyle from "tippy.js/dist/tippy.css?inline"; const ResourceGroupDetail = lazy(() => import("./resource-group-detail.jsx")); const ResourceHome = lazy(() => import("./resource-home.jsx")); @@ -48,6 +50,7 @@ export function ResourceCatalog({ + @@ -56,6 +59,7 @@ export function ResourceCatalog({ + ); } diff --git a/src/resource-group-detail.jsx b/src/resource-group-detail.jsx index c71e322..861054a 100644 --- a/src/resource-group-detail.jsx +++ b/src/resource-group-detail.jsx @@ -1,4 +1,5 @@ import ResourceGroupDescription from "./resource-group-description"; +import ResourceGroupQueueMetrics from "./resource-group-queue-metrics"; import ResourceGroupResources from "./resource-group-resources"; import ResourceGroupSoftware from "./resource-group-software"; @@ -17,6 +18,10 @@ export default function ResourceGroupDetail({ baseUri, resourceGroupId }) { baseUri={baseUri} resourceGroupId={resourceGroupId} /> + ); } diff --git a/src/resource-group-queue-metrics.jsx b/src/resource-group-queue-metrics.jsx new file mode 100644 index 0000000..ecfa25b --- /dev/null +++ b/src/resource-group-queue-metrics.jsx @@ -0,0 +1,74 @@ +import { useJSON } from "./utils"; + +import Grid from "./grid"; +import Icon from "./icon"; +import InfoTip from "./info-tip"; + +const formatTime = (value) => { + const hours = Math.floor(value); + const minutes = Math.floor((value - hours) * 60); + const seconds = Math.round((value - hours) * 3600 - minutes * 60); + return [hours, minutes, seconds] + .map((value) => value.toString().padStart(2, "0")) + .join(":"); +}; + +const formatHeaderTip = (content) => (value) => { + return ( + <> + {value} + + ); +}; + +export default function ResourceGroupQueueMetrics({ + baseUri, + resourceGroupId, +}) { + const data = useJSON( + `${baseUri}/api/resource-groups/${resourceGroupId}/queue-metrics.json`, + null + ); + if (!data || data.error) return; + + const columns = [ + { + key: "name", + name: "Resource", + }, + { + key: "waitTime", + name: "Wait Time", + format: formatTime, + formatHeader: formatHeaderTip( + "Wait time is the average time a job spends waiting in the queue before running on a resource." + ), + }, + { + key: "wallTime", + name: "Wall Time", + format: formatTime, + formatHeader: formatHeaderTip( + "Wall time is the average time a job spends running on a resource." + ), + }, + { + key: "expansionFactor", + name: "Expansion Factor", + format: (value) => value.toFixed(2), + formatHeader: formatHeaderTip( + 'Expansion factor is the ratio of the total time (wait time and wall time) to the wall time. It measures how much waiting in the queue "expands" the total time taken to complete a job relative to its length.' + ), + }, + ]; + + return ( + <> +

+ + Queue Metrics +

+ + + ); +}