diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts index 09b34e2a8c..d56c8bb0a7 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts @@ -24,7 +24,7 @@ class PipelineImportModal extends Modal { } findSubmitButton() { - return this.findFooter().findByTestId('import-button'); + return this.findFooter().findByTestId('modal-submit-button'); } findUploadPipelineRadio() { @@ -56,7 +56,7 @@ class PipelineImportModal extends Modal { } findImportModalError() { - return this.find().findByTestId('import-modal-error'); + return this.find().findByTestId('error-message-alert'); } mockCreatePipelineAndVersion(params: CreatePipelineAndVersionKFData, namespace: string) { diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts index 81a84bc995..e4874e4cd0 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts @@ -15,7 +15,7 @@ class PipelineImportModal extends Modal { } findSubmitButton() { - return this.findFooter().findByRole('button', { name: 'Upload', hidden: true }); + return cy.findByTestId('modal-submit-button'); } findVersionNameInput() { @@ -51,7 +51,7 @@ class PipelineImportModal extends Modal { } findImportModalError() { - return this.find().findByTestId('import-modal-error'); + return this.find().findByTestId('error-message-alert'); } selectPipelineByName(name: string) { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts index 354e3d0b79..842074d18f 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts @@ -34,6 +34,7 @@ import type { PipelineKF, PipelineVersionKF } from '~/concepts/pipelines/kfTypes import { tablePagination } from '~/__tests__/cypress/cypress/pages/components/Pagination'; import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url'; import { pipelineRunsGlobal } from '~/__tests__/cypress/cypress/pages/pipelines/pipelineRunsGlobal'; +import { argoAlert } from '~/__tests__/cypress/cypress/pages/pipelines/argoAlert'; const projectName = 'test-project-name'; const initialMockPipeline = buildMockPipeline({ display_name: 'Test pipeline' }); @@ -43,6 +44,7 @@ const initialMockPipelineVersion = buildMockPipelineVersion({ const pipelineYamlPath = './cypress/tests/mocked/pipelines/mock-upload-pipeline.yaml'; const argoWorkflowPipeline = './cypress/tests/mocked/pipelines/argo-workflow-pipeline.yaml'; const tooLargePipelineYAMLPath = './cypress/tests/mocked/pipelines/not-a-pipeline-2-megabytes.yaml'; +const v1PipelineYamlPath = './cypress/tests/mocked/pipelines/v1-pipeline.yaml'; describe('Pipelines', () => { it('Empty state', () => { @@ -644,6 +646,26 @@ describe('Pipelines', () => { pipelineImportModal.findImportModalError().contains('Unsupported pipeline version'); }); + it('fails to import a v1 pipeline', () => { + initIntercepts({}); + pipelinesGlobal.visit(projectName); + + // Open the "Import pipeline" modal + pipelinesGlobal.findImportPipelineButton().click(); + + // Fill out the "Import pipeline" modal and submit + pipelineImportModal.shouldBeOpen(); + pipelineImportModal.fillPipelineName('New pipeline'); + pipelineImportModal.fillPipelineDescription('New pipeline description'); + pipelineImportModal.uploadPipelineYaml(v1PipelineYamlPath); + pipelineImportModal.submit(); + + pipelineImportModal.findImportModalError().should('exist'); + pipelineImportModal.findImportModalError().contains('Pipeline update and recompile required'); + argoAlert.findCloudServiceReleaseNotesLink().should('exist'); + argoAlert.findSelfManagedReleaseNotesLink().should('exist'); + }); + it('imports a new pipeline by url', () => { initIntercepts({}); pipelinesGlobal.visit(projectName); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/v1-pipeline.yaml b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/v1-pipeline.yaml new file mode 100644 index 0000000000..42e6e53312 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/v1-pipeline.yaml @@ -0,0 +1,680 @@ +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: conditional-execution-pipeline + annotations: + tekton.dev/output_artifacts: '{"flip-coin": [{"key": "artifacts/$PIPELINERUN/flip-coin/Output.tgz", + "name": "flip-coin-Output", "path": "/tmp/outputs/Output/data"}], "random-num": + [{"key": "artifacts/$PIPELINERUN/random-num/Output.tgz", "name": "random-num-Output", + "path": "/tmp/outputs/Output/data"}], "random-num-2": [{"key": "artifacts/$PIPELINERUN/random-num-2/Output.tgz", + "name": "random-num-2-Output", "path": "/tmp/outputs/Output/data"}]}' + tekton.dev/input_artifacts: '{"print-msg": [{"name": "random-num-Output", "parent_task": + "random-num"}], "print-msg-2": [{"name": "random-num-Output", "parent_task": + "random-num"}], "print-msg-3": [{"name": "random-num-2-Output", "parent_task": + "random-num-2"}], "print-msg-4": [{"name": "random-num-2-Output", "parent_task": + "random-num-2"}]}' + tekton.dev/artifact_bucket: mlpipeline + tekton.dev/artifact_endpoint: minio-service.kubeflow:9000 + tekton.dev/artifact_endpoint_scheme: http:// + tekton.dev/artifact_items: '{"flip-coin": [["Output", "$(results.Output.path)"]], + "print-msg": [], "print-msg-2": [], "print-msg-3": [], "print-msg-4": [], "random-num": + [["Output", "$(results.Output.path)"]], "random-num-2": [["Output", "$(results.Output.path)"]]}' + sidecar.istio.io/inject: "false" + tekton.dev/template: '' + pipelines.kubeflow.org/big_data_passing_format: $(workspaces.$TASK_NAME.path)/artifacts/$ORIG_PR_NAME/$TASKRUN_NAME/$TASK_PARAM_NAME + pipelines.kubeflow.org/pipeline_spec: '{"description": "Shows how to use dsl.Condition().", + "name": "conditional-execution-pipeline"}' + labels: + pipelines.kubeflow.org/pipelinename: '' + pipelines.kubeflow.org/generation: '' +spec: + pipelineSpec: + tasks: + - name: flip-coin + taskSpec: + steps: + - name: main + args: + - '----output-paths' + - $(results.Output.path) + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def flip_coin(): + """Flip a coin and output heads or tails randomly.""" + import random + result = 'heads' if random.randint(0, 1) == 0 else 'tails' + print(result) + return result + + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format( + str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Flip coin', description='Flip a coin and output heads or tails randomly.') + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = flip_coin(**_parsed_args) + + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) + image: registry.access.redhat.com/ubi8/python-39 + results: + - name: Output + type: string + description: /tmp/outputs/Output/data + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Flip coin", "outputs": + [{"name": "Output", "type": "String"}], "version": "Flip coin@sha256=32d5bd05b9fa18850505b73d6fb8489cc61f83033306230c8e4da12bdd8890e0"}' + - name: random-num + taskSpec: + steps: + - name: main + args: + - --low + - '0' + - --high + - '9' + - '----output-paths' + - $(results.Output.path) + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def random_num(low, high): + """Generate a random number between low and high.""" + import random + result = random.randint(low, high) + print(result) + return result + + def _serialize_int(int_value: int) -> str: + if isinstance(int_value, str): + return int_value + if not isinstance(int_value, int): + raise TypeError('Value "{}" has type "{}" instead of int.'.format( + str(int_value), str(type(int_value)))) + return str(int_value) + + import argparse + _parser = argparse.ArgumentParser(prog='Random num', description='Generate a random number between low and high.') + _parser.add_argument("--low", dest="low", type=int, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--high", dest="high", type=int, required=True, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = random_num(**_parsed_args) + + _outputs = [_outputs] + + _output_serializers = [ + _serialize_int, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) + image: registry.access.redhat.com/ubi8/python-39 + results: + - name: Output + type: string + description: /tmp/outputs/Output/data + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Random num", + "outputs": [{"name": "Output", "type": "Integer"}], "version": "Random + num@sha256=053403c9d093bbdb07a6da42e22012e69fa5132e38cc179dae5f3a629543650c"}' + when: + - input: $(tasks.condition-1.results.outcome) + operator: in + values: + - "true" + - name: print-msg + params: + - name: random-num-Output + value: $(tasks.random-num.results.Output) + taskSpec: + steps: + - name: main + args: + - --msg + - heads and $(inputs.params.random-num-Output) > 5! + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def print_msg(msg): + """Print a message.""" + print(msg) + + import argparse + _parser = argparse.ArgumentParser(prog='Print msg', description='Print a message.') + _parser.add_argument("--msg", dest="msg", type=str, required=True, default=argparse.SUPPRESS) + _parsed_args = vars(_parser.parse_args()) + + _outputs = print_msg(**_parsed_args) + image: registry.access.redhat.com/ubi8/python-39 + params: + - name: random-num-Output + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Print msg", "outputs": + [], "version": "Print msg@sha256=1d475b025fa0e9910c3c2827a8280bb0fb85abeba446658a944570e1de7f0f98"}' + when: + - input: $(tasks.condition-2.results.outcome) + operator: in + values: + - "true" + - name: print-msg-2 + params: + - name: random-num-Output + value: $(tasks.random-num.results.Output) + taskSpec: + steps: + - name: main + args: + - --msg + - heads and $(inputs.params.random-num-Output) <= 5! + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def print_msg(msg): + """Print a message.""" + print(msg) + + import argparse + _parser = argparse.ArgumentParser(prog='Print msg', description='Print a message.') + _parser.add_argument("--msg", dest="msg", type=str, required=True, default=argparse.SUPPRESS) + _parsed_args = vars(_parser.parse_args()) + + _outputs = print_msg(**_parsed_args) + image: registry.access.redhat.com/ubi8/python-39 + params: + - name: random-num-Output + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Print msg", "outputs": + [], "version": "Print msg@sha256=1d475b025fa0e9910c3c2827a8280bb0fb85abeba446658a944570e1de7f0f98"}' + when: + - input: $(tasks.condition-3.results.outcome) + operator: in + values: + - "true" + - name: random-num-2 + taskSpec: + steps: + - name: main + args: + - --low + - '10' + - --high + - '19' + - '----output-paths' + - $(results.Output.path) + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def random_num(low, high): + """Generate a random number between low and high.""" + import random + result = random.randint(low, high) + print(result) + return result + + def _serialize_int(int_value: int) -> str: + if isinstance(int_value, str): + return int_value + if not isinstance(int_value, int): + raise TypeError('Value "{}" has type "{}" instead of int.'.format( + str(int_value), str(type(int_value)))) + return str(int_value) + + import argparse + _parser = argparse.ArgumentParser(prog='Random num', description='Generate a random number between low and high.') + _parser.add_argument("--low", dest="low", type=int, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--high", dest="high", type=int, required=True, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = random_num(**_parsed_args) + + _outputs = [_outputs] + + _output_serializers = [ + _serialize_int, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) + image: registry.access.redhat.com/ubi8/python-39 + results: + - name: Output + type: string + description: /tmp/outputs/Output/data + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Random num", + "outputs": [{"name": "Output", "type": "Integer"}], "version": "Random + num@sha256=053403c9d093bbdb07a6da42e22012e69fa5132e38cc179dae5f3a629543650c"}' + when: + - input: $(tasks.condition-4.results.outcome) + operator: in + values: + - "true" + - name: print-msg-3 + params: + - name: random-num-2-Output + value: $(tasks.random-num-2.results.Output) + taskSpec: + steps: + - name: main + args: + - --msg + - tails and $(inputs.params.random-num-2-Output) > 15! + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def print_msg(msg): + """Print a message.""" + print(msg) + + import argparse + _parser = argparse.ArgumentParser(prog='Print msg', description='Print a message.') + _parser.add_argument("--msg", dest="msg", type=str, required=True, default=argparse.SUPPRESS) + _parsed_args = vars(_parser.parse_args()) + + _outputs = print_msg(**_parsed_args) + image: registry.access.redhat.com/ubi8/python-39 + params: + - name: random-num-2-Output + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Print msg", "outputs": + [], "version": "Print msg@sha256=1d475b025fa0e9910c3c2827a8280bb0fb85abeba446658a944570e1de7f0f98"}' + when: + - input: $(tasks.condition-5.results.outcome) + operator: in + values: + - "true" + - name: print-msg-4 + params: + - name: random-num-2-Output + value: $(tasks.random-num-2.results.Output) + taskSpec: + steps: + - name: main + args: + - --msg + - tails and $(inputs.params.random-num-2-Output) <= 15! + command: + - sh + - -ec + - | + program_path=$(mktemp) + printf "%s" "$0" > "$program_path" + python3 -u "$program_path" "$@" + - | + def print_msg(msg): + """Print a message.""" + print(msg) + + import argparse + _parser = argparse.ArgumentParser(prog='Print msg', description='Print a message.') + _parser.add_argument("--msg", dest="msg", type=str, required=True, default=argparse.SUPPRESS) + _parsed_args = vars(_parser.parse_args()) + + _outputs = print_msg(**_parsed_args) + image: registry.access.redhat.com/ubi8/python-39 + params: + - name: random-num-2-Output + metadata: + labels: + pipelines.kubeflow.org/cache_enabled: "true" + annotations: + pipelines.kubeflow.org/component_spec_digest: '{"name": "Print msg", "outputs": + [], "version": "Print msg@sha256=1d475b025fa0e9910c3c2827a8280bb0fb85abeba446658a944570e1de7f0f98"}' + when: + - input: $(tasks.condition-6.results.outcome) + operator: in + values: + - "true" + - name: condition-1 + params: + - name: operand1 + value: $(tasks.flip-coin.results.Output) + - name: operand2 + value: heads + - name: operator + value: == + taskSpec: + results: + - name: outcome + type: string + description: Conditional task outcome + params: + - name: operand1 + - name: operand2 + - name: operator + steps: + - name: main + command: + - sh + - -ec + - program_path=$(mktemp); printf "%s" "$0" > "$program_path"; python3 -u + "$program_path" "$1" "$2" + args: + - | + import sys + input1=str.rstrip(sys.argv[1]) + input2=str.rstrip(sys.argv[2]) + try: + input1=int(input1) + input2=int(input2) + except: + input1=str(input1) + outcome="true" if (input1 $(inputs.params.operator) input2) else "false" + f = open("/tekton/results/outcome", "w") + f.write(outcome) + f.close() + - $(inputs.params.operand1) + - $(inputs.params.operand2) + image: registry.access.redhat.com/ubi8/python-39 + - name: condition-2 + params: + - name: operand1 + value: $(tasks.random-num.results.Output) + - name: operand2 + value: '5' + - name: operator + value: '>' + taskSpec: + results: + - name: outcome + type: string + description: Conditional task outcome + params: + - name: operand1 + - name: operand2 + - name: operator + steps: + - name: main + command: + - sh + - -ec + - program_path=$(mktemp); printf "%s" "$0" > "$program_path"; python3 -u + "$program_path" "$1" "$2" + args: + - | + import sys + input1=str.rstrip(sys.argv[1]) + input2=str.rstrip(sys.argv[2]) + try: + input1=int(input1) + input2=int(input2) + except: + input1=str(input1) + outcome="true" if (input1 $(inputs.params.operator) input2) else "false" + f = open("/tekton/results/outcome", "w") + f.write(outcome) + f.close() + - $(inputs.params.operand1) + - $(inputs.params.operand2) + image: registry.access.redhat.com/ubi8/python-39 + when: + - input: $(tasks.condition-1.results.outcome) + operator: in + values: + - "true" + - name: condition-3 + params: + - name: operand1 + value: $(tasks.random-num.results.Output) + - name: operand2 + value: '5' + - name: operator + value: <= + taskSpec: + results: + - name: outcome + type: string + description: Conditional task outcome + params: + - name: operand1 + - name: operand2 + - name: operator + steps: + - name: main + command: + - sh + - -ec + - program_path=$(mktemp); printf "%s" "$0" > "$program_path"; python3 -u + "$program_path" "$1" "$2" + args: + - | + import sys + input1=str.rstrip(sys.argv[1]) + input2=str.rstrip(sys.argv[2]) + try: + input1=int(input1) + input2=int(input2) + except: + input1=str(input1) + outcome="true" if (input1 $(inputs.params.operator) input2) else "false" + f = open("/tekton/results/outcome", "w") + f.write(outcome) + f.close() + - $(inputs.params.operand1) + - $(inputs.params.operand2) + image: registry.access.redhat.com/ubi8/python-39 + when: + - input: $(tasks.condition-1.results.outcome) + operator: in + values: + - "true" + - name: condition-4 + params: + - name: operand1 + value: $(tasks.flip-coin.results.Output) + - name: operand2 + value: tails + - name: operator + value: == + taskSpec: + results: + - name: outcome + type: string + description: Conditional task outcome + params: + - name: operand1 + - name: operand2 + - name: operator + steps: + - name: main + command: + - sh + - -ec + - program_path=$(mktemp); printf "%s" "$0" > "$program_path"; python3 -u + "$program_path" "$1" "$2" + args: + - | + import sys + input1=str.rstrip(sys.argv[1]) + input2=str.rstrip(sys.argv[2]) + try: + input1=int(input1) + input2=int(input2) + except: + input1=str(input1) + outcome="true" if (input1 $(inputs.params.operator) input2) else "false" + f = open("/tekton/results/outcome", "w") + f.write(outcome) + f.close() + - $(inputs.params.operand1) + - $(inputs.params.operand2) + image: registry.access.redhat.com/ubi8/python-39 + - name: condition-5 + params: + - name: operand1 + value: $(tasks.random-num-2.results.Output) + - name: operand2 + value: '15' + - name: operator + value: '>' + taskSpec: + results: + - name: outcome + type: string + description: Conditional task outcome + params: + - name: operand1 + - name: operand2 + - name: operator + steps: + - name: main + command: + - sh + - -ec + - program_path=$(mktemp); printf "%s" "$0" > "$program_path"; python3 -u + "$program_path" "$1" "$2" + args: + - | + import sys + input1=str.rstrip(sys.argv[1]) + input2=str.rstrip(sys.argv[2]) + try: + input1=int(input1) + input2=int(input2) + except: + input1=str(input1) + outcome="true" if (input1 $(inputs.params.operator) input2) else "false" + f = open("/tekton/results/outcome", "w") + f.write(outcome) + f.close() + - $(inputs.params.operand1) + - $(inputs.params.operand2) + image: registry.access.redhat.com/ubi8/python-39 + when: + - input: $(tasks.condition-4.results.outcome) + operator: in + values: + - "true" + - name: condition-6 + params: + - name: operand1 + value: $(tasks.random-num-2.results.Output) + - name: operand2 + value: '15' + - name: operator + value: <= + taskSpec: + results: + - name: outcome + type: string + description: Conditional task outcome + params: + - name: operand1 + - name: operand2 + - name: operator + steps: + - name: main + command: + - sh + - -ec + - program_path=$(mktemp); printf "%s" "$0" > "$program_path"; python3 -u + "$program_path" "$1" "$2" + args: + - | + import sys + input1=str.rstrip(sys.argv[1]) + input2=str.rstrip(sys.argv[2]) + try: + input1=int(input1) + input2=int(input2) + except: + input1=str(input1) + outcome="true" if (input1 $(inputs.params.operator) input2) else "false" + f = open("/tekton/results/outcome", "w") + f.write(outcome) + f.close() + - $(inputs.params.operand1) + - $(inputs.params.operand2) + image: registry.access.redhat.com/ubi8/python-39 + when: + - input: $(tasks.condition-4.results.outcome) + operator: in + values: + - "true" \ No newline at end of file diff --git a/frontend/src/concepts/dashboard/DashboardModalFooter.tsx b/frontend/src/concepts/dashboard/DashboardModalFooter.tsx index f4a4aff15f..518c08b68a 100644 --- a/frontend/src/concepts/dashboard/DashboardModalFooter.tsx +++ b/frontend/src/concepts/dashboard/DashboardModalFooter.tsx @@ -20,6 +20,7 @@ type DashboardModalFooterProps = { isCancelDisabled?: boolean; alertTitle?: string; error?: Error; + alertLinks?: React.ReactNode; }; const DashboardModalFooter: React.FC = ({ @@ -32,12 +33,19 @@ const DashboardModalFooter: React.FC = ({ isCancelDisabled, error, alertTitle, + alertLinks, }) => ( // make sure alert uses the full width {error && ( - + {error.message} diff --git a/frontend/src/concepts/pipelines/content/InvalidArgoDeploymentAlert.tsx b/frontend/src/concepts/pipelines/content/InvalidArgoDeploymentAlert.tsx index 3c17af4319..f06cd046bc 100644 --- a/frontend/src/concepts/pipelines/content/InvalidArgoDeploymentAlert.tsx +++ b/frontend/src/concepts/pipelines/content/InvalidArgoDeploymentAlert.tsx @@ -1,15 +1,10 @@ -import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/react-core'; import React from 'react'; +import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; import { useBrowserStorage } from '~/components/browserStorage'; import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; +import PipelineMigrationNoteLinks from '~/concepts/pipelines/content/PipelineMigrationNoteLinks'; import { ODH_PRODUCT_NAME } from '~/utilities/const'; -const INVALID_ARGO_DEPLOYMENT_SELF_DOCUMENTATION_URL = - 'https://docs.redhat.com/en/documentation/red_hat_openshift_ai_self-managed/2-latest/html/working_with_data_science_pipelines/migrating-to-data-science-pipelines-2_ds-pipelines'; - -const INVALID_ARGO_DEPLOYMENT_CLOUD_DOCUMENTATION_URL = - 'https://docs.redhat.com/en/documentation/red_hat_openshift_ai_cloud_service/1/html/working_with_data_science_pipelines/migrating-to-data-science-pipelines-2_ds-pipelines'; - export const InvalidArgoDeploymentAlert: React.FC = () => { const [invalidArgoDeploymentAlertDismissed, setInvalidArgoDeploymentAlertDismissed] = useBrowserStorage('invalidArgoDeploymentAlertDismissed', false, true, true); @@ -29,28 +24,7 @@ export const InvalidArgoDeploymentAlert: React.FC = () => { return ( - - Self-Managed release notes - - - Cloud Service release notes - - - } + actionLinks={} actionClose={ setInvalidArgoDeploymentAlertDismissed(true)} /> } diff --git a/frontend/src/concepts/pipelines/content/PipelineMigrationNoteLinks.tsx b/frontend/src/concepts/pipelines/content/PipelineMigrationNoteLinks.tsx new file mode 100644 index 0000000000..083fc66504 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/PipelineMigrationNoteLinks.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { AlertActionLink } from '@patternfly/react-core'; + +const INVALID_ARGO_DEPLOYMENT_SELF_DOCUMENTATION_URL = + 'https://docs.redhat.com/en/documentation/red_hat_openshift_ai_self-managed/2-latest/html/working_with_data_science_pipelines/migrating-to-data-science-pipelines-2_ds-pipelines'; + +const INVALID_ARGO_DEPLOYMENT_CLOUD_DOCUMENTATION_URL = + 'https://docs.redhat.com/en/documentation/red_hat_openshift_ai_cloud_service/1/html/working_with_data_science_pipelines/migrating-to-data-science-pipelines-2_ds-pipelines'; + +const PipelineMigrationNoteLinks: React.FC = () => ( + <> + + Self-Managed release notes + + + Cloud Service release notes + + +); + +export default PipelineMigrationNoteLinks; diff --git a/frontend/src/concepts/pipelines/content/const.ts b/frontend/src/concepts/pipelines/content/const.ts index 8e6b4cbc12..8c0b41dea6 100644 --- a/frontend/src/concepts/pipelines/content/const.ts +++ b/frontend/src/concepts/pipelines/content/const.ts @@ -11,3 +11,6 @@ export const PIPELINE_CREATE_RUN_TOOLTIP_ARGO_ERROR = export const NAME_CHARACTER_LIMIT = 255; export const DESCRIPTION_CHARACTER_LIMIT = 255; + +export const PIPELINE_IMPORT_V1_ERROR_TEXT = + 'DSP 1.0 pipelines are no longer supported by OpenShift AI. To import this pipeline, you must update and recompile it. To learn more, view the Migrating pipelines from DSP 1.0 to 2.0 documentation.'; diff --git a/frontend/src/concepts/pipelines/content/import/PipelineImportBase.tsx b/frontend/src/concepts/pipelines/content/import/PipelineImportBase.tsx index d8f97a3752..9266d17c93 100644 --- a/frontend/src/concepts/pipelines/content/import/PipelineImportBase.tsx +++ b/frontend/src/concepts/pipelines/content/import/PipelineImportBase.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Alert, Button, Form, FormGroup, Stack, StackItem } from '@patternfly/react-core'; +import { Form, FormGroup, Stack, StackItem } from '@patternfly/react-core'; import { Modal } from '@patternfly/react-core/deprecated'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { PipelineKF, PipelineVersionKF } from '~/concepts/pipelines/kfTypes'; @@ -10,11 +10,14 @@ import { PIPELINE_ARGO_ERROR, NAME_CHARACTER_LIMIT, DESCRIPTION_CHARACTER_LIMIT, + PIPELINE_IMPORT_V1_ERROR_TEXT, } from '~/concepts/pipelines/content/const'; import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; import useDebounceCallback from '~/utilities/useDebounceCallback'; import NameDescriptionField from '~/concepts/k8s/NameDescriptionField'; -import { PipelineUploadOption, extractKindFromPipelineYAML } from './utils'; +import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; +import PipelineMigrationNoteLinks from '~/concepts/pipelines/content/PipelineMigrationNoteLinks'; +import { PipelineUploadOption, extractKindFromPipelineYAML, isYAMLPipelineV1 } from './utils'; import PipelineUploadRadio from './PipelineUploadRadio'; import { PipelineImportData } from './useImportModalData'; @@ -47,6 +50,7 @@ const PipelineImportBase: React.FC = ({ const { name, description, fileContents, pipelineUrl, uploadOption } = data; const [hasDuplicateName, setHasDuplicateName] = React.useState(false); const isArgoWorkflow = extractKindFromPipelineYAML(fileContents) === 'Workflow'; + const isV1PipelineFile = isYAMLPipelineV1(fileContents); const isImportButtonDisabled = !apiAvailable || @@ -91,7 +95,11 @@ const PipelineImportBase: React.FC = ({ }) .catch((e) => { setImporting(false); - setError(e); + if (e.message.includes('InvalidInputError') && isV1PipelineFile) { + setError(new Error(PIPELINE_IMPORT_V1_ERROR_TEXT)); + } else { + setError(e); + } }); } }; @@ -101,21 +109,24 @@ const PipelineImportBase: React.FC = ({ title={title} isOpen onClose={() => onBeforeClose()} - actions={[ - , - , - ]} + footer={ + onBeforeClose()} + onSubmit={onSubmit} + submitLabel={submitButtonText} + isSubmitLoading={importing} + isSubmitDisabled={isImportButtonDisabled} + error={error} + alertTitle={ + isArgoWorkflow + ? PIPELINE_ARGO_ERROR + : isV1PipelineFile + ? 'Pipeline update and recompile required' + : 'Error creating pipeline' + } + alertLinks={isV1PipelineFile ? : undefined} + /> + } variant="medium" data-testid="import-pipeline-modal" > @@ -153,25 +164,16 @@ const PipelineImportBase: React.FC = ({ setData('fileContents', value)} + setFileContents={(value) => { + setData('fileContents', value); + setError(undefined); + }} pipelineUrl={pipelineUrl} setPipelineUrl={(url) => setData('pipelineUrl', url)} uploadOption={uploadOption} setUploadOption={(option) => setData('uploadOption', option)} /> - {error && ( - - - {error.message} - - - )} diff --git a/frontend/src/concepts/pipelines/content/import/utils.ts b/frontend/src/concepts/pipelines/content/import/utils.ts index 7b98badb77..8a862c8897 100644 --- a/frontend/src/concepts/pipelines/content/import/utils.ts +++ b/frontend/src/concepts/pipelines/content/import/utils.ts @@ -23,3 +23,18 @@ export const extractKindFromPipelineYAML = (yamlFile: string): string | undefine return undefined; } }; + +export const isYAMLPipelineV1 = (yamlFile: string): boolean => { + try { + const parsedYaml = YAML.parse(yamlFile); + return ( + parsedYaml && + parsedYaml.kind === 'PipelineRun' && + parsedYaml.apiVersion === 'tekton.dev/v1beta1' + ); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error parsing YAML file:', e); + return false; + } +};