- {props.fields.map((f, i) => {
+ {fields.map((f, i) => {
const [key, value] = f;
// only try to parse json if value is a string
@@ -97,18 +103,17 @@ const DetailsTable = (props: DetailsTableProps) => {
// do nothing
}
}
- // If the value isn't a JSON object, just display it as is
+ // If a ValueComponent and a value is provided, render the value with
+ // the ValueComponent. Otherwise render the value as a string (empty string if null or undefined).
return (
{key}
- {(() => {
- const ValueComponent = props.valueComponent;
- if (ValueComponent && !!value && !isString(value)) {
- return ;
- }
- return value;
- })()}
+ {ValueComponent && value ? (
+
+ ) : (
+ `${value || ''}`
+ )}
);
diff --git a/frontend/src/components/MinioArtifactLink.test.tsx b/frontend/src/components/MinioArtifactLink.test.tsx
deleted file mode 100644
index eb2affb869b..00000000000
--- a/frontend/src/components/MinioArtifactLink.test.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * 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
- *
- * https://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.
- */
-
-import React from 'react';
-import MinioArtifactLink from './MinioArtifactLink';
-import { render } from '@testing-library/react';
-
-describe('MinioArtifactLink', () => {
- it('handles undefined artifact', () => {
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
`);
- });
-
- it('handles null artifact', () => {
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
`);
- });
-
- it('handles empty artifact', () => {
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
`);
- });
-
- it('handles invalid artifact: no bucket', () => {
- const s3Artifact = {
- accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
- bucket: '',
- endpoint: 'minio.kubeflow',
- key: 'bar',
- secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
- };
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
`);
- });
-
- it('handles invalid artifact: no key', () => {
- const s3Artifact = {
- accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
- bucket: 'foo',
- endpoint: 'minio.kubeflow',
- key: '',
- secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
- };
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
`);
- });
-
- it('handles s3 artifact', () => {
- const s3Artifact = {
- accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
- bucket: 'foo',
- endpoint: 's3.amazonaws.com',
- key: 'bar',
- secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
- };
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
-
- `);
- });
-
- it('handles minio artifact', () => {
- const minioArtifact = {
- accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
- bucket: 'foo',
- endpoint: 'minio.kubeflow',
- key: 'bar',
- secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
- };
- const { container } = render(
);
- expect(container).toMatchInlineSnapshot(`
-
- `);
- });
-});
diff --git a/frontend/src/components/MinioArtifactLink.tsx b/frontend/src/components/MinioArtifactLink.tsx
deleted file mode 100644
index cd1080c26a4..00000000000
--- a/frontend/src/components/MinioArtifactLink.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as React from 'react';
-import { StoragePath, StorageService } from '../lib/WorkflowParser';
-import { S3Artifact } from '../../third_party/argo-ui/argo_template';
-import { buildQuery } from 'src/lib/Utils';
-
-const artifactApiUri = ({ source, bucket, key }: StoragePath, namespace?: string) =>
- `artifacts/get${buildQuery({ source, bucket, key, namespace })}`;
-
-/**
- * A component that renders an artifact link.
- */
-const MinioArtifactLink: React.FC<{
- artifact: Partial
| null | undefined;
- namespace?: string;
-}> = ({ artifact, namespace }) => {
- if (!artifact || !artifact.key || !artifact.bucket) {
- return null;
- }
-
- const { key, bucket, endpoint } = artifact;
- const source = endpoint === 's3.amazonaws.com' ? StorageService.S3 : StorageService.MINIO;
- const linkText = `${source.toString()}://${bucket}/${key}`;
- // Opens in new window safely
- return (
-
- {linkText}
-
- );
-};
-
-export default MinioArtifactLink;
diff --git a/frontend/src/components/MinioArtifactPreview.test.tsx b/frontend/src/components/MinioArtifactPreview.test.tsx
new file mode 100644
index 00000000000..80f0e50a6c6
--- /dev/null
+++ b/frontend/src/components/MinioArtifactPreview.test.tsx
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2019-2020 Google LLC
+ *
+ * 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
+ *
+ * https://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.
+ */
+import MinioArtifactPreview from './MinioArtifactPreview';
+import React from 'react';
+import TestUtils from '../TestUtils';
+import { act, render } from '@testing-library/react';
+import { Apis } from '../lib/Apis';
+
+describe('MinioArtifactPreview', () => {
+ const readFile = jest.spyOn(Apis, 'readFile');
+
+ beforeEach(() => {
+ readFile.mockResolvedValue('preview ...');
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('handles undefined artifact', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+
+ it('handles null artifact', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+
+ it('handles empty artifact', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+
+ it('handles invalid artifact: no bucket', () => {
+ const s3Artifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: '',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+
+ it('handles invalid artifact: no key', () => {
+ const s3Artifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: '',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(``);
+ });
+
+ it('handles string value', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(`
+
+ teststring
+
+ `);
+ });
+
+ it('handles boolean value', () => {
+ const { container } = render();
+ expect(container).toMatchInlineSnapshot(`
+
+ false
+
+ `);
+ });
+
+ it('handles s3 artifact', async () => {
+ const s3Artifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 's3.amazonaws.com',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+
+ const { container } = render();
+ await act(TestUtils.flushPromises);
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('handles minio artifact', async () => {
+ const minioArtifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const container = document.body.appendChild(document.createElement('div'));
+ await act(async () => {
+ render(, { container });
+ });
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('handles artifact with namespace', async () => {
+ const minioArtifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const { container } = render(
+ ,
+ );
+ await act(TestUtils.flushPromises);
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('handles artifact cleanly even when fetch fails', async () => {
+ const minioArtifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ readFile.mockRejectedValue('unknown error');
+ const { container } = render();
+ await act(TestUtils.flushPromises);
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('handles artifact that previews fully', async () => {
+ const minioArtifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const data = `012\n345\n678\n910`;
+ readFile.mockResolvedValue(data);
+ const { container } = render(
+ ,
+ );
+ await act(TestUtils.flushPromises);
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('handles artifact that previews with maxlines', async () => {
+ const minioArtifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const data = `012\n345\n678\n910`;
+ readFile.mockResolvedValue(data);
+ const { container } = render(
+ ,
+ );
+ await act(TestUtils.flushPromises);
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('handles artifact that previews with maxbytes', async () => {
+ const minioArtifact = {
+ accessKeySecret: { key: 'accesskey', optional: false, name: 'minio' },
+ bucket: 'foo',
+ endpoint: 'minio.kubeflow',
+ key: 'bar',
+ secretKeySecret: { key: 'secretkey', optional: false, name: 'minio' },
+ };
+ const data = `012\n345\n678\n910`;
+ readFile.mockResolvedValue(data);
+ const { container } = render(
+ ,
+ );
+ await act(TestUtils.flushPromises);
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+});
diff --git a/frontend/src/components/MinioArtifactPreview.tsx b/frontend/src/components/MinioArtifactPreview.tsx
new file mode 100644
index 00000000000..9241ffcc7e8
--- /dev/null
+++ b/frontend/src/components/MinioArtifactPreview.tsx
@@ -0,0 +1,128 @@
+import * as React from 'react';
+import { stylesheet } from 'typestyle';
+import { color } from '../Css';
+import { StorageService, StoragePath } from '../lib/WorkflowParser';
+import { S3Artifact } from '../../third_party/argo-ui/argo_template';
+import { isS3Endpoint } from '../lib/AwsHelper';
+import { Apis } from '../lib/Apis';
+import { ExternalLink } from '../atoms/ExternalLink';
+import { ValueComponentProps } from './DetailsTable';
+
+const css = stylesheet({
+ root: {
+ width: '100%',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+ preview: {
+ maxHeight: 250,
+ overflowY: 'auto',
+ padding: 3,
+ backgroundColor: color.lightGrey,
+ },
+});
+
+/**
+ * Check if a javascript object is an argo S3Artifact object.
+ * @param value Any javascript object.
+ */
+export function isS3Artifact(value: any): value is S3Artifact {
+ return value && value.key && value.bucket;
+}
+
+export interface MinioArtifactPreviewProps extends ValueComponentProps> {
+ namespace?: string;
+ maxbytes?: number;
+ maxlines?: number;
+}
+
+function getStoragePath(value?: string | Partial) {
+ if (!value || typeof value === 'string') return;
+ const { key, bucket, endpoint } = value;
+ if (!bucket || !key) return;
+ const source = isS3Endpoint(endpoint) ? StorageService.S3 : StorageService.MINIO;
+ return { source, bucket, key };
+}
+
+async function getPreview(
+ storagePath: StoragePath,
+ namespace: string | undefined,
+ maxbytes: number,
+ maxlines?: number,
+) {
+ // TODO how to handle binary data (can probably use magic number to id common mime types)
+ let data = await Apis.readFile(storagePath, namespace, maxbytes + 1);
+ // is preview === data and no maxlines
+ if (data.length <= maxbytes && !maxlines) {
+ return data;
+ }
+ // remove extra byte at the end (we requested maxbytes +1)
+ data = data.slice(0, maxbytes);
+ // check num lines
+ if (maxlines) {
+ data = data
+ .split('\n')
+ .slice(0, maxlines)
+ .join('\n')
+ .trim();
+ }
+ return `${data}\n...`;
+}
+
+/**
+ * A component that renders a preview to an artifact with a link to the full content.
+ */
+const MinioArtifactPreview: React.FC = ({
+ value,
+ namespace,
+ maxbytes = 255,
+ maxlines,
+}) => {
+ const [content, setContent] = React.useState(undefined);
+ const storagePath = getStoragePath(value);
+
+ React.useEffect(() => {
+ let cancelled = false;
+ if (storagePath) {
+ getPreview(storagePath, namespace, maxbytes, maxlines).then(
+ data => !cancelled && setContent(data),
+ error => console.error(error), // TODO error badge on link?
+ );
+ }
+ return () => {
+ cancelled = true;
+ };
+ }, [storagePath, namespace, maxbytes, maxlines]);
+
+ if (!storagePath) {
+ // if value is undefined, null, or an invalid s3artifact object, don't render
+ if (value === null || value === undefined || typeof value === 'object') return null;
+ // otherwise render value as string (with default string method)
+ return {`${value}`};
+ }
+
+ // TODO need to come to an agreement how to encode artifact info inside a url
+ // namespace is currently not supported
+ const linkText = Apis.buildArtifactUrl(storagePath);
+ const artifactUrl = Apis.buildReadFileUrl(storagePath, namespace, maxbytes);
+
+ // Opens in new window safely
+ // TODO use ArtifactLink instead (but it need to support namespace)
+ return (
+
+
+ {linkText}
+
+ {content && (
+
+ )}
+
+ );
+};
+
+export default MinioArtifactPreview;
diff --git a/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap b/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap
deleted file mode 100644
index 9d154c8fe96..00000000000
--- a/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap
+++ /dev/null
@@ -1,571 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`DetailsTable does not render booleans as JSON 1`] = `
-
-
-
-
- key1
-
-
- true
-
-
-
-
- key2
-
-
- false
-
-
-
-
-`;
-
-exports[`DetailsTable does not render nulls as JSON 1`] = `
-
-
-
-
- key
-
-
- null
-
-
-
-
-`;
-
-exports[`DetailsTable does not render numbers as JSON 1`] = `
-
-
-
-`;
-
-exports[`DetailsTable does not render strings as JSON 1`] = `
-
-
-
-
- key
-
-
- "some string"
-
-
-
-
-`;
-
-exports[`DetailsTable does render arrays as JSON 1`] = `
-
-
-
-`;
-
-exports[`DetailsTable does render arrays as JSON 2`] = `
-
-
-
-`;
-
-exports[`DetailsTable shows a row with a title 1`] = `
-
-
- some title
-
-
-
-
- key
-
-
- value
-
-
-
-
-`;
-
-exports[`DetailsTable shows key and JSON value in row 1`] = `
-
-
-
-`;
-
-exports[`DetailsTable shows key and value for large values 1`] = `
-
-
-
-
- key
-
-
- Lorem Ipsum is simply dummy text of the printing and typesetting
- industry. Lorem Ipsum has been the industry's standard dummy text ever
- since the 1500s, when an unknown printer took a galley of type and
- scrambled it to make a type specimen book. It has survived not only five
- centuries, but also the leap into electronic typesetting, remaining
- essentially unchanged. It was popularised in the 1960s with the release
- of Letraset sheets containing Lorem Ipsum passages, and more recently
- with desktop publishing software like Aldus PageMaker including versions
- of Lorem Ipsum.
-
-
-
-
-`;
-
-exports[`DetailsTable shows key and value in row 1`] = `
-
-
-
-
- key
-
-
- value
-
-
-
-
-`;
-
-exports[`DetailsTable shows keys and values for multiple rows 1`] = `
-
-
-
-
- key1
-
-
- value1
-
-
-
-
- key2
-
-
-
-
-
- key3
-
-
- value3
-
-
-
-
- key4
-
-
- value4
-
-
-
-
- key5
-
-
-
-
-
- key6
-
-
- value6
-
-
-
-
- key6
-
-
- value7
-
-
-
-
-`;
-
-exports[`DetailsTable shows no rows 1`] = `
-
-
-
-`;
-
-exports[`DetailsTable shows one row 1`] = `
-
-
-
-
- key
-
-
- value
-
-
-
-
-`;
diff --git a/frontend/src/lib/Apis.test.ts b/frontend/src/lib/Apis.test.ts
index f074b6d934a..95f7157305e 100644
--- a/frontend/src/lib/Apis.test.ts
+++ b/frontend/src/lib/Apis.test.ts
@@ -135,6 +135,32 @@ describe('Apis', () => {
});
});
+ it('buildReadFileUrl', () => {
+ expect(
+ Apis.buildReadFileUrl(
+ {
+ bucket: 'testbucket',
+ key: 'testkey',
+ source: StorageService.GCS,
+ },
+ 'testnamespace',
+ 255,
+ ),
+ ).toEqual(
+ 'artifacts/get?source=gcs&namespace=testnamespace&peek=255&bucket=testbucket&key=testkey',
+ );
+ });
+
+ it('buildArtifactUrl', () => {
+ expect(
+ Apis.buildArtifactUrl({
+ bucket: 'testbucket',
+ key: 'testkey',
+ source: StorageService.GCS,
+ }),
+ ).toEqual('gcs://testbucket/testkey');
+ });
+
it('getTensorboardApp', async () => {
const spy = fetchSpy(
JSON.stringify({ podAddress: 'http://some/address', tfVersion: '1.14.0' }),
diff --git a/frontend/src/lib/Apis.ts b/frontend/src/lib/Apis.ts
index 1ba8c4cce4e..2675a010e0a 100644
--- a/frontend/src/lib/Apis.ts
+++ b/frontend/src/lib/Apis.ts
@@ -205,9 +205,28 @@ export class Apis {
/**
* Reads file from storage using server.
*/
- public static readFile(path: StoragePath, namespace?: string): Promise {
- const { source, bucket, key } = path;
- return this._fetch(`artifacts/get${buildQuery({ source, bucket, key, namespace })}`);
+ public static readFile(path: StoragePath, namespace?: string, peek?: number): Promise {
+ return this._fetch(this.buildReadFileUrl(path, namespace, peek));
+ }
+
+ /**
+ * Builds an url for the readFile API to retrieve a workflow artifact.
+ * @param props object describing the artifact (e.g. source, bucket, and key)
+ */
+ public static buildReadFileUrl(path: StoragePath, namespace?: string, peek?: number) {
+ const { source, ...rest } = path;
+ return `artifacts/get${buildQuery({ source: `${source}`, namespace, peek, ...rest })}`;
+ }
+
+ /**
+ * Builds an url to visually represents a workflow artifact location.
+ * @param param.source source of the artifact (e.g. minio, gcs, s3, http, or https)
+ * @param param.bucket name of the bucket with the artifact (or host for http/https)
+ * @param param.key key (i.e. path) of the artifact in the bucket
+ */
+ public static buildArtifactUrl({ source, bucket, key }: StoragePath) {
+ // TODO see https://github.com/kubeflow/pipelines/pull/3725
+ return `${source}://${bucket}/${key}`;
}
/**
diff --git a/frontend/src/lib/Utils.test.ts b/frontend/src/lib/Utils.test.ts
index f5396cea15e..5de4a894284 100644
--- a/frontend/src/lib/Utils.test.ts
+++ b/frontend/src/lib/Utils.test.ts
@@ -241,7 +241,7 @@ describe('Utils', () => {
describe('generateMinioArtifactUrl', () => {
it('handles minio:// URIs', () => {
expect(generateMinioArtifactUrl('minio://my-bucket/a/b/c')).toBe(
- 'artifacts/get?source=minio&bucket=my-bucket&key=a/b/c',
+ 'artifacts/get?source=minio&bucket=my-bucket&key=a%2Fb%2Fc',
);
});
diff --git a/frontend/src/lib/Utils.tsx b/frontend/src/lib/Utils.tsx
index 6d20fd071bd..a0ad3f4d2f5 100644
--- a/frontend/src/lib/Utils.tsx
+++ b/frontend/src/lib/Utils.tsx
@@ -292,8 +292,21 @@ export function generateGcsConsoleUri(gcsUri: string): string | undefined {
const MINIO_URI_PREFIX = 'minio://';
-function generateArtifactUrl(source: string, bucket: string, key: string): string {
- return `artifacts/get?source=${source}&bucket=${bucket}&key=${key}`;
+/**
+ * Generates the path component of the url to retrieve an artifact.
+ *
+ * @param source source of the artifact. Can be "minio", "s3", "http", "https", or "gcs".
+ * @param bucket bucket where the artifact is stored, value is assumed to be uri encoded.
+ * @param key path to the artifact, value is assumed to be uri encoded.
+ * @param peek number of characters or bytes to return. If not provided, the entire content of the artifact will be returned.
+ */
+export function generateArtifactUrl(
+ source: string,
+ bucket: string,
+ key: string,
+ peek?: number,
+): string {
+ return `artifacts/get${buildQuery({ source, bucket, key, peek })}`;
}
/**
@@ -302,7 +315,7 @@ function generateArtifactUrl(source: string, bucket: string, key: string): strin
* @param minioUri Minio uri that starts with minio://, like minio://ml-pipeline/path/file
* @returns A URL that leads to the artifact data. Returns undefined when minioUri is not valid.
*/
-export function generateMinioArtifactUrl(minioUri: string): string | undefined {
+export function generateMinioArtifactUrl(minioUri: string, peek?: number): string | undefined {
if (!minioUri.startsWith(MINIO_URI_PREFIX)) {
return undefined;
}
@@ -312,12 +325,12 @@ export function generateMinioArtifactUrl(minioUri: string): string | undefined {
if (matches == null) {
return undefined;
}
- return generateArtifactUrl('minio', matches[1], matches[2]);
+ return generateArtifactUrl('minio', matches[1], matches[2], peek);
}
-export function buildQuery(queriesMap: { [key: string]: string | undefined }): string {
+export function buildQuery(queriesMap: { [key: string]: string | number | undefined }): string {
const queryContent = Object.entries(queriesMap)
- .filter((entry): entry is [string, string] => entry[1] != null)
+ .filter((entry): entry is [string, string | number] => entry[1] != null)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
if (!queryContent) {
diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx
index 3aceca29d17..ff08f2b5a8e 100644
--- a/frontend/src/pages/RunDetails.tsx
+++ b/frontend/src/pages/RunDetails.tsx
@@ -45,12 +45,12 @@ import CompareTable from '../components/CompareTable';
import DetailsTable from '../components/DetailsTable';
import Graph from '../components/Graph';
import LogViewer from '../components/LogViewer';
-import MinioArtifactLink from '../components/MinioArtifactLink';
import PlotCard from '../components/PlotCard';
import { PodEvents, PodInfo } from '../components/PodYaml';
import { RoutePage, RoutePageFactory, RouteParams } from '../components/Router';
import SidePanel from '../components/SidePanel';
import { ToolbarProps } from '../components/Toolbar';
+import MinioArtifactPreview from '../components/MinioArtifactPreview';
import { HTMLViewerConfig } from '../components/viewers/HTMLViewer';
import { PlotType, ViewerConfig } from '../components/viewers/Viewer';
import { componentMap } from '../components/viewers/ViewerContainer';
@@ -361,9 +361,10 @@ class RunDetails extends Page {
(
-
- )}
+ valueComponent={MinioArtifactPreview}
+ valueComponentProps={{
+ namespace: this.state.workflow?.metadata?.namespace,
+ }}
/>
@@ -371,9 +372,10 @@ class RunDetails extends Page {
(
-
- )}
+ valueComponent={MinioArtifactPreview}
+ valueComponentProps={{
+ namespace: this.state.workflow?.metadata?.namespace,
+ }}
/>
)}
diff --git a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap
index 59551b1d2e2..9543229f82e 100644
--- a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap
+++ b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap
@@ -1062,6 +1062,11 @@ exports[`RunDetails opens side panel when graph node is clicked 1`] = `
fields={Array []}
title="Input artifacts"
valueComponent={[Function]}
+ valueComponentProps={
+ Object {
+ "namespace": undefined,
+ }
+ }
/>