feat(frontend): Support Pipeline summary card and pulling version template for V2. Fix #6633 (#6634)

This commit is contained in:
James Liu 2021-09-30 11:20:31 -07:00 committed by GitHub
parent 220d79df66
commit e0f4f7ce99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 821 additions and 225 deletions

View File

@ -0,0 +1,263 @@
{
"pipelineSpec": {
"components": {
"comp-preprocess": {
"executorLabel": "exec-preprocess",
"inputDefinitions": {
"parameters": {
"input_dict_parameter": {
"type": "STRING"
},
"input_list_parameter": {
"type": "STRING"
},
"message": {
"type": "STRING"
}
}
},
"outputDefinitions": {
"artifacts": {
"output_dataset_one": {
"artifactType": {
"schemaTitle": "system.Dataset",
"schemaVersion": "0.0.1"
}
},
"output_dataset_two_path": {
"artifactType": {
"schemaTitle": "system.Dataset",
"schemaVersion": "0.0.1"
}
}
},
"parameters": {
"output_bool_parameter_path": {
"type": "STRING"
},
"output_dict_parameter_path": {
"type": "STRING"
},
"output_list_parameter_path": {
"type": "STRING"
},
"output_parameter_path": {
"type": "STRING"
}
}
}
},
"comp-train": {
"executorLabel": "exec-train",
"inputDefinitions": {
"artifacts": {
"dataset_one_path": {
"artifactType": {
"schemaTitle": "system.Dataset",
"schemaVersion": "0.0.1"
}
},
"dataset_two": {
"artifactType": {
"schemaTitle": "system.Dataset",
"schemaVersion": "0.0.1"
}
}
},
"parameters": {
"input_bool": {
"type": "STRING"
},
"input_dict": {
"type": "STRING"
},
"input_list": {
"type": "STRING"
},
"message": {
"type": "STRING"
},
"num_steps": {
"type": "INT"
}
}
},
"outputDefinitions": {
"artifacts": {
"model": {
"artifactType": {
"schemaTitle": "system.Model",
"schemaVersion": "0.0.1"
}
}
}
}
}
},
"deploymentSpec": {
"executors": {
"exec-preprocess": {
"container": {
"args": [
"--executor_input",
"{{$}}",
"--function_to_execute",
"preprocess"
],
"command": [
"sh",
"-c",
"(python3 -m ensurepip || python3 -m ensurepip --user) && (PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location 'kfp==1.8.0' || PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location 'kfp==1.8.0' --user) && \"$0\" \"$@\"",
"sh",
"-ec",
"program_path=$(mktemp -d)\nprintf \"%s\" \"$0\" > \"$program_path/ephemeral_component.py\"\npython3 -m kfp.v2.components.executor_main --component_module_path \"$program_path/ephemeral_component.py\" \"$@\"\n",
"\nfrom kfp.v2.dsl import *\nfrom typing import *\n\ndef preprocess(\n # An input parameter of type string.\n message: str,\n # An input parameter of type dict.\n input_dict_parameter: Dict[str, int],\n # An input parameter of type list.\n input_list_parameter: List[str],\n # Use Output[T] to get a metadata-rich handle to the output artifact\n # of type `Dataset`.\n output_dataset_one: Output[Dataset],\n # A locally accessible filepath for another output artifact of type\n # `Dataset`.\n output_dataset_two_path: OutputPath('Dataset'),\n # A locally accessible filepath for an output parameter of type string.\n output_parameter_path: OutputPath(str),\n # A locally accessible filepath for an output parameter of type bool.\n output_bool_parameter_path: OutputPath(bool),\n # A locally accessible filepath for an output parameter of type dict.\n output_dict_parameter_path: OutputPath(Dict[str, int]),\n # A locally accessible filepath for an output parameter of type list.\n output_list_parameter_path: OutputPath(List[str]),\n):\n \"\"\"Dummy preprocessing step.\"\"\"\n\n # Use Dataset.path to access a local file path for writing.\n # One can also use Dataset.uri to access the actual URI file path.\n with open(output_dataset_one.path, 'w') as f:\n f.write(message)\n\n # OutputPath is used to just pass the local file path of the output artifact\n # to the function.\n with open(output_dataset_two_path, 'w') as f:\n f.write(message)\n\n with open(output_parameter_path, 'w') as f:\n f.write(message)\n\n with open(output_bool_parameter_path, 'w') as f:\n f.write(\n str(True)) # use either `str()` or `json.dumps()` for bool values.\n\n import json\n with open(output_dict_parameter_path, 'w') as f:\n f.write(json.dumps(input_dict_parameter))\n\n with open(output_list_parameter_path, 'w') as f:\n f.write(json.dumps(input_list_parameter))\n\n"
],
"image": "python:3.7"
}
},
"exec-train": {
"container": {
"args": [
"--executor_input",
"{{$}}",
"--function_to_execute",
"train"
],
"command": [
"sh",
"-c",
"(python3 -m ensurepip || python3 -m ensurepip --user) && (PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location 'kfp==1.8.0' || PIP_DISABLE_PIP_VERSION_CHECK=1 python3 -m pip install --quiet --no-warn-script-location 'kfp==1.8.0' --user) && \"$0\" \"$@\"",
"sh",
"-ec",
"program_path=$(mktemp -d)\nprintf \"%s\" \"$0\" > \"$program_path/ephemeral_component.py\"\npython3 -m kfp.v2.components.executor_main --component_module_path \"$program_path/ephemeral_component.py\" \"$@\"\n",
"\nfrom kfp.v2.dsl import *\nfrom typing import *\n\ndef train(\n # Use InputPath to get a locally accessible path for the input artifact\n # of type `Dataset`.\n dataset_one_path: InputPath('Dataset'),\n # Use Input[T] to get a metadata-rich handle to the input artifact\n # of type `Dataset`.\n dataset_two: Input[Dataset],\n # An input parameter of type string.\n message: str,\n # Use Output[T] to get a metadata-rich handle to the output artifact\n # of type `Dataset`.\n model: Output[Model],\n # An input parameter of type bool.\n input_bool: bool,\n # An input parameter of type dict.\n input_dict: Dict[str, int],\n # An input parameter of type List[str].\n input_list: List[str],\n # An input parameter of type int with a default value.\n num_steps: int = 100,\n):\n \"\"\"Dummy Training step.\"\"\"\n with open(dataset_one_path, 'r') as input_file:\n dataset_one_contents = input_file.read()\n\n with open(dataset_two.path, 'r') as input_file:\n dataset_two_contents = input_file.read()\n\n line = (f'dataset_one_contents: {dataset_one_contents} || '\n f'dataset_two_contents: {dataset_two_contents} || '\n f'message: {message} || '\n f'input_bool: {input_bool}, type {type(input_bool)} || '\n f'input_dict: {input_dict}, type {type(input_dict)} || '\n f'input_list: {input_list}, type {type(input_list)} \\n')\n\n with open(model.path, 'w') as output_file:\n for i in range(num_steps):\n output_file.write('Step {}\\n{}\\n=====\\n'.format(i, line))\n\n # model is an instance of Model artifact, which has a .metadata dictionary\n # to store arbitrary metadata for the output artifact.\n model.metadata['accuracy'] = 0.9\n\n"
],
"image": "python:3.7"
}
}
}
},
"pipelineInfo": {
"name": "my-test-pipeline-beta"
},
"root": {
"dag": {
"tasks": {
"preprocess": {
"cachingOptions": {
"enableCache": true
},
"componentRef": {
"name": "comp-preprocess"
},
"inputs": {
"parameters": {
"input_dict_parameter": {
"componentInputParameter": "input_dict"
},
"input_list_parameter": {
"runtimeValue": {
"constantValue": {
"stringValue": "[\"a\", \"b\", \"c\"]"
}
}
},
"message": {
"componentInputParameter": "message"
}
}
},
"taskInfo": {
"name": "preprocess"
}
},
"train": {
"cachingOptions": {
"enableCache": true
},
"componentRef": {
"name": "comp-train"
},
"dependentTasks": [
"preprocess"
],
"inputs": {
"artifacts": {
"dataset_one_path": {
"taskOutputArtifact": {
"outputArtifactKey": "output_dataset_one",
"producerTask": "preprocess"
}
},
"dataset_two": {
"taskOutputArtifact": {
"outputArtifactKey": "output_dataset_two_path",
"producerTask": "preprocess"
}
}
},
"parameters": {
"input_bool": {
"taskOutputParameter": {
"outputParameterKey": "output_bool_parameter_path",
"producerTask": "preprocess"
}
},
"input_dict": {
"taskOutputParameter": {
"outputParameterKey": "output_dict_parameter_path",
"producerTask": "preprocess"
}
},
"input_list": {
"taskOutputParameter": {
"outputParameterKey": "output_list_parameter_path",
"producerTask": "preprocess"
}
},
"message": {
"taskOutputParameter": {
"outputParameterKey": "output_parameter_path",
"producerTask": "preprocess"
}
},
"num_steps": {
"runtimeValue": {
"constantValue": {
"intValue": "100"
}
}
}
}
},
"taskInfo": {
"name": "train"
}
}
}
},
"inputDefinitions": {
"parameters": {
"input_dict": {
"type": "STRING"
},
"message": {
"type": "STRING"
}
}
}
},
"schemaVersion": "2.0.0",
"sdkVersion": "kfp-1.8.0"
},
"runtimeConfig": {
"gcsOutputDirectory": "dummy_root",
"parameters": {
"input_dict": {
"stringValue": "{\"A\": 1, \"B\": 2}"
}
}
}
}

View File

@ -42,7 +42,8 @@ const PIPELINE_ID_V2_PYTHON_TWO_STEPS = '8fbe3bd6-a01f-11e8-98d0-529269fb1460';
const PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT: ApiPipelineVersion = {
created_at: new Date('2021-11-24T20:58:23.000Z'),
id: PIPELINE_ID_V2_PYTHON_TWO_STEPS,
name: 'v2_lightweight_python_functions_pipeline',
name: 'default version',
description: 'This is default version description.',
parameters: [
{
name: 'message',
@ -50,11 +51,27 @@ const PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT: ApiPipelineVersion = {
],
};
const PIPELINE_V2_PYTHON_TWO_STEPS: ApiPipeline = {
description: 'V2 two steps: preprocess and training.',
...PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT,
description: 'This is pipeline level description.',
name: 'v2_lightweight_python_functions_pipeline',
default_version: PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT,
};
const PIPELINE_ID_V2_PYTHON_TWO_STEPS_REV = '9fbe3bd6-a01f-11e8-98d0-529269fb1460';
const PIPELINE_V2_PYTHON_TWO_STEPS_REV: ApiPipelineVersion = {
created_at: new Date('2021-12-24T20:58:23.000Z'),
id: PIPELINE_ID_V2_PYTHON_TWO_STEPS_REV,
name: 'revision',
code_source_url:
'https://github.com/kubeflow/pipelines/blob/master/sdk/python/kfp/v2/compiler_cli_tests/test_data/lightweight_python_functions_v2_pipeline.py',
description: 'This is version description.',
parameters: [
{
name: 'revision-message',
},
],
};
const PIPELINE_ID_V2_LOOPS_CONDITIONS = '8fbe3bd6-a01f-11e8-98d0-529269fb1461';
const PIPELINE_V2_LOOPS_CONDITIONS_DEFAULT: ApiPipelineVersion = {
created_at: new Date('2021-04-13T20:58:23.000Z'),
@ -942,9 +959,28 @@ export const v2PipelineSpecMap: Map<string, string> = new Map([
PIPELINE_ID_V2_PYTHON_TWO_STEPS,
'./mock-backend/data/v2/pipeline/mock_lightweight_python_functions_v2_pipeline.json',
],
[
PIPELINE_ID_V2_PYTHON_TWO_STEPS_REV,
'./mock-backend/data/v2/pipeline/mock_lightweight_python_functions_v2_pipeline_rev.json',
],
[
PIPELINE_ID_V2_LOOPS_CONDITIONS,
'./mock-backend/data/v2/pipeline/pipeline_with_loops_and_conditions.json',
],
[PIPELINE_ID_V2_XGBOOST, './mock-backend/data/v2/pipeline/xgboost_sample_pipeline.json'],
]);
// Kubeflow versions
export const V2_TWO_STEPS_VERSION_LIST: ApiPipelineVersion[] = [
PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT,
PIPELINE_V2_PYTHON_TWO_STEPS_REV,
];
export const PIPELINE_VERSIONS_LIST_MAP: Map<string, ApiPipelineVersion[]> = new Map([
[PIPELINE_ID_V2_PYTHON_TWO_STEPS, V2_TWO_STEPS_VERSION_LIST],
]);
export const PIPELINE_VERSIONS_LIST_FULL: ApiPipelineVersion[] = [
...pipelines,
PIPELINE_V2_PYTHON_TWO_STEPS_REV,
];

View File

@ -12,25 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import * as _path from 'path';
import * as express from 'express';
import { Response } from 'express-serve-static-core';
import * as fs from 'fs';
import RunUtils from '../src/lib/RunUtils';
import helloWorldRuntime from './integration-test-runtime';
import proxyMiddleware from './proxy-middleware';
import * as _path from 'path';
import { ApiExperiment, ApiListExperimentsResponse } from '../src/apis/experiment';
import { ApiFilter, PredicateOp } from '../src/apis/filter';
import { ApiListExperimentsResponse, ApiExperiment } from '../src/apis/experiment';
import { ApiListJobsResponse, ApiJob } from '../src/apis/job';
import { ApiJob, ApiListJobsResponse } from '../src/apis/job';
import {
ApiListPipelinesResponse,
ApiPipeline,
ApiListPipelineVersionsResponse,
ApiPipeline,
ApiPipelineVersion,
} from '../src/apis/pipeline';
import { ApiListRunsResponse, ApiResourceType, ApiRun, ApiRunStorageState } from '../src/apis/run';
import { ExperimentSortKeys, PipelineSortKeys, RunSortKeys } from '../src/lib/Apis';
import { Response } from 'express-serve-static-core';
import { data as fixedData, namedPipelines, v2PipelineSpecMap } from './fixed-data';
import RunUtils from '../src/lib/RunUtils';
import {
data as fixedData,
namedPipelines,
PIPELINE_VERSIONS_LIST_FULL,
PIPELINE_VERSIONS_LIST_MAP,
v2PipelineSpecMap,
} from './fixed-data';
import helloWorldRuntime from './integration-test-runtime';
import proxyMiddleware from './proxy-middleware';
const rocMetadataJsonPath = './eval-output/metadata.json';
const rocMetadataJsonPath2 = './eval-output/metadata2.json';
@ -468,26 +474,6 @@ export default (app: express.Application) => {
res.json(response);
});
app.get(v1beta1Prefix + '/pipeline_versions', (req, res) => {
res.header('Content-Type', 'application/json');
const response: ApiListPipelineVersionsResponse = {
next_page_token: '',
versions: [],
};
let versions: ApiPipelineVersion[] = fixedData.versions;
const start = req.query.page_token ? +req.query.page_token : 0;
const end = start + (+req.query.page_size || 20);
response.versions = versions.slice(start, end);
if (end < versions.length) {
response.next_page_token = end + '';
}
res.json(response);
});
app.delete(v1beta1Prefix + '/pipelines/:pid', (req, res) => {
res.header('Content-Type', 'application/json');
const i = fixedData.pipelines.findIndex(p => p.id === req.params.pid);
@ -543,6 +529,16 @@ export default (app: express.Application) => {
app.get(v1beta1Prefix + '/pipeline_versions/:pid/templates', (req, res) => {
res.header('Content-Type', 'text/x-yaml');
// Find v2 pipeline template
const templatePath = v2PipelineSpecMap.get(req.params.pid);
if (templatePath != null) {
console.log(templatePath);
res.send(JSON.stringify({ template: fs.readFileSync(templatePath, 'utf-8') }));
return;
}
// Default and v1 version list. Return mock template consistently.
const version = fixedData.versions.find(p => p.id === req.params.pid);
if (!version) {
res.status(404).send(`No pipeline was found with ID: ${req.params.pid}`);
@ -555,7 +551,7 @@ export default (app: express.Application) => {
app.get(v1beta1Prefix + '/pipeline_versions/:pid', (req, res) => {
res.header('Content-Type', 'application/json');
const pipeline = fixedData.versions.find(p => p.id === req.params.pid);
const pipeline = PIPELINE_VERSIONS_LIST_FULL.find(p => p.id === req.params.pid);
if (!pipeline) {
res.status(404).send(`No pipeline was found with ID: ${req.params.pid}`);
return;
@ -576,16 +572,40 @@ export default (app: express.Application) => {
req.query['resource_key.type'] === 'PIPELINE' &&
req.query.page_size > 0
) {
const pipeline = fixedData.pipelines.find(p => p.id === req.query['resource_key.id']);
if (pipeline == null) {
return;
}
const pipeline_versions_list_response: ApiListPipelinesResponse = {
total_size: 1,
pipelines: [pipeline],
const response: ApiListPipelineVersionsResponse = {
next_page_token: '',
versions: [],
};
res.send(JSON.stringify(pipeline_versions_list_response));
let versions: ApiPipelineVersion[] =
PIPELINE_VERSIONS_LIST_MAP.get(req.query['resource_key.id']) || [];
if (versions.length === 0) {
const pipeline = fixedData.pipelines.find(p => p.id === req.query['resource_key.id']);
if (pipeline == null || !pipeline.default_version) {
return;
}
// Default version list is pipeline with single default version.
const pipeline_versions_list_response: ApiListPipelineVersionsResponse = {
total_size: 1,
versions: [pipeline.default_version],
};
res.json(pipeline_versions_list_response);
}
const start = req.query.page_token ? +req.query.page_token : 0;
const end = start + (+req.query.page_size || 20);
response.versions = versions.slice(start, end);
if (end < versions.length) {
response.next_page_token = end + '';
}
res.json(response);
return;
}
return;
});

View File

@ -8153,9 +8153,9 @@
}
},
"@testing-library/dom": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.6.0.tgz",
"integrity": "sha512-EDBMEWK8IVpNF7B7C1knb0lLB4Si9RWte/YTEi6CqmqUK5CYCoecwOOG9pEijU/H6s3u0drUxH5sKT07FCgFIg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.7.0.tgz",
"integrity": "sha512-8oOfBG51v8aN9D8eehwzgnEETf9Lxv/3dZyPZuar1JAp9OK0I9d7Y2MR6TEQyj/E/iN1kCIeYaCI445s5C9RDg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
@ -8211,9 +8211,9 @@
}
},
"@jest/types": {
"version": "27.1.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.1.1.tgz",
"integrity": "sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA==",
"version": "27.2.4",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.4.tgz",
"integrity": "sha512-IDO2ezTxeMvQAHxzG/ZvEyA47q0aVfzT95rGFl7bZs/Go0aIucvfDbS2rmnoEdXxlLQhcolmoG/wvL/uKx4tKA==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
@ -8241,6 +8241,12 @@
"@types/yargs-parser": "*"
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -8293,13 +8299,13 @@
"dev": true
},
"pretty-format": {
"version": "27.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz",
"integrity": "sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==",
"version": "27.2.4",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.4.tgz",
"integrity": "sha512-NUjw22WJHldzxyps2YjLZkUj6q1HvjqFezkB9Y2cklN8NtVZN/kZEXGZdFw4uny3oENzV5EEMESrkI0YDUH8vg==",
"dev": true,
"requires": {
"@jest/types": "^27.1.1",
"ansi-regex": "^5.0.0",
"@jest/types": "^27.2.4",
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
@ -11643,12 +11649,6 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@ -16769,16 +16769,6 @@
"vary": "~1.1.2"
},
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dev": true,
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -16794,33 +16784,6 @@
"ms": "2.0.0"
}
},
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"dev": true
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"dev": true,
"requires": {
"mime-db": "1.43.0"
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"dev": true
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -16838,12 +16801,6 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@ -17219,18 +17176,6 @@
"requires": {
"ms": "2.0.0"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@ -17429,9 +17374,9 @@
"dev": true
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true
},
"fragment-cache": {
@ -27393,12 +27338,12 @@
}
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dev": true,
"requires": {
"forwarded": "~0.1.2",
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
}
},
@ -27646,12 +27591,6 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@ -29480,6 +29419,13 @@
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}
}
},
"request-promise-core": {
@ -30115,12 +30061,6 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@ -30206,14 +30146,6 @@
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
},
"dependencies": {
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true
}
}
},
"set-blocking": {
@ -31969,31 +31901,30 @@
}
},
"tailwindcss": {
"version": "npm:@tailwindcss/postcss7-compat@2.2.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.14.tgz",
"integrity": "sha512-vtzYILqywIY1GWELwHVF7goPhaJpm/1P5kJZ0Kx8lNlarALTFEWwIWCM6MxQ7pXzDWa1eUeozVJVeqfOBXKwXg==",
"version": "npm:@tailwindcss/postcss7-compat@2.2.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.7.tgz",
"integrity": "sha512-1QkWUEeLV1AoNipMCE6IlL7XYScGb+DAzaXy35ooMDvl0G8kCMHBNqGxyVAnTcK8gyJNUzkKXExkUnbjAndd/g==",
"dev": true,
"requires": {
"arg": "^5.0.1",
"arg": "^5.0.0",
"autoprefixer": "^9",
"bytes": "^3.0.0",
"chalk": "^4.1.2",
"chalk": "^4.1.1",
"chokidar": "^3.5.2",
"color": "^4.0.1",
"cosmiconfig": "^7.0.1",
"color": "^3.2.0",
"cosmiconfig": "^7.0.0",
"detective": "^5.2.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.7",
"fs-extra": "^10.0.0",
"glob-parent": "^6.0.1",
"glob-parent": "^6.0.0",
"html-tags": "^3.1.0",
"is-color-stop": "^1.1.0",
"is-glob": "^4.0.1",
"lodash": "^4.17.21",
"lodash.topath": "^4.5.2",
"modern-normalize": "^1.1.0",
"node-emoji": "^1.11.0",
"node-emoji": "^1.8.1",
"normalize-path": "^3.0.0",
"object-hash": "^2.2.0",
"postcss": "^7",
@ -32046,13 +31977,30 @@
}
},
"color": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz",
"integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dev": true,
"requires": {
"color-convert": "^2.0.1",
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
},
"dependencies": {
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
}
}
},
"color-convert": {
@ -32080,19 +32028,6 @@
"simple-swizzle": "^0.2.2"
}
},
"cosmiconfig": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
"integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
"dev": true,
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
}
},
"fast-glob": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
@ -32325,6 +32260,12 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
"dev": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
@ -33052,18 +32993,18 @@
},
"dependencies": {
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
"integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==",
"dev": true
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"version": "2.1.32",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
"integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
"dev": true,
"requires": {
"mime-db": "1.43.0"
"mime-db": "1.49.0"
}
}
}
@ -33597,11 +33538,6 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"dev": true
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"uuid-browser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz",
@ -34618,6 +34554,12 @@
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
"integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
"dev": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},

View File

@ -0,0 +1,142 @@
/*
* 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 { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ApiPipeline, ApiPipelineVersion } from 'src/apis/pipeline/api';
import { testBestPractices } from 'src/TestUtils';
import { PipelineVersionCard } from './PipelineVersionCard';
const DEFAULT_VERSION_NAME = 'default version';
const REVISION_NAME = 'revision';
const PIPELINE_ID_V2_PYTHON_TWO_STEPS = '8fbe3bd6-a01f-11e8-98d0-529269fb1460';
const PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT: ApiPipelineVersion = {
created_at: new Date('2021-11-24T20:58:23.000Z'),
id: PIPELINE_ID_V2_PYTHON_TWO_STEPS,
name: DEFAULT_VERSION_NAME,
description: 'This is default version description.',
parameters: [
{
name: 'message',
},
],
};
const PIPELINE_ID_V2_PYTHON_TWO_STEPS_REV = '9fbe3bd6-a01f-11e8-98d0-529269fb1460';
const PIPELINE_V2_PYTHON_TWO_STEPS_REV: ApiPipelineVersion = {
created_at: new Date('2021-12-24T20:58:23.000Z'),
id: PIPELINE_ID_V2_PYTHON_TWO_STEPS_REV,
name: REVISION_NAME,
description: 'This is version description.',
parameters: [
{
name: 'revision-message',
},
],
};
const V2_TWO_STEPS_VERSION_LIST: ApiPipelineVersion[] = [
PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT,
PIPELINE_V2_PYTHON_TWO_STEPS_REV,
];
const PIPELINE_V2_PYTHON_TWO_STEPS: ApiPipeline = {
...PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT,
description: 'This is pipeline level description.',
name: 'v2_lightweight_python_functions_pipeline',
default_version: PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT,
};
testBestPractices();
describe('PipelineVersionCard', () => {
it('makes Show Summary button visible by default', async () => {
render(
<PipelineVersionCard
apiPipeline={PIPELINE_V2_PYTHON_TWO_STEPS}
selectedVersion={PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT}
versions={V2_TWO_STEPS_VERSION_LIST}
handleVersionSelected={versionId => {
return Promise.resolve();
}}
></PipelineVersionCard>,
);
screen.getByText('Show Summary');
expect(screen.queryByText('Hide')).toBeNull();
});
it('clicks to open and hide Summary', async () => {
render(
<PipelineVersionCard
apiPipeline={PIPELINE_V2_PYTHON_TWO_STEPS}
selectedVersion={PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT}
versions={V2_TWO_STEPS_VERSION_LIST}
handleVersionSelected={versionId => {
return Promise.resolve();
}}
></PipelineVersionCard>,
);
userEvent.click(screen.getByText('Show Summary'));
expect(screen.queryByText('Show Summary')).toBeNull();
userEvent.click(screen.getByText('Hide'));
screen.getByText('Show Summary');
});
it('shows Summary and checks detail', async () => {
render(
<PipelineVersionCard
apiPipeline={PIPELINE_V2_PYTHON_TWO_STEPS}
selectedVersion={PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT}
versions={V2_TWO_STEPS_VERSION_LIST}
handleVersionSelected={versionId => {
return Promise.resolve();
}}
></PipelineVersionCard>,
);
userEvent.click(screen.getByText('Show Summary'));
screen.getByText('Pipeline ID');
screen.getByText(PIPELINE_ID_V2_PYTHON_TWO_STEPS);
screen.getByText('Version');
screen.getByText(DEFAULT_VERSION_NAME);
screen.getByText('Version source');
screen.getByText('Uploaded on');
screen.getByText('Pipeline Description');
screen.getByText('This is pipeline level description.');
screen.getByText('Default Version Description');
screen.getByText('This is default version description.');
});
it('shows version list', async () => {
const { getByRole } = render(
<PipelineVersionCard
apiPipeline={PIPELINE_V2_PYTHON_TWO_STEPS}
selectedVersion={PIPELINE_V2_PYTHON_TWO_STEPS_DEFAULT}
versions={V2_TWO_STEPS_VERSION_LIST}
handleVersionSelected={versionId => {
return Promise.resolve();
}}
></PipelineVersionCard>,
);
userEvent.click(screen.getByText('Show Summary'));
fireEvent.click(getByRole('button', { name: DEFAULT_VERSION_NAME }));
fireEvent.click(getByRole('listbox'));
getByRole('option', { name: REVISION_NAME });
});
});

View File

@ -0,0 +1,123 @@
/*
* 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 Button from '@material-ui/core/Button';
import FormControl from '@material-ui/core/FormControl';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import Paper from '@material-ui/core/Paper';
import Select from '@material-ui/core/Select';
import React, { useState } from 'react';
import { ApiPipeline, ApiPipelineVersion } from 'src/apis/pipeline';
import { Description } from 'src/components/Description';
import { commonCss } from 'src/Css';
import { formatDateString } from 'src/lib/Utils';
interface PipelineVersionCardProps {
apiPipeline: ApiPipeline | null;
selectedVersion: ApiPipelineVersion | undefined;
versions: ApiPipelineVersion[];
handleVersionSelected: (versionId: string) => Promise<void>;
}
export function PipelineVersionCard({
apiPipeline,
selectedVersion,
versions,
handleVersionSelected,
}: PipelineVersionCardProps) {
const [summaryShown, setSummaryShown] = useState(false);
const createVersionUrl = () => {
return selectedVersion?.code_source_url;
};
return (
<>
{!!apiPipeline && summaryShown && (
<Paper className='absolute bottom-3 left-20 p-5 w-136 z-20'>
<div className='items-baseline flex justify-between'>
<div className={commonCss.header}>Static Pipeline Summary</div>
<Button onClick={() => setSummaryShown(false)} color='secondary'>
Hide
</Button>
</div>
<div className='text-gray-900 mt-5'>Pipeline ID</div>
<div>{apiPipeline.id || 'Unable to obtain Pipeline ID'}</div>
{versions.length > 0 && (
<>
<div className='text-gray-900 mt-5'>
<form autoComplete='off'>
<FormControl>
<InputLabel>Version</InputLabel>
<Select
aria-label='version_selector'
data-testid='version_selector'
value={
selectedVersion ? selectedVersion.id : apiPipeline.default_version!.id!
}
onChange={event => handleVersionSelected(event.target.value)}
inputProps={{ id: 'version-selector', name: 'selectedVersion' }}
>
{versions.map((v, _) => (
<MenuItem key={v.id} value={v.id}>
{v.name}
</MenuItem>
))}
</Select>
</FormControl>
</form>
</div>
<div className='text-blue-500 mt-5'>
<a href={createVersionUrl()} target='_blank' rel='noopener noreferrer'>
Version source
</a>
</div>
</>
)}
<div className='text-gray-900 mt-5'>Uploaded on</div>
<div>
{selectedVersion
? formatDateString(selectedVersion.created_at)
: formatDateString(apiPipeline.created_at)}
</div>
<div className='text-gray-900 mt-5'>Pipeline Description</div>
<Description description={apiPipeline.description || 'empty pipeline description'} />
{/* selectedVersion is always populated by either selected or pipeline default version if it exists */}
{selectedVersion && selectedVersion.description ? (
<>
<div className='text-gray-900 mt-5'>
{selectedVersion.id === apiPipeline.default_version?.id
? 'Default Version Description'
: 'Version Description'}
</div>
<Description description={selectedVersion.description} />
</>
) : null}
</Paper>
)}
{!summaryShown && (
<div className='flex absolute bottom-5 left-10 pb-5 pl-10 bg-transparent z-20'>
<Button onClick={() => setSummaryShown(!summaryShown)} color='secondary'>
Show Summary
</Button>
</div>
)}
</>
);
}

View File

@ -156,17 +156,21 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
templateString={templateString}
pipelineFlowElements={graphV2!}
setSubDagLayers={setLayers}
apiPipeline={pipeline}
selectedVersion={selectedVersion}
versions={versions}
handleVersionSelected={this.handleVersionSelected.bind(this)}
/>
)}
{!showV2Pipeline && (
<PipelineDetailsV1
pipeline={pipeline}
selectedVersion={selectedVersion}
versions={versions}
templateString={templateString}
graph={graph}
reducedGraph={reducedGraph}
updateBanner={this.props.updateBanner}
selectedVersion={selectedVersion}
versions={versions}
handleVersionSelected={this.handleVersionSelected.bind(this)}
/>
)}
@ -319,32 +323,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
this.props.updateToolbar({ breadcrumbs, actions: toolbarActions, pageTitle });
let graph: graphlib.Graph | null = null;
let reducedGraph: graphlib.Graph | null | undefined = null;
let graphV2: PipelineFlowElement[] = [];
if (templateString) {
try {
const template = JsYaml.safeLoad(templateString);
if (WorkflowUtils.isArgoWorkflowTemplate(template)) {
graph = StaticGraphParser.createGraph(template!);
reducedGraph = graph ? transitiveReduction(graph) : undefined;
if (graph && reducedGraph && compareGraphEdges(graph, reducedGraph)) {
reducedGraph = undefined; // disable reduction switch
}
} else if (isFeatureEnabled(FeatureKey.V2)) {
const pipelineSpec = WorkflowUtils.convertJsonToV2PipelineSpec(templateString);
graphV2 = convertFlowElements(pipelineSpec);
} else {
throw new Error(
'Unable to convert string response from server to Argo workflow template' +
': https://argoproj.github.io/argo-workflows/workflow-templates/',
);
}
} catch (err) {
await this.showPageError('Error: failed to generate Pipeline graph.', err);
}
}
const [graph, reducedGraph, graphV2] = await this._createGraph(templateString);
if (isFeatureEnabled(FeatureKey.V2) && graphV2.length > 0) {
this.setStateSafe({
@ -379,17 +358,27 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
this.props.history.replace({
pathname: `/pipelines/details/${this.state.pipeline.id}/version/${versionId}`,
});
const graph = await this._createGraph(selectedVersionPipelineTemplate);
let reducedGraph = graph ? transitiveReduction(graph) : undefined;
if (graph && reducedGraph && compareGraphEdges(graph, reducedGraph)) {
reducedGraph = undefined; // disable reduction switch
const [graph, reducedGraph, graphV2] = await this._createGraph(
selectedVersionPipelineTemplate,
);
if (isFeatureEnabled(FeatureKey.V2) && graphV2.length > 0) {
this.setStateSafe({
graph: undefined,
reducedGraph: undefined,
graphV2,
selectedVersion,
templateString: selectedVersionPipelineTemplate,
});
} else {
this.setStateSafe({
graph,
reducedGraph,
graphV2: undefined,
selectedVersion,
templateString: selectedVersionPipelineTemplate,
});
}
this.setStateSafe({
graph,
reducedGraph,
selectedVersion,
templateString: selectedVersionPipelineTemplate,
});
}
}
@ -409,16 +398,38 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> {
return '';
}
private async _createGraph(templateString: string): Promise<dagre.graphlib.Graph | null> {
private async _createGraph(
templateString: string,
): Promise<
[dagre.graphlib.Graph | null, dagre.graphlib.Graph | null | undefined, PipelineFlowElement[]]
> {
let graph: graphlib.Graph | null = null;
let reducedGraph: graphlib.Graph | null | undefined = null;
let graphV2: PipelineFlowElement[] = [];
if (templateString) {
try {
const template = JsYaml.safeLoad(templateString);
return StaticGraphParser.createGraph(template!);
if (WorkflowUtils.isArgoWorkflowTemplate(template)) {
graph = StaticGraphParser.createGraph(template!);
reducedGraph = graph ? transitiveReduction(graph) : undefined;
if (graph && reducedGraph && compareGraphEdges(graph, reducedGraph)) {
reducedGraph = undefined; // disable reduction switch
}
} else if (isFeatureEnabled(FeatureKey.V2)) {
const pipelineSpec = WorkflowUtils.convertJsonToV2PipelineSpec(templateString);
graphV2 = convertFlowElements(pipelineSpec);
} else {
throw new Error(
'Unable to convert string response from server to Argo workflow template' +
': https://argoproj.github.io/argo-workflows/workflow-templates/',
);
}
} catch (err) {
await this.showPageError('Error: failed to generate Pipeline graph.', err);
}
}
return null;
return [graph, reducedGraph, graphV2];
}
private _deleteCallback(_: string[], success: boolean): void {

View File

@ -88,21 +88,21 @@ export interface PipelineDetailsV1Props {
graph: dagre.graphlib.Graph | null;
reducedGraph: dagre.graphlib.Graph | null;
pipeline: ApiPipeline | null;
selectedVersion: ApiPipelineVersion | undefined;
versions: ApiPipelineVersion[];
templateString?: string;
updateBanner: (bannerProps: BannerProps) => void;
selectedVersion: ApiPipelineVersion | undefined;
versions: ApiPipelineVersion[];
handleVersionSelected: (versionId: string) => Promise<void>;
}
const PipelineDetailsV1: React.FC<PipelineDetailsV1Props> = ({
pipeline,
selectedVersion,
versions,
graph,
reducedGraph,
templateString,
updateBanner,
selectedVersion,
versions,
handleVersionSelected,
}: PipelineDetailsV1Props) => {
const [selectedTab, setSelectedTab] = useState(0);

View File

@ -33,13 +33,40 @@ describe('PipelineDetailsV2', () => {
<CommonTestWrapper>
<PipelineDetailsV2
pipelineFlowElements={[]}
setSubDagLayers={layers => {}}
setSubDagLayers={function(layers: string[]): void {
return;
}}
apiPipeline={null}
selectedVersion={undefined}
versions={[]}
handleVersionSelected={function(versionId: string): Promise<void> {
return Promise.resolve();
}}
></PipelineDetailsV2>
</CommonTestWrapper>,
);
expect(screen.getByTestId('StaticCanvas')).not.toBeNull();
});
it('Render summary card', async () => {
render(
<CommonTestWrapper>
<PipelineDetailsV2
pipelineFlowElements={[]}
setSubDagLayers={function(layers: string[]): void {
return;
}}
apiPipeline={null}
selectedVersion={undefined}
versions={[]}
handleVersionSelected={function(versionId: string): Promise<void> {
return Promise.resolve();
}}
></PipelineDetailsV2>
</CommonTestWrapper>,
);
userEvent.click(screen.getByText('Show Summary'));
});
it('Render Execution node', async () => {
render(
<CommonTestWrapper>
@ -55,6 +82,12 @@ describe('PipelineDetailsV2', () => {
},
]}
setSubDagLayers={layers => {}}
apiPipeline={null}
selectedVersion={undefined}
versions={[]}
handleVersionSelected={function(versionId: string): Promise<void> {
return Promise.resolve();
}}
></PipelineDetailsV2>
</CommonTestWrapper>,
);
@ -78,6 +111,12 @@ describe('PipelineDetailsV2', () => {
},
]}
setSubDagLayers={layers => {}}
apiPipeline={null}
selectedVersion={undefined}
versions={[]}
handleVersionSelected={function(versionId: string): Promise<void> {
return Promise.resolve();
}}
></PipelineDetailsV2>
</CommonTestWrapper>,
);

View File

@ -15,9 +15,11 @@
*/
import React, { useState } from 'react';
import { Elements, FlowElement } from 'react-flow-renderer';
import { ApiPipeline, ApiPipelineVersion } from 'src/apis/pipeline';
import MD2Tabs from 'src/atoms/MD2Tabs';
import Editor from 'src/components/Editor';
import { FlowElementDataBase } from 'src/components/graph/Constants';
import { PipelineVersionCard } from 'src/components/navigators/PipelineVersionCard';
import SidePanel from 'src/components/SidePanel';
import { StaticNodeDetailsV2 } from 'src/components/tabs/StaticNodeDetailsV2';
import { isSafari } from 'src/lib/Utils';
@ -31,12 +33,20 @@ interface PipelineDetailsV2Props {
templateString?: string;
pipelineFlowElements: PipelineFlowElement[];
setSubDagLayers: (layers: string[]) => void;
apiPipeline: ApiPipeline | null;
selectedVersion: ApiPipelineVersion | undefined;
versions: ApiPipelineVersion[];
handleVersionSelected: (versionId: string) => Promise<void>;
}
function PipelineDetailsV2({
templateString,
pipelineFlowElements,
setSubDagLayers,
apiPipeline,
selectedVersion,
versions,
handleVersionSelected,
}: PipelineDetailsV2Props) {
const [layers, setLayers] = useState(['root']);
const [selectedTab, setSelectedTab] = useState(0);
@ -79,6 +89,12 @@ function PipelineDetailsV2({
elements={pipelineFlowElements}
onSelectionChange={onSelectionChange}
></StaticCanvas>
<PipelineVersionCard
apiPipeline={apiPipeline}
selectedVersion={selectedVersion}
versions={versions}
handleVersionSelected={handleVersionSelected}
/>
{templateString && (
<div className='z-20'>
<SidePanel

View File

@ -5,6 +5,10 @@ module.exports = {
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
spacing: {
'112': '28rem',
'136': '34rem',
},
// https://tailwindcss.com/docs/customizing-colors
colors: {
// https://material.io/resources/color