+ );
+};
+
+export default ArtifactPreview;
+
+async function getPreview(
+ storagePath: StoragePath | undefined,
+ namespace: string | undefined,
+ maxbytes: number,
+ maxlines?: number,
+): Promise {
+ if (!storagePath) {
+ return ``;
+ }
+ // 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 || data.split('\n').length < 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...`;
+}
diff --git a/frontend/src/components/DetailsTable.test.tsx b/frontend/src/components/DetailsTable.test.tsx
index 1c1dcc7e132..d01ce26e79d 100644
--- a/frontend/src/components/DetailsTable.test.tsx
+++ b/frontend/src/components/DetailsTable.test.tsx
@@ -62,7 +62,7 @@ describe('DetailsTable', () => {
expect(container).toMatchInlineSnapshot(`
some title
diff --git a/frontend/src/components/DetailsTable.tsx b/frontend/src/components/DetailsTable.tsx
index 499520d698b..a337d31b269 100644
--- a/frontend/src/components/DetailsTable.tsx
+++ b/frontend/src/components/DetailsTable.tsx
@@ -14,15 +14,15 @@
* limitations under the License.
*/
-import * as React from 'react';
-import { stylesheet } from 'typestyle';
-import { color, spacing, commonCss } from '../Css';
-import { KeyValue } from '../lib/StaticGraphParser';
-import Editor from './Editor';
import 'brace';
import 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
+import * as React from 'react';
+import { stylesheet } from 'typestyle';
+import { color, commonCss, spacing } from '../Css';
+import { KeyValue } from '../lib/StaticGraphParser';
+import Editor from './Editor';
export const css = stylesheet({
key: {
@@ -71,7 +71,7 @@ const DetailsTable = (props: DetailsTableProps) => {
const { fields, title, valueComponent: ValueComponent, valueComponentProps } = props;
return (
- {!!title &&
{title}
}
+ {!!title &&
{title}
}
{fields.map((f, i) => {
const [key, value] = f;
diff --git a/frontend/src/components/tabs/ExecutionTitle.test.tsx b/frontend/src/components/tabs/ExecutionTitle.test.tsx
new file mode 100644
index 00000000000..f82c90aabd8
--- /dev/null
+++ b/frontend/src/components/tabs/ExecutionTitle.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Kubeflow Authors
+ *
+ * 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 { Execution, Value } from '@kubeflow/frontend';
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { testBestPractices } from 'src/TestUtils';
+import { CommonTestWrapper } from 'src/TestWrapper';
+import { ExecutionTitle } from './ExecutionTitle';
+
+testBestPractices();
+describe('ExecutionTitle', () => {
+ const execution = new Execution();
+ const executionName = 'fake-execution';
+ const executionId = 123;
+ beforeEach(() => {
+ execution.setId(executionId);
+ execution.getCustomPropertiesMap().set('task_name', new Value().setStringValue(executionName));
+ });
+
+ it('Shows execution name', () => {
+ render(
+
+
+ ,
+ );
+ screen.getByText(executionName, { selector: 'a', exact: false });
+ });
+
+ it('Shows execution description', () => {
+ render(
+
+
+ ,
+ );
+ screen.getByText('This step corresponds to execution');
+ });
+});
diff --git a/frontend/src/components/tabs/ExecutionTitle.tsx b/frontend/src/components/tabs/ExecutionTitle.tsx
new file mode 100644
index 00000000000..e7379e63472
--- /dev/null
+++ b/frontend/src/components/tabs/ExecutionTitle.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Kubeflow Authors
+ *
+ * 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 { Execution } from '@kubeflow/frontend';
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { commonCss } from 'src/Css';
+import { ExecutionHelpers } from 'src/lib/MlmdUtils';
+import { RoutePageFactory } from '../Router';
+
+interface ExecutionTitleProps {
+ execution: Execution;
+}
+
+export function ExecutionTitle({ execution }: ExecutionTitleProps) {
+ return (
+ <>
+
+ This step corresponds to execution{' '}
+
+ "{ExecutionHelpers.getName(execution)}".
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/tabs/InputOutputTab.test.tsx b/frontend/src/components/tabs/InputOutputTab.test.tsx
new file mode 100644
index 00000000000..49997fa2039
--- /dev/null
+++ b/frontend/src/components/tabs/InputOutputTab.test.tsx
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2021 The Kubeflow Authors
+ *
+ * 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 {
+ Api,
+ Artifact,
+ Execution,
+ Value,
+ Event,
+ GetArtifactsByIDResponse,
+ GetEventsByExecutionIDsResponse,
+} from '@kubeflow/frontend';
+import { render, waitFor, screen } from '@testing-library/react';
+import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
+import React from 'react';
+import { Apis } from 'src/lib/Apis';
+import * as mlmdUtils from 'src/lib/MlmdUtils';
+import { testBestPractices } from 'src/TestUtils';
+import { CommonTestWrapper } from 'src/TestWrapper';
+import InputOutputTab from './InputOutputTab';
+
+const executionName = 'fake-execution';
+const artifactId = 100;
+const artifactUri = 'gs://bucket/test';
+const artifactUriView = 'gcs://bucket/test';
+const inputArtifactName = 'input_artifact';
+const outputArtifactName = 'output_artifact';
+const namespace = 'namespace';
+
+testBestPractices();
+describe('InoutOutputTab', () => {
+ it('shows execution title', () => {
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getEventsByExecutionIDs')
+ .mockResolvedValue(new GetEventsByExecutionIDsResponse());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getArtifactsByID')
+ .mockReturnValue(new GetArtifactsByIDResponse());
+
+ render(
+
+
+ ,
+ );
+ screen.getByText(executionName, { selector: 'a', exact: false });
+ });
+
+ it("doesn't show Input/Output artifacts and parameters if no exists", async () => {
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getEventsByExecutionIDs')
+ .mockResolvedValue(new GetEventsByExecutionIDsResponse());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getArtifactsByID')
+ .mockReturnValue(new GetArtifactsByIDResponse());
+
+ render(
+
+
+ ,
+ );
+ await waitFor(() => screen.queryAllByText('Input Parameters').length == 0);
+ await waitFor(() => screen.queryAllByText('Input Artifacts').length == 0);
+ await waitFor(() => screen.queryAllByText('Output Parameters').length == 0);
+ await waitFor(() => screen.queryAllByText('Output Artifacts').length == 0);
+ await waitFor(() => screen.getByText('There is no input/output parameter or artifact.'));
+ });
+
+ it('shows Input parameters with various types', async () => {
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getEventsByExecutionIDs')
+ .mockResolvedValue(new GetEventsByExecutionIDsResponse());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getArtifactsByID')
+ .mockReturnValue(new GetArtifactsByIDResponse());
+
+ const execution = buildBasicExecution();
+ execution
+ .getCustomPropertiesMap()
+ .set('thisKeyIsNotInput', new Value().setStringValue("value shouldn't show"));
+ execution
+ .getCustomPropertiesMap()
+ .set('input:stringkey', new Value().setStringValue('string input'));
+ execution.getCustomPropertiesMap().set('input:intkey', new Value().setIntValue(42));
+ execution.getCustomPropertiesMap().set('input:doublekey', new Value().setDoubleValue(1.99));
+ execution
+ .getCustomPropertiesMap()
+ .set(
+ 'input:structkey',
+ new Value().setStructValue(Struct.fromJavaScript({ struct: { key: 'value', num: 42 } })),
+ );
+ execution
+ .getCustomPropertiesMap()
+ .set(
+ 'input:arraykey',
+ new Value().setStructValue(Struct.fromJavaScript({ list: ['a', 'b', 'c'] })),
+ );
+ render(
+
+
+ ,
+ );
+
+ screen.getByText('stringkey');
+ screen.getByText('string input');
+ screen.getByText('intkey');
+ screen.getByText('42');
+ screen.getByText('doublekey');
+ screen.getByText('1.99');
+ screen.getByText('structkey');
+ screen.getByText('arraykey');
+ expect(screen.queryByText('thisKeyIsNotInput')).toBeNull();
+ });
+
+ it('shows Output parameters with various types', async () => {
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getEventsByExecutionIDs')
+ .mockResolvedValue(new GetEventsByExecutionIDsResponse());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getArtifactsByID')
+ .mockReturnValue(new GetArtifactsByIDResponse());
+
+ const execution = buildBasicExecution();
+ execution
+ .getCustomPropertiesMap()
+ .set('thisKeyIsNotOutput', new Value().setStringValue("value shouldn't show"));
+ execution
+ .getCustomPropertiesMap()
+ .set('output:stringkey', new Value().setStringValue('string output'));
+ execution.getCustomPropertiesMap().set('output:intkey', new Value().setIntValue(42));
+ execution.getCustomPropertiesMap().set('output:doublekey', new Value().setDoubleValue(1.99));
+ execution
+ .getCustomPropertiesMap()
+ .set(
+ 'output:structkey',
+ new Value().setStructValue(Struct.fromJavaScript({ struct: { key: 'value', num: 42 } })),
+ );
+ execution
+ .getCustomPropertiesMap()
+ .set(
+ 'output:arraykey',
+ new Value().setStructValue(Struct.fromJavaScript({ list: ['a', 'b', 'c'] })),
+ );
+ render(
+
+
+ ,
+ );
+
+ screen.getByText('stringkey');
+ screen.getByText('string output');
+ screen.getByText('intkey');
+ screen.getByText('42');
+ screen.getByText('doublekey');
+ screen.getByText('1.99');
+ screen.getByText('structkey');
+ screen.getByText('arraykey');
+ expect(screen.queryByText('thisKeyIsNotOutput')).toBeNull();
+ });
+
+ it('shows Input artifacts', async () => {
+ jest.spyOn(Apis, 'readFile').mockResolvedValue('artifact preview');
+ const getEventResponse = new GetEventsByExecutionIDsResponse();
+ getEventResponse.getEventsList().push(buildInputEvent());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getEventsByExecutionIDs')
+ .mockResolvedValueOnce(getEventResponse);
+ const getArtifactsResponse = new GetArtifactsByIDResponse();
+ getArtifactsResponse.getArtifactsList().push(buildArtifact());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getArtifactsByID')
+ .mockReturnValueOnce(getArtifactsResponse);
+
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => screen.getByText(artifactUriView));
+ await waitFor(() => screen.getByText(inputArtifactName));
+ });
+
+ it('shows Output artifacts', async () => {
+ jest.spyOn(Apis, 'readFile').mockResolvedValue('artifact preview');
+ const getEventResponse = new GetEventsByExecutionIDsResponse();
+ getEventResponse.getEventsList().push(buildOutputEvent());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getEventsByExecutionIDs')
+ .mockResolvedValueOnce(getEventResponse);
+ const getArtifactsResponse = new GetArtifactsByIDResponse();
+ getArtifactsResponse.getArtifactsList().push(buildArtifact());
+ jest
+ .spyOn(Api.getInstance().metadataStoreService, 'getArtifactsByID')
+ .mockReturnValueOnce(getArtifactsResponse);
+
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => screen.getByText(artifactUriView));
+ await waitFor(() => screen.getByText(outputArtifactName));
+ });
+});
+
+function buildBasicExecution() {
+ const execution = new Execution();
+ const executionId = 123;
+
+ execution.setId(executionId);
+ execution.getCustomPropertiesMap().set('task_name', new Value().setStringValue(executionName));
+
+ return execution;
+}
+
+function buildArtifact() {
+ const artifact = new Artifact();
+ artifact.getCustomPropertiesMap();
+ artifact.setUri(artifactUri);
+ artifact.setId(artifactId);
+ return artifact;
+}
+
+function buildInputEvent() {
+ const event = new Event();
+ const path = new Event.Path();
+ path.getStepsList().push(new Event.Path.Step().setKey(inputArtifactName));
+ event
+ .setType(Event.Type.INPUT)
+ .setArtifactId(artifactId)
+ .setPath(path);
+ return event;
+}
+
+function buildOutputEvent() {
+ const event = new Event();
+ const path = new Event.Path();
+ path.getStepsList().push(new Event.Path.Step().setKey(outputArtifactName));
+ event
+ .setType(Event.Type.OUTPUT)
+ .setArtifactId(artifactId)
+ .setPath(path);
+ return event;
+}
diff --git a/frontend/src/components/tabs/InputOutputTab.tsx b/frontend/src/components/tabs/InputOutputTab.tsx
new file mode 100644
index 00000000000..c37f88127d0
--- /dev/null
+++ b/frontend/src/components/tabs/InputOutputTab.tsx
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2021 The Kubeflow Authors
+ *
+ * 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 { Execution, getMetadataValue } from '@kubeflow/frontend';
+import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
+import React from 'react';
+import { useQuery } from 'react-query';
+import { Link } from 'react-router-dom';
+import { ErrorBoundary } from 'src/atoms/ErrorBoundary';
+import { commonCss, padding } from 'src/Css';
+import {
+ filterEventWithInputArtifact,
+ filterEventWithOutputArtifact,
+ getLinkedArtifactsByExecution,
+ LinkedArtifact,
+} from 'src/lib/MlmdUtils';
+import { KeyValue } from 'src/lib/StaticGraphParser';
+import ArtifactPreview from '../ArtifactPreview';
+import Banner from '../Banner';
+import DetailsTable from '../DetailsTable';
+import { RoutePageFactory } from '../Router';
+import { ExecutionTitle } from './ExecutionTitle';
+
+type ParamList = Array>;
+
+export interface IOTabProps {
+ execution: Execution;
+ namespace: string | undefined;
+}
+
+export function InputOutputTab({ execution, namespace }: IOTabProps) {
+ const executionId = execution.getId();
+
+ // Retrieves input and output artifacts from Metadata store.
+ const { isSuccess, error, data } = useQuery(
+ ['execution_artifact', { id: executionId, state: execution.getLastKnownState() }],
+ () => getLinkedArtifactsByExecution(execution),
+ { staleTime: Infinity },
+ );
+
+ // Restructs artifacts and parameters for visualization.
+ const inputParams = extractInputFromExecution(execution);
+ const outputParams = extractOutputFromExecution(execution);
+ let inputArtifacts: ParamList = [];
+ let outputArtifacts: ParamList = [];
+ if (isSuccess && data) {
+ inputArtifacts = getArtifactParamList(filterEventWithInputArtifact(data));
+ outputArtifacts = getArtifactParamList(filterEventWithOutputArtifact(data));
+ }
+
+ let isIoEmpty = false;
+ if (
+ inputParams.length === 0 &&
+ outputParams.length === 0 &&
+ inputArtifacts.length === 0 &&
+ outputArtifacts.length === 0
+ ) {
+ isIoEmpty = true;
+ }
+
+ return (
+
+