From 04a64fd982db0145b3e271992339f7c4456a41c1 Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Tue, 4 Jun 2024 09:33:14 -0500 Subject: [PATCH] Add s3 storage artifact route and ui integration of it --- backend/package-lock.json | 275 +++++++++++++++++- backend/package.json | 1 + backend/src/routes/api/storage/index.ts | 63 ++++ .../src/routes/api/storage/storageUtils.ts | 118 ++++++++ backend/src/types.ts | 75 +++++ backend/src/utils/constants.ts | 1 + docs/dashboard-config.md | 4 +- frontend/src/__mocks__/mockDashboardConfig.ts | 3 + frontend/src/concepts/areas/const.ts | 4 + frontend/src/concepts/areas/types.ts | 7 +- .../content/artifacts/ArtifactUriLink.tsx | 46 +++ .../pipelines/content/artifacts/utils.ts | 13 + .../markdown/MarkdownCompare.tsx | 22 +- .../artifacts/ArtifactNodeDetails.tsx | 5 +- .../artifacts/ArtifactVisualization.tsx | 46 ++- frontend/src/k8sTypes.ts | 1 + .../ArtifactOverviewDetails.tsx | 5 +- .../experiments/artifacts/ArtifactsTable.tsx | 5 +- .../__tests__/ArtifactDetails.spec.tsx | 9 + .../__tests__/ArtifactsTable.spec.tsx | 9 + .../compareRuns/CompareRunsMetricsSection.tsx | 21 +- frontend/src/services/storageService.ts | 19 ++ ...dhdashboardconfigs.opendatahub.io.crd.yaml | 2 + .../odh-dashboard-config.yaml | 1 + 24 files changed, 719 insertions(+), 36 deletions(-) create mode 100644 backend/src/routes/api/storage/index.ts create mode 100644 backend/src/routes/api/storage/storageUtils.ts create mode 100644 frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx create mode 100644 frontend/src/concepts/pipelines/content/artifacts/utils.ts create mode 100644 frontend/src/services/storageService.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 80d86d65cc..4731f4ce4d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,6 +22,7 @@ "http-errors": "^1.8.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21", + "minio": "^7.1.3", "pino": "^8.11.0", "prom-client": "^14.0.1", "ts-node": "^10.9.1" @@ -2450,6 +2451,12 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "devOptional": true }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "optional": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2741,6 +2748,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2758,7 +2770,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -3026,6 +3037,27 @@ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/block-stream2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3047,6 +3079,11 @@ "node": ">=8" } }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, "node_modules/browserslist": { "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", @@ -3123,6 +3160,14 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3180,7 +3225,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "optional": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -3535,6 +3579,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5379,6 +5431,27 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz", "integrity": "sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==" }, + "node_modules/fast-xml-parser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", + "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastify": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.22.0.tgz", @@ -5570,6 +5643,14 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/find-my-way": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.0.tgz", @@ -5618,7 +5699,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "optional": true, "dependencies": { "is-callable": "^1.1.3" } @@ -5790,7 +5870,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "optional": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -5956,7 +6035,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "optional": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -6078,7 +6156,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6090,7 +6167,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "optional": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -6431,6 +6507,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -6495,7 +6586,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -6571,6 +6661,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6698,7 +6802,6 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "optional": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -8652,6 +8755,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "devOptional": true }, + "node_modules/json-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-stream/-/json-stream-1.0.0.tgz", + "integrity": "sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -8941,6 +9049,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minio": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minio/-/minio-7.1.3.tgz", + "integrity": "sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^0.2.13", + "fast-xml-parser": "^4.2.2", + "ipaddr.js": "^2.0.1", + "json-stream": "^1.0.0", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "through2": "^4.0.2", + "web-encoding": "^1.1.5", + "xml": "^1.0.1", + "xml2js": "^0.5.0" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/minio/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/minipass": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", @@ -9715,6 +9855,23 @@ "node": ">=0.6" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10131,6 +10288,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -10310,6 +10472,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -10384,11 +10554,18 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -10515,6 +10692,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -10692,6 +10874,27 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "optional": true }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tiny-lru": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz", @@ -11106,11 +11309,22 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { "version": "3.4.0", @@ -11182,6 +11396,17 @@ "makeerror": "1.0.12" } }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -11213,7 +11438,6 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "optional": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -11338,6 +11562,31 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index 10fe6d13fe..5a0dc59e4e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -50,6 +50,7 @@ "http-errors": "^1.8.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21", + "minio": "^7.1.3", "pino": "^8.11.0", "prom-client": "^14.0.1", "ts-node": "^10.9.1" diff --git a/backend/src/routes/api/storage/index.ts b/backend/src/routes/api/storage/index.ts new file mode 100644 index 0000000000..8e696c5283 --- /dev/null +++ b/backend/src/routes/api/storage/index.ts @@ -0,0 +1,63 @@ +import { FastifyInstance, FastifyReply } from 'fastify'; +import { createMinioClient, getObjectStream } from './storageUtils'; +import { getDashboardConfig } from '../../../utils/resourceUtils'; +import { getDirectCallOptions } from '../../../utils/directCallUtils'; +import { getAccessToken } from '../../../utils/directCallUtils'; +import { OauthFastifyRequest } from '../../../types'; + +export default async (fastify: FastifyInstance): Promise => { + const dashConfig = getDashboardConfig(); + if (dashConfig?.spec.dashboardConfig.disableS3Endpoint === false) { + fastify.get( + '/:namespace/:bucket', + async ( + request: OauthFastifyRequest<{ + Querystring: Record; + Params: { '*': string; [key: string]: string }; + Body: { [key: string]: unknown }; + }>, + reply: FastifyReply, + ) => { + try { + const { namespace, bucket } = request.params; + const query = request.query; + const key = query.key; + + const requestOptions = await getDirectCallOptions(fastify, request, request.url); + const token = getAccessToken(requestOptions); + + const stream = await getObjectStream({ + bucket, + client: await createMinioClient(fastify, token, namespace), + key, + }); + + reply.type('text/plain'); + + await new Promise((resolve, reject) => { + stream.on('data', (chunk) => { + reply.raw.write(chunk); + }); + + stream.on('end', () => { + reply.raw.end(); + resolve(); + }); + + stream.on('error', (err) => { + fastify.log.error('Stream error:', err); + reply.raw.statusCode = 500; + reply.raw.end(err.message); + reject(err); + }); + }); + + return; + } catch (err) { + reply.code(500).send(err.message); + return reply; + } + }, + ); + } +}; diff --git a/backend/src/routes/api/storage/storageUtils.ts b/backend/src/routes/api/storage/storageUtils.ts new file mode 100644 index 0000000000..9ac4849cd8 --- /dev/null +++ b/backend/src/routes/api/storage/storageUtils.ts @@ -0,0 +1,118 @@ +import { Client as MinioClient } from 'minio'; +import { DSPipelineKind, KubeFastifyInstance } from '../../../types'; +import { Transform, PassThrough } from 'stream'; + +/** + * Create minio client with aws instance profile credentials if needed. + * @param config minio client options where `accessKey` and `secretKey` are optional. + */ +export async function createMinioClient( + fastify: KubeFastifyInstance, + token: string, + namespace: string, +): Promise { + try { + const dspaResponse = await fastify.kube.customObjectsApi + .listNamespacedCustomObject( + 'datasciencepipelinesapplications.opendatahub.io', + 'v1alpha1', + namespace, + 'datasciencepipelinesapplications', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ) + .catch((e) => { + throw `A ${ + e.statusCode + } error occurred when trying to fetch dspa aws storage credentials: ${ + e.response?.body?.message || e?.response?.statusMessage + }`; + }); + + const dspas = ( + dspaResponse?.body as { + items: DSPipelineKind[]; + } + )?.items; + + if (!dspas || !dspas.length) { + throw 'No Data Science Pipeline Application found'; + } + + // check if data connection is available + if ( + dspas[0].status.conditions.find((condition) => condition.type === 'ObjectStoreAvailable') + .status !== 'True' + ) { + throw 'Object store is not available'; + } + + // always get the first one + const externalStorage = dspas[0].spec.objectStorage.externalStorage; + + if (externalStorage) { + const { region, host: endPoint, s3CredentialsSecret } = externalStorage; + + // get secret + const secret = await fastify.kube.coreV1Api.readNamespacedSecret( + s3CredentialsSecret.secretName, + namespace, + undefined, + undefined, + undefined, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + const accessKey = atob(secret.body.data[s3CredentialsSecret.accessKey]); + const secretKey = atob(secret.body.data[s3CredentialsSecret.secretKey]); + + if (!accessKey || !secretKey) { + throw 'Access key or secret key is empty'; + } + + // sessionToken + return new MinioClient({ accessKey, secretKey, endPoint, region }); + } + } catch (err) { + console.error('Unable to create minio client: ', err); + throw new Error('Unable to create minio client: ' + err); + } +} + +/** MinioRequestConfig describes the info required to retrieve an artifact. */ +export interface MinioRequestConfig { + bucket: string; + key: string; + client: MinioClient; +} + +/** + * Returns a stream from an object in a s3 compatible object store (e.g. minio). + * + * @param param.bucket Bucket name to retrieve the object from. + * @param param.key Key of the object to retrieve. + * @param param.client Minio client. + * + */ +export async function getObjectStream({ + bucket, + key, + client, +}: MinioRequestConfig): Promise { + const stream = await client.getObject(bucket, key); + return stream.pipe(new PassThrough()); +} diff --git a/backend/src/types.ts b/backend/src/types.ts index 6f702af60a..d727f089ef 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -37,6 +37,7 @@ export type DashboardConfig = K8sResourceCommon & { disableModelMesh: boolean; disableAcceleratorProfiles: boolean; disablePipelineExperiments: boolean; + disableS3Endpoint: boolean; disableDistributedWorkloads: boolean; disableModelRegistry: boolean; }; @@ -1014,9 +1015,83 @@ export type K8sCondition = { lastHeartbeatTime?: string; }; +export type DSPipelineExternalStorageKind = { + bucket: string; + host: string; + port?: ''; + scheme: string; + region: string; + s3CredentialsSecret: { + accessKey: string; + secretKey: string; + secretName: string; + }; +}; + export type DSPipelineKind = K8sResourceCommon & { + metadata: { + name: string; + namespace: string; + }; spec: { dspVersion: string; + apiServer?: Partial<{ + apiServerImage: string; + artifactImage: string; + artifactScriptConfigMap: Partial<{ + key: string; + name: string; + }>; + enableSamplePipeline: boolean; + }>; + database?: Partial<{ + externalDB: Partial<{ + host: string; + passwordSecret: Partial<{ + key: string; + name: string; + }>; + pipelineDBName: string; + port: string; + username: string; + }>; + image: string; + mariaDB: Partial<{ + image: string; + passwordSecret: Partial<{ + key: string; + name: string; + }>; + pipelineDBName: string; + username: string; + }>; + }>; + mlpipelineUI?: { + configMap?: string; + image: string; + }; + persistentAgent?: Partial<{ + image: string; + pipelineAPIServerName: string; + }>; + scheduledWorkflow?: Partial<{ + image: string; + }>; + objectStorage: Partial<{ + externalStorage: DSPipelineExternalStorageKind; + minio: Partial<{ + bucket: string; + image: string; + s3CredentialsSecret: Partial<{ + accessKey: string; + secretKey: string; + secretName: string; + }>; + }>; + }>; + viewerCRD?: Partial<{ + image: string; + }>; }; status?: { conditions?: K8sCondition[]; diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 5e2991db93..971f64ba0a 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -62,6 +62,7 @@ export const blankDashboardCR: DashboardConfig = { disableModelMesh: false, disableAcceleratorProfiles: false, disablePipelineExperiments: true, + disableS3Endpoint: true, disableDistributedWorkloads: false, disableModelRegistry: true, }, diff --git a/docs/dashboard-config.md b/docs/dashboard-config.md index a17994c6d7..01324560e3 100644 --- a/docs/dashboard-config.md +++ b/docs/dashboard-config.md @@ -61,7 +61,8 @@ spec: disableKServeMetrics: true disableBiasMetrics: false disablePerformanceMetrics: false - disablePipelineExperiments: false + disablePipelineExperiments: true + disableS3Endpoint: true disableDistributedWorkloads: false ``` @@ -157,6 +158,7 @@ spec: disableBiasMetrics: false disablePerformanceMetrics: false disablePipelineExperiments: true + disableS3Endpoint: true notebookController: enabled: true gpuSetting: autodetect diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index e90471c3f3..cdee3e82ae 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -22,6 +22,7 @@ type MockDashboardConfigType = { disablePerformanceMetrics?: boolean; disableBiasMetrics?: boolean; disablePipelineExperiments?: boolean; + disableS3Endpoint?: boolean; disableDistributedWorkloads?: boolean; disableModelRegistry?: boolean; disableNotebookController?: boolean; @@ -49,6 +50,7 @@ export const mockDashboardConfig = ({ disablePerformanceMetrics = false, disableBiasMetrics = false, disablePipelineExperiments = true, + disableS3Endpoint = true, disableDistributedWorkloads = false, disableModelRegistry = true, disableNotebookController = false, @@ -87,6 +89,7 @@ export const mockDashboardConfig = ({ disableModelMesh, disableAcceleratorProfiles, disablePipelineExperiments, + disableS3Endpoint, disableDistributedWorkloads, disableModelRegistry, }, diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts index e386bf0829..10834bdb4a 100644 --- a/frontend/src/concepts/areas/const.ts +++ b/frontend/src/concepts/areas/const.ts @@ -73,6 +73,10 @@ export const SupportedAreasStateMap: SupportedAreasState = { featureFlags: ['disablePipelineExperiments'], reliantAreas: [SupportedArea.DS_PIPELINES], }, + [SupportedArea.S3_ENDPOINT]: { + featureFlags: ['disableS3Endpoint'], + reliantAreas: [SupportedArea.DS_PIPELINES], + }, [SupportedArea.DISTRIBUTED_WORKLOADS]: { featureFlags: ['disableDistributedWorkloads'], requiredComponents: [StackComponent.KUEUE], diff --git a/frontend/src/concepts/areas/types.ts b/frontend/src/concepts/areas/types.ts index b912330ec7..acb81d32d9 100644 --- a/frontend/src/concepts/areas/types.ts +++ b/frontend/src/concepts/areas/types.ts @@ -24,12 +24,16 @@ export enum SupportedArea { HOME = 'home', /* Standalone areas */ - DS_PIPELINES = 'ds-pipelines', // TODO: Jupyter Tile Support? (outside of feature flags today) WORKBENCHES = 'workbenches', // TODO: Support Applications/Tile area // TODO: Support resources area + /* Pipelines areas */ + DS_PIPELINES = 'ds-pipelines', + PIPELINE_EXPERIMENTS = 'pipeline-experiments', + S3_ENDPOINT = 's3-endpoint', + /* Admin areas */ BYON = 'bring-your-own-notebook', CLUSTER_SETTINGS = 'cluster-settings', @@ -50,7 +54,6 @@ export enum SupportedArea { BIAS_METRICS = 'bias-metrics', PERFORMANCE_METRICS = 'performance-metrics', TRUSTY_AI = 'trusty-ai', - PIPELINE_EXPERIMENTS = 'pipeline-experiments', /* Distributed Workloads areas */ DISTRIBUTED_WORKLOADS = 'distributed-workloads', diff --git a/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx b/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx new file mode 100644 index 0000000000..663bd8fb10 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/ArtifactUriLink.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; +import { extractS3UriComponents } from './utils'; + +interface ArtifactUriLinkProps { + uri?: string; +} + +export const ArtifactUriLink: React.FC = ({ uri }) => { + const { namespace } = usePipelinesAPI(); + const isS3EndpointAvailable = useIsAreaAvailable(SupportedArea.S3_ENDPOINT).status; + + const url = React.useMemo(() => { + if (!uri || !isS3EndpointAvailable) { + return; + } + + // Check if the uri starts with http or https return it as is + if (uri.startsWith('http://') || uri.startsWith('https://')) { + return uri; + } + + // Otherwise check if the uri is s3 + // If it is not s3, return undefined as we only support fetching from s3 buckets + const uriComponents = extractS3UriComponents(uri); + if (!uriComponents) { + return; + } + + const { bucket, path } = uriComponents; + + return `/api/storage/${namespace}/${bucket}?key=${encodeURIComponent(path)}`; + }, [isS3EndpointAvailable, namespace, uri]); + + if (!url) { + return uri; + } + + return ( + + {uri} + + ); +}; diff --git a/frontend/src/concepts/pipelines/content/artifacts/utils.ts b/frontend/src/concepts/pipelines/content/artifacts/utils.ts new file mode 100644 index 0000000000..a8033d607f --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/utils.ts @@ -0,0 +1,13 @@ +export function extractS3UriComponents(uri: string): { bucket: string; path: string } | undefined { + const s3Prefix = 's3://'; + if (!uri.startsWith(s3Prefix)) { + return; + } + + const s3UrlWithoutPrefix = uri.slice(s3Prefix.length); + const firstSlashIndex = s3UrlWithoutPrefix.indexOf('/'); + const bucket = s3UrlWithoutPrefix.substring(0, firstSlashIndex); + const path = s3UrlWithoutPrefix.substring(firstSlashIndex + 1); + + return { bucket, path }; +} diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx index 322a379fc5..9a0069568d 100644 --- a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx @@ -22,6 +22,9 @@ import { import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; import { PipelineRunArtifactSelect } from '~/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect'; import MarkdownView from '~/components/MarkdownView'; +import { fetchStorageObject } from '~/services/storageService'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { extractS3UriComponents } from '~/concepts/pipelines/content/artifacts/utils'; type MarkdownCompareProps = { runArtifacts?: RunArtifact[]; @@ -35,6 +38,7 @@ export type MarkdownAndTitle = { const MarkdownCompare: React.FC = ({ runArtifacts, isLoaded }) => { const [expandedGraph, setExpandedGraph] = React.useState(undefined); + const { namespace } = usePipelinesAPI(); const fullArtifactPaths: FullArtifactPath[] = React.useMemo(() => { if (!runArtifacts) { @@ -56,13 +60,25 @@ const MarkdownCompare: React.FC = ({ runArtifacts, isLoade })) .filter((markdown) => !!markdown.uri) .forEach(async ({ uri, title, run }) => { - const data = uri; // TODO: fetch data from uri: https://issues.redhat.com/browse/RHOAIENG-7206 + const uriComponents = extractS3UriComponents(uri); + if (!uriComponents) { + return; + } + const text = await fetchStorageObject( + namespace, + uriComponents.bucket, + uriComponents.path, + ).catch(() => null); + + if (text === null) { + return; + } runMapBuilder[run.run_id] = run; const config = { title, - config: data, + config: text, }; if (run.run_id in configMapBuilder) { @@ -73,7 +89,7 @@ const MarkdownCompare: React.FC = ({ runArtifacts, isLoade }); return { configMap: configMapBuilder, runMap: runMapBuilder }; - }, [fullArtifactPaths]); + }, [fullArtifactPaths, namespace]); if (!isLoaded) { return ( diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx index 2b50456d9a..37a650948b 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx @@ -19,6 +19,7 @@ import { getArtifactName } from '~/pages/pipelines/global/experiments/artifacts/ import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/PipelinesTableRowTime'; import PipelineRunDrawerRightContent from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; type ArtifactNodeDetailsProps = Pick< React.ComponentProps, @@ -84,7 +85,9 @@ export const ArtifactNodeDetails: React.FC = ({ > {artifactName} - {artifact.uri} + + + diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactVisualization.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactVisualization.tsx index b22888a9c6..04e11ba3fd 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactVisualization.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactVisualization.tsx @@ -23,14 +23,42 @@ import ROCCurve from '~/concepts/pipelines/content/artifacts/charts/ROCCurve'; import ConfusionMatrix from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix'; import { buildConfusionMatrixConfig } from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/utils'; import { isConfusionMatrix } from '~/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils'; +import { fetchStorageObject } from '~/services/storageService'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { extractS3UriComponents } from '~/concepts/pipelines/content/artifacts/utils'; +import MarkdownView from '~/components/MarkdownView'; +import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; interface ArtifactVisualizationProps { artifact: Artifact; } export const ArtifactVisualization: React.FC = ({ artifact }) => { + const [downloadedArtifact, setDownloadedArtifact] = React.useState(null); + const { namespace } = usePipelinesAPI(); + const isS3EndpointAvailable = useIsAreaAvailable(SupportedArea.S3_ENDPOINT).status; + const artifactType = artifact.getType(); + React.useEffect(() => { + if (!isS3EndpointAvailable) { + return; + } + + if (artifactType === ArtifactType.MARKDOWN || artifactType === ArtifactType.HTML) { + const uri = artifact.getUri(); + if (uri) { + const uriComponents = extractS3UriComponents(uri); + if (!uriComponents) { + return; + } + fetchStorageObject(namespace, uriComponents.bucket, uriComponents.path) + .catch(() => null) + .then((text) => setDownloadedArtifact(text)); + } + } + }, [artifact, artifactType, isS3EndpointAvailable, namespace]); + if (artifactType === ArtifactType.CLASSIFICATION_METRICS) { const confusionMatrix = artifact.getCustomPropertiesMap().get('confusionMatrix'); const confidenceMetrics = artifact.getCustomPropertiesMap().get('confidenceMetrics'); @@ -132,13 +160,21 @@ export const ArtifactVisualization: React.FC = ({ ar ); } - if (artifactType === ArtifactType.HTML || artifactType === ArtifactType.MARKDOWN) { + if (downloadedArtifact) { return ( - - - + + Artifact content + + ); } - return null; + return ( + + + + ); }; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 1d9dda9aa0..74ce2770c2 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -1212,6 +1212,7 @@ export type DashboardCommonConfig = { disableAcceleratorProfiles: boolean; // TODO Temp feature flag - remove with https://issues.redhat.com/browse/RHOAIENG-3826 disablePipelineExperiments: boolean; + disableS3Endpoint: boolean; disableDistributedWorkloads: boolean; disableModelRegistry: boolean; }; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx index 7697eaa293..4cc242a507 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx @@ -12,6 +12,7 @@ import { } from '@patternfly/react-core'; import { Artifact } from '~/third_party/mlmd'; +import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; import { ArtifactPropertyDescriptionList } from './ArtifactPropertyDescriptionList'; interface ArtifactOverviewDetailsProps { @@ -32,7 +33,9 @@ export const ArtifactOverviewDetails: React.FC = ( {artifact?.uri && ( <> URI - {artifact.uri} + + + )} diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx index 1e3a65eed7..4285892466 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx @@ -13,6 +13,7 @@ import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import { ArtifactType } from '~/concepts/pipelines/kfTypes'; import { useMlmdListContext, usePipelinesAPI } from '~/concepts/pipelines/context'; import { artifactsDetailsRoute } from '~/routes'; +import { ArtifactUriLink } from '~/concepts/pipelines/content/artifacts/ArtifactUriLink'; import { FilterOptions, columns, initialFilterData, options } from './constants'; import { getArtifactName } from './utils'; @@ -149,7 +150,9 @@ export const ArtifactsTable: React.FC = ({ {artifact.id} {artifact.type} - {artifact.uri} + + + diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx index 9da0401ea1..cf637a20f3 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx @@ -15,6 +15,15 @@ jest.mock('~/redux/selectors', () => ({ useUser: jest.fn(() => ({ isAdmin: true })), })); +jest.mock('~/concepts/areas/useIsAreaAvailable', () => () => ({ + status: true, + featureFlags: {}, + reliantAreas: {}, + requiredComponents: {}, + requiredCapabilities: {}, + customCondition: jest.fn(), +})); + jest.mock('~/concepts/pipelines/context/PipelinesContext', () => ({ usePipelinesAPI: jest.fn(() => ({ pipelinesServer: { diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx index 124ee72372..f9dcba5607 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx @@ -16,6 +16,15 @@ jest.mock('~/redux/selectors', () => ({ useUser: jest.fn(() => ({ isAdmin: true })), })); +jest.mock('~/concepts/areas/useIsAreaAvailable', () => () => ({ + status: true, + featureFlags: {}, + reliantAreas: {}, + requiredComponents: {}, + requiredCapabilities: {}, + customCondition: jest.fn(), +})); + jest.mock('~/concepts/pipelines/context/PipelinesContext', () => ({ usePipelinesAPI: jest.fn(() => ({ pipelinesServer: { diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx index c6ecf5535e..2c0b8f71f3 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx @@ -17,6 +17,7 @@ import ScalarMetricTable from '~/concepts/pipelines/content/compareRuns/metricsS import RocCurveCompare from '~/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveCompare'; import ConfusionMatrixCompare from '~/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare'; import MarkdownCompare from '~/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; export const CompareRunMetricsSection: React.FunctionComponent = () => { const { runs, selectedRuns } = useCompareRuns(); @@ -26,6 +27,7 @@ export const CompareRunMetricsSection: React.FunctionComponent = () => { const [activeTabKey, setActiveTabKey] = React.useState( MetricSectionTabLabels.SCALAR, ); + const isS3EndpointAvailable = useIsAreaAvailable(SupportedArea.S3_ENDPOINT).status; const selectedMlmdPackages = React.useMemo( () => @@ -91,15 +93,16 @@ export const CompareRunMetricsSection: React.FunctionComponent = () => { - {MetricSectionTabLabels.MARKDOWN}} - isDisabled // TODO enable when markdown can be fetched from storage (s3): https://issues.redhat.com/browse/RHOAIENG-7206 - > - - - - + {isS3EndpointAvailable && ( + {MetricSectionTabLabels.MARKDOWN}} + > + + + + + )} ); diff --git a/frontend/src/services/storageService.ts b/frontend/src/services/storageService.ts new file mode 100644 index 0000000000..b8658d82a9 --- /dev/null +++ b/frontend/src/services/storageService.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +export const fetchStorageObject = ( + namespace: string, + bucket: string, + key: string, +): Promise => { + const url = `/api/storage/${namespace}/${bucket}`; + return axios + .get(url, { + params: { + key, + }, + }) + .then((response) => response.data) + .catch((e) => { + throw new Error(e.response.data.message); + }); +}; diff --git a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml index e3cf6c4a65..fb49e77472 100644 --- a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml +++ b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml @@ -69,6 +69,8 @@ spec: type: boolean disablePipelineExperiments: type: boolean + disableS3Endpoint: + type: boolean disableDistributedWorkloads: type: boolean disableModelRegistry: diff --git a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml index db4a69a0e3..3e682d042e 100644 --- a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml +++ b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml @@ -17,6 +17,7 @@ spec: disableProjects: true disablePipelines: true disablePipelineExperiments: true + disableS3Endpoint: true disableModelServing: true disableProjectSharing: true disableCustomServingRuntimes: true