├── .prettierrc ├── cypress ├── fixtures │ └── example.json ├── tsconfig.json ├── support │ └── index.ts ├── .eslintrc.js ├── plugins │ └── index.js └── integration │ └── no-toolbar.ts ├── cypress.json ├── .storybook ├── preview.js └── main.js ├── lerna.json ├── .codesandbox └── ci.json ├── stories ├── Introduction.stories.mdx └── PipelineEditor.stories.js ├── examples ├── create-react-app-with-typescript │ ├── tsconfig.json │ ├── src │ │ ├── node.svg │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── App.tsx │ ├── .gitignore │ ├── package.json │ ├── README.md │ └── public │ │ └── index.html └── create-react-app │ ├── src │ ├── node.svg │ ├── index.js │ └── App.js │ ├── package.json │ ├── .gitignore │ ├── README.md │ └── public │ └── index.html ├── .vscode ├── settings.json └── launch.json ├── packages ├── pipeline-services │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── migration │ │ │ ├── errors │ │ │ │ └── index.ts │ │ │ ├── migrateV3 │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── migrateV4 │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── migrateV2 │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── migrateV5 │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── migrateV1 │ │ │ │ ├── index.ts │ │ │ │ └── index.test.ts │ │ │ ├── index.ts │ │ │ ├── migrateV7 │ │ │ │ ├── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── migrateV8 │ │ │ │ └── index.ts │ │ │ └── migrateV6 │ │ │ │ └── index.ts │ │ └── validation │ │ │ ├── validators │ │ │ ├── index.ts │ │ │ ├── enum-validators.ts │ │ │ ├── nested-enum-validators.ts │ │ │ ├── string-array-validators.ts │ │ │ ├── string-validators.ts │ │ │ └── number-validators.ts │ │ │ ├── utils.ts │ │ │ ├── types.ts │ │ │ ├── check-circular-references │ │ │ └── index.ts │ │ │ └── utils.test.ts │ ├── jest.config.js │ └── package.json └── pipeline-editor │ ├── tsconfig.json │ ├── src │ ├── css.d.ts │ ├── properties-panels │ │ ├── index.ts │ │ ├── PipelineProperties.tsx │ │ ├── useActiveFormItemShim.ts │ │ ├── index.test.tsx │ │ └── PropertiesPanel.tsx │ ├── styled.d.ts │ ├── CustomFormControls │ │ ├── index.ts │ │ ├── test-utils.ts │ │ ├── ErrorMessage.tsx │ │ ├── FileWidget.tsx │ │ ├── CustomFieldTemplate.tsx │ │ ├── components.ts │ │ ├── CustomOneOf.tsx │ │ └── CustomArray.tsx │ ├── index.ts │ ├── PipelineController │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── errors │ │ └── index.ts │ ├── IconButton │ │ └── index.tsx │ ├── ThemeProvider │ │ ├── utils.ts │ │ ├── useSystemInfo.ts │ │ ├── utils.test.ts │ │ └── index.tsx │ ├── NodeTooltip │ │ ├── utils.ts │ │ ├── index.tsx │ │ ├── index.test.tsx │ │ └── utils.test.ts │ ├── PalettePanel │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── PipelineEditor │ │ ├── useBlockEvents.ts │ │ └── index.test.tsx │ ├── types.ts │ ├── SplitPanelLayout │ │ └── index.test.tsx │ └── TabbedPanelLayout │ │ └── index.tsx │ ├── jest.config.js │ ├── jest.setup.js │ └── package.json ├── .gitignore ├── tsconfig.base.json ├── jest.config.base.js ├── jest.config.js ├── Makefile ├── .github ├── PULL_REQUEST_TEMPLATE └── workflows │ └── build.yaml ├── README.md ├── package.json ├── CONTRIBUTING.md ├── .eslintrc.js └── images └── banner.svg /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:6006", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | }; 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "1.12.1" 6 | } 7 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": [ 3 | "/examples/create-react-app", 4 | "/examples/create-react-app-with-typescript" 5 | ], 6 | "silent": true, 7 | "node": "16" 8 | } 9 | -------------------------------------------------------------------------------- /stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | 3 | 4 | 5 | # Welcome to Storybook 6 | 7 | This Storybook is only used for Cypress testing. 8 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: [ 3 | "../stories/**/*.stories.mdx", 4 | "../stories/**/*.stories.@(js|jsx|ts|tsx)", 5 | ], 6 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"], 7 | }; 8 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "isolatedModules": false, 5 | "target": "es5", 6 | "lib": ["es5", "dom"], 7 | "types": ["cypress", "@testing-library/cypress"] 8 | }, 9 | "include": ["**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.editor.labelFormat": "short", 3 | "[typescript]": { 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | } 7 | }, 8 | "[typescriptreact]": { 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/pipeline-services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "outDir": "dist", 8 | "declaration": true, 9 | "downlevelIteration": true, 10 | "sourceMap": true 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Chrome", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}/packages/demo/src", 10 | "sourceMapPathOverrides": { 11 | "webpack:///src/*": "${webRoot}/*" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/create-react-app/src/node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/pipeline-editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "module": "esnext", 7 | "outDir": "dist", 8 | "jsx": "react-jsx", 9 | "jsxFactory": "", 10 | "jsxFragmentFactory": "", 11 | "declaration": true, 12 | "downlevelIteration": true, 13 | "sourceMap": true 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/src/node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/create-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@elyra/pipeline-editor": "latest", 7 | "react": "latest", 8 | "react-dom": "latest", 9 | "react-scripts": "latest" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cypress/videos 2 | cypress/screenshots 3 | 4 | # production 5 | dist 6 | build 7 | 8 | # dependencies 9 | node_modules 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | coverage 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # PyCharm 34 | .idea/ 35 | *.iml 36 | 37 | -------------------------------------------------------------------------------- /examples/create-react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # IDEs and editors 15 | /.idea 16 | /.vscode 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | yarn.lock 29 | package-lock.json -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # IDEs and editors 15 | /.idea 16 | /.vscode 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | yarn.lock 29 | package-lock.json -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app-with-typescript", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@elyra/pipeline-editor": "latest", 7 | "react": "latest", 8 | "react-dom": "latest", 9 | "react-scripts": "latest" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "latest", 13 | "@types/react-dom": "latest", 14 | "typescript": "latest" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/css.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | declare module "*.css"; 18 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import "@testing-library/cypress/add-commands"; 18 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./migration"; 18 | export * from "./validation"; 19 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | plugins: ["cypress"], 19 | env: { 20 | "cypress/globals": true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /examples/create-react-app/README.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | This example demonstrates how you can use `@elyra/pipeline-editor` in a Create React App project. 20 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/README.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | This example demonstrates how you can use `@elyra/pipeline-editor` in a Create React App project with TypeScript support. 20 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = (_on, _config) => { 18 | // `on` is used to hook into various events Cypress emits 19 | // `config` is the resolved Cypress config 20 | }; 21 | -------------------------------------------------------------------------------- /packages/pipeline-services/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const baseConfig = require("../../jest.config.base"); 18 | 19 | module.exports = { 20 | ...baseConfig, 21 | roots: ["/src"], 22 | }; 23 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/properties-panels/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export { default as NodeProperties } from "./NodeProperties"; 18 | export { default as PipelineProperties } from "./PipelineProperties"; 19 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/styled.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import "styled-components"; 18 | import { Theme } from "./types"; 19 | 20 | declare module "styled-components" { 21 | export interface DefaultTheme extends Theme {} 22 | } 23 | -------------------------------------------------------------------------------- /packages/pipeline-services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elyra/pipeline-services", 3 | "version": "1.12.1", 4 | "main": "dist/index.js", 5 | "homepage": "https://github.com/elyra-ai/pipeline-editor", 6 | "bugs": { 7 | "url": "https://github.com/elyra-ai/pipeline-editor/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/elyra-ai/pipeline-editor" 12 | }, 13 | "license": "Apache-2.0", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "clean": "rm -rf node_modules; rm -rf dist; yarn unlink || true", 19 | "build": "tsc", 20 | "watch": "tsc -w --preserveWatchOutput", 21 | "test": "jest", 22 | "link": "yarn link" 23 | }, 24 | "dependencies": { 25 | "immer": "^9.0.7", 26 | "jsonc-parser": "^3.0.0" 27 | }, 28 | "publishConfig": { 29 | "access": "public" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render } from "react-dom"; 18 | 19 | import App from "./App"; 20 | 21 | const rootElement = document.getElementById("root"); 22 | render(, rootElement); 23 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* istanbul ignore file */ 18 | 19 | export * from "./FileWidget"; 20 | export * from "./CustomFieldTemplate"; 21 | export * from "./CustomArray"; 22 | export * from "./CustomOneOf"; 23 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "skipLibCheck": true, 5 | 6 | /* Strict Type-Checking Options */ 7 | "strict": true, 8 | "strictNullChecks": true, 9 | "strictFunctionTypes": true, 10 | "strictBindCallApply": true, 11 | "strictPropertyInitialization": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | 15 | /* Additional Checks */ 16 | // "noUnusedLocals": true, 17 | // "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "isolatedModules": true, 22 | 23 | /* Module Resolution Options */ 24 | "moduleResolution": "node", 25 | "allowSyntheticDefaultImports": true, 26 | "esModuleInterop": true, 27 | 28 | /* Advanced Options */ 29 | "resolveJsonModule": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | preset: "ts-jest", 19 | testEnvironment: "node", 20 | testMatch: [ 21 | // Match all typescript tests. 22 | "**/*.test.{ts,tsx}", 23 | // Ignore snapshot tests. 24 | "!**/*.snap.test.{ts,tsx}", 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./errors"; 18 | export { default as ThemeProvider, createTheme } from "./ThemeProvider"; 19 | export { default as PipelineEditor } from "./PipelineEditor"; 20 | export { PIPELINE_CURRENT_VERSION } from "./PipelineController"; 21 | -------------------------------------------------------------------------------- /cypress/integration/no-toolbar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | describe("no toolbar", () => { 18 | before(() => { 19 | cy.visit("/iframe.html?id=example-pipelineeditor--no-toolbar"); 20 | }); 21 | 22 | it("renders empty pipeline message", () => { 23 | cy.findByText(/your flow is empty/i).should("exist"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/errors/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class ComponentNotFoundError extends Error { 18 | constructor() { 19 | /* istanbul ignore next */ 20 | super("Component not found in any catalogue"); 21 | Object.setPrototypeOf(this, ComponentNotFoundError.prototype); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/create-react-app/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { StrictMode } from "react"; 18 | import ReactDOM from "react-dom"; 19 | 20 | import App from "./App"; 21 | 22 | const rootElement = document.getElementById("root"); 23 | ReactDOM.render( 24 | 25 | 26 | , 27 | rootElement 28 | ); 29 | -------------------------------------------------------------------------------- /packages/pipeline-editor/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const baseConfig = require("../../jest.config.base"); 18 | 19 | module.exports = { 20 | ...baseConfig, 21 | testEnvironment: "jest-environment-jsdom", 22 | setupFilesAfterEnv: [ 23 | "@testing-library/jest-dom/extend-expect", 24 | "./jest.setup.js", 25 | ], 26 | roots: ["/src"], 27 | }; 28 | -------------------------------------------------------------------------------- /stories/PipelineEditor.stories.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | 19 | import { PipelineEditor } from "../packages/pipeline-editor"; 20 | 21 | export default { 22 | title: "Example/PipelineEditor", 23 | component: PipelineEditor, 24 | }; 25 | 26 | const Template = (args) => ; 27 | 28 | export const NoToolbar = Template.bind({}); 29 | NoToolbar.args = {}; 30 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/test-utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { createStore } from "redux"; 18 | 19 | export function createPropertiesStore({ name }: { name: string }, value: any) { 20 | const initialState = { 21 | propertiesReducer: { 22 | [name]: value, 23 | }, 24 | }; 25 | 26 | function reducer(state: any) { 27 | return state; 28 | } 29 | 30 | return createStore(reducer, initialState); 31 | } 32 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/properties-panels/PipelineProperties.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Message } from "./PropertiesPanel"; 18 | 19 | interface Props { 20 | pipelineFlow: any; 21 | propertiesSchema?: any; 22 | onChange?: (data: any) => any; 23 | } 24 | 25 | function PipelineProperties(props: Props) { 26 | if (props.propertiesSchema === undefined) { 27 | return No pipeline properties defined.; 28 | } 29 | 30 | return
; 31 | } 32 | 33 | export default PipelineProperties; 34 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/PipelineController/utils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { getFileName } from "./utils"; 18 | 19 | it("strips extension when withExtension is false", () => { 20 | const name = getFileName("an/example/file.ipynb", { 21 | withExtension: false, 22 | }); 23 | expect(name).toBe("file"); 24 | }); 25 | 26 | it("keeps extension when withExtension is true", () => { 27 | const name = getFileName("an/example/file.ipynb", { 28 | withExtension: true, 29 | }); 30 | expect(name).toBe("file.ipynb"); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/pipeline-editor/jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | jest.mock("@elyra/canvas/dist/styles/common-canvas.min.css", () => "", { 18 | virtual: true, 19 | }); 20 | 21 | global.crypto = { 22 | getRandomValues: () => { 23 | return new Uint8Array(256); 24 | }, 25 | }; 26 | 27 | window.matchMedia = () => { 28 | return { 29 | matches: true, 30 | addEventListener: () => {}, 31 | }; 32 | }; 33 | 34 | window.scrollTo = () => { 35 | return; 36 | }; 37 | 38 | window.Element.prototype.getComputedTextLength = () => { 39 | return 200; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV3/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // NOTE: technically a pipeline can have a missing app_data field however, if 18 | // this is really an Elyra v2 pipeline, it should be guaranteed to have app_data 19 | // otherwise we wouldn't know this is a v2 pipeline. 20 | function migrate(pipeline: any) { 21 | // No-Op this is to disable old versions of Elyra 22 | // to see a pipeline with Python Script nodes 23 | pipeline.pipelines[0].app_data.version = 3; 24 | 25 | return pipeline; 26 | } 27 | 28 | export default migrate; 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | collectCoverageFrom: [ 19 | // Collect coverage for all typescript files. 20 | "**/*.{ts,tsx}", 21 | // Ignore `src/index.ts`, because it should only be exports. 22 | "!**/src/index.ts", 23 | // Ignore any typescript declaration files. 24 | "!**/*.d.ts", 25 | // Ignore any test utils. 26 | "!**/test-utils.{ts,tsx}", 27 | // ignore tests and snapshot tests. 28 | "!**/*.test.{ts,tsx}", 29 | ], 30 | coverageReporters: ["lcov", "text"], 31 | projects: ["/packages/*"], 32 | }; 33 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/validators/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface Validator { 18 | enabled: boolean; 19 | isValid: (value: T) => boolean; 20 | message?: string; 21 | } 22 | 23 | export function getErrorMessages(value: T, validators: Validator[]) { 24 | return validators 25 | .filter((v) => v.enabled && !v.isValid(value)) 26 | .map((v) => v.message); 27 | } 28 | 29 | export * from "./string-validators"; 30 | export * from "./number-validators"; 31 | export * from "./string-array-validators"; 32 | export * from "./enum-validators"; 33 | export * from "./nested-enum-validators"; 34 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import styled from "styled-components"; 18 | 19 | export const ErrorMessage = styled.div.attrs({ 20 | className: "elyra-errorMessage", 21 | })` 22 | position: absolute; 23 | left: 0; 24 | right: 0; 25 | padding: 5px; 26 | box-sizing: border-box; 27 | z-index: 1; 28 | border-style: solid; 29 | border-width: 1px; 30 | border-color: ${({ theme }) => theme.palette.errorMessage.errorBorder}; 31 | background-color: ${({ theme }) => theme.palette.errorMessage.main}; 32 | color: ${({ theme }) => theme.palette.errorMessage.contrastText}; 33 | `; 34 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/validators/enum-validators.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Validator } from "."; 18 | 19 | export interface EnumValidatorOptions { 20 | required?: boolean; 21 | pipeline_default?: boolean; 22 | } 23 | 24 | export function getEnumValidators({ 25 | required, 26 | pipeline_default, 27 | }: EnumValidatorOptions) { 28 | const validators: Validator[] = [ 29 | { 30 | enabled: required === true, 31 | isValid: (value: T) => { 32 | return value !== undefined || !!pipeline_default; 33 | }, 34 | }, 35 | ]; 36 | 37 | return validators; 38 | } 39 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class ElyraOutOfDateError extends Error { 18 | constructor() { 19 | /* istanbul ignore next */ 20 | super( 21 | "Pipeline was last edited in a newer version of Elyra. Update Elyra to use this pipeline." 22 | ); 23 | } 24 | } 25 | 26 | export class PipelineOutOfDateError extends Error { 27 | constructor() { 28 | /* istanbul ignore next */ 29 | super("Pipeline is out of date."); 30 | } 31 | } 32 | 33 | export class InvalidPipelineError extends Error { 34 | constructor() { 35 | /* istanbul ignore next */ 36 | super("Pipeline is invalid."); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018-2023 Elyra Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | .PHONY: help clean install dev-link watch 18 | 19 | help: 20 | # http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 21 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 22 | 23 | clean: ## Make a clean source tree and unlink packages 24 | - yarn clean 25 | 26 | lint: ## Run linters 27 | yarn lint 28 | yarn format 29 | 30 | install: ## Install dependencies and build packages 31 | yarn install && yarn build 32 | 33 | dev-link: ## Link packages 34 | yarn link-all 35 | 36 | watch: ## Watch packages. For use alongside jupyter lab --watch 37 | yarn watch 38 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/IconButton/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import styled from "styled-components"; 18 | 19 | const Container = styled.div` 20 | cursor: pointer; 21 | user-select: none; 22 | display: inline-block; 23 | transition: transform 50ms ease; 24 | position: relative; 25 | 26 | &:active { 27 | transform: scale(1.272019649); 28 | } 29 | `; 30 | 31 | const Icon = styled.div` 32 | color: ${({ theme }) => theme.palette.text.primary}; 33 | `; 34 | 35 | function IconButton(props: React.HTMLAttributes) { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export default IconButton; 44 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV4/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function migrate(pipelineFlow: any) { 18 | for (const pipeline of pipelineFlow.pipelines) { 19 | for (const node of pipeline.nodes) { 20 | if (node.type === "execution_node") { 21 | node.app_data = { 22 | label: node.app_data.ui_data?.label ?? "", 23 | component_parameters: node.app_data, 24 | ui_data: node.app_data.ui_data ?? {}, 25 | }; 26 | delete node.app_data.component_parameters.ui_data; 27 | } 28 | } 29 | } 30 | 31 | pipelineFlow.pipelines[0].app_data.version = 4; 32 | 33 | return pipelineFlow; 34 | } 35 | 36 | export default migrate; 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Developer's Certificate of Origin 1.1 5 | 6 | By making a contribution to this project, I certify that: 7 | 8 | (a) The contribution was created in whole or in part by me and I 9 | have the right to submit it under the Apache License 2.0; or 10 | 11 | (b) The contribution is based upon previous work that, to the best 12 | of my knowledge, is covered under an appropriate open source 13 | license and I have the right under that license to submit that 14 | work with modifications, whether created in whole or in part 15 | by me, under the same open source license (unless I am 16 | permitted to submit under a different license), as indicated 17 | in the file; or 18 | 19 | (c) The contribution was provided directly to me by some other 20 | person who certified (a), (b) or (c) and I have not modified 21 | it. 22 | 23 | (d) I understand and agree that this project and the contribution 24 | are public and that a record of the contribution (including all 25 | personal information I submit with it, including my sign-off) is 26 | maintained indefinitely and may be redistributed consistent with 27 | this project or the open source license(s) involved. 28 | 29 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV3/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import rawMigrate from "./"; 20 | 21 | // wrap migrate functions in immer 22 | const migrate = produce((d: any) => rawMigrate(d)); 23 | 24 | it("should only bump version", () => { 25 | const v2 = { 26 | pipelines: [ 27 | { 28 | app_data: { 29 | name: "name", 30 | version: 2, 31 | }, 32 | nodes: [], 33 | }, 34 | ], 35 | }; 36 | const expected = { 37 | pipelines: [ 38 | { 39 | app_data: { 40 | name: "name", 41 | version: 3, 42 | }, 43 | nodes: [], 44 | }, 45 | ], 46 | }; 47 | const actual = migrate(v2); 48 | expect(actual).toEqual(expected); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/ThemeProvider/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DeepPartial } from "redux"; 18 | 19 | function isPlainObject(item: any) { 20 | return item && typeof item === "object" && item.constructor === Object; 21 | } 22 | 23 | export function deepmerge(target: T, source: DeepPartial) { 24 | const output = { ...target }; 25 | 26 | if (isPlainObject(target) && isPlainObject(source)) { 27 | for (let _key of Object.keys(source)) { 28 | const key = _key as keyof DeepPartial; 29 | 30 | const tVal = target[key]; 31 | const sVal = source[key] as DeepPartial; 32 | 33 | if (sVal !== undefined) { 34 | if (isPlainObject(sVal) && tVal !== undefined) { 35 | output[key] = deepmerge(tVal, sVal); 36 | } else { 37 | output[key] = sVal as T[keyof T]; 38 | } 39 | } 40 | } 41 | } 42 | 43 | return output; 44 | } 45 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/properties-panels/useActiveFormItemShim.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useEffect } from "react"; 18 | 19 | function useActiveFormItemShim() { 20 | // Inject a `selected` class into common properties. 21 | useEffect(() => { 22 | function handleClick(e: MouseEvent) { 23 | const els = document.querySelectorAll( 24 | ".properties-control-panel > .properties-control-panel > .properties-ctrl-wrapper" 25 | ); 26 | 27 | for (const el of els) { 28 | if (el.contains(e.target as Node)) { 29 | el.classList.add("selected"); 30 | } else { 31 | el.classList.remove("selected"); 32 | } 33 | } 34 | } 35 | 36 | document.addEventListener("mousedown", handleClick, { 37 | capture: true, // prevent children from blocking 38 | }); 39 | return () => { 40 | document.removeEventListener("mousedown", handleClick); 41 | }; 42 | }, []); 43 | } 44 | 45 | export default useActiveFormItemShim; 46 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/NodeTooltip/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export function toPrettyString(o: any) { 18 | function toString(o: any) { 19 | if (typeof o === "boolean") { 20 | return o ? "Yes" : "No"; 21 | } 22 | 23 | if (o === undefined) { 24 | return "undefined"; 25 | } 26 | 27 | if (o === null) { 28 | return "null"; 29 | } 30 | 31 | return o.toString(); 32 | } 33 | 34 | if (Array.isArray(o)) { 35 | return o.map((v) => toString(v)).join("\n"); 36 | } 37 | 38 | if (!!o && o.constructor === Object) { 39 | return Object.entries(o) 40 | .map(([key, value]) => `${key}: ${toString(value)}`) 41 | .join("\n"); 42 | } 43 | 44 | return toString(o); 45 | } 46 | 47 | export function hasValue(o: any) { 48 | if (o === undefined || o === null) { 49 | return false; 50 | } 51 | 52 | if (Array.isArray(o)) { 53 | return o.length > 0; 54 | } 55 | 56 | if (typeof o === "boolean") { 57 | return true; 58 | } 59 | 60 | return o !== ""; 61 | } 62 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV2/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "path"; 18 | 19 | // NOTE: technically a pipeline can have a missing app_data field however, if 20 | // this is really an Elyra v1 pipeline, it should be guaranteed to have app_data 21 | // otherwise we wouldn't know this is a v1 pipeline. 22 | function migrate( 23 | pipeline: any, 24 | setNodePathsRelativeToPipelineV2?: (pipeline: any) => any 25 | ) { 26 | if (setNodePathsRelativeToPipelineV2) { 27 | pipeline.pipelines[0] = setNodePathsRelativeToPipelineV2( 28 | pipeline.pipelines[0] 29 | ); 30 | } else { 31 | for (const node of pipeline.pipelines[0].nodes) { 32 | if (node.app_data) { 33 | // If setNodePathsRelativeToPipeline is not given then just set 34 | // filename to the basename and the user will have to update the correct 35 | // path in node properties 36 | node.app_data.filename = path.basename(node.app_data.filename); 37 | } 38 | } 39 | } 40 | 41 | pipeline.pipelines[0].app_data.version = 2; 42 | 43 | return pipeline; 44 | } 45 | 46 | export default migrate; 47 | -------------------------------------------------------------------------------- /examples/create-react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/PalettePanel/index.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { fireEvent, render, screen } from "../test-utils"; 18 | import PalettePanel, { Node } from "./"; 19 | 20 | describe("Node", () => { 21 | it("renders", () => { 22 | render(); 23 | expect(screen.getByText("example label")).toBeInTheDocument(); 24 | }); 25 | }); 26 | 27 | it("can drag node", () => { 28 | render( 29 | 42 | ); 43 | 44 | const setDragImage = jest.fn(); 45 | const setData = jest.fn(); 46 | fireEvent.dragStart(screen.getByText("example label"), { 47 | dataTransfer: { 48 | setDragImage, 49 | setData, 50 | }, 51 | }); 52 | 53 | expect(setDragImage).toHaveBeenCalledTimes(1); 54 | expect(setData).toHaveBeenCalledTimes(1); 55 | expect(setData).toHaveBeenCalledWith("text", expect.any(String)); 56 | }); 57 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/validators/nested-enum-validators.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Validator } from "."; 18 | import { 19 | NestedEnumData as Data, 20 | NestedEnumFlatData as FlatData, 21 | } from "../types"; 22 | 23 | export interface NestedEnumValidatorOptions { 24 | data: Data[]; 25 | allownooptions?: boolean; 26 | required?: boolean; 27 | } 28 | 29 | export function getNestedEnumValidators({ 30 | data, 31 | allownooptions, 32 | required, 33 | }: NestedEnumValidatorOptions) { 34 | const validators: Validator[] = [ 35 | { 36 | enabled: required === true, 37 | isValid: (value: T) => value !== undefined, 38 | }, 39 | { 40 | enabled: data !== undefined, 41 | isValid: (value: T) => { 42 | const selected = data.find((item: Data) => item.value === value?.value); 43 | const option = selected?.options?.find( 44 | (item: Data) => item.value === value?.option 45 | ); 46 | return !!selected && (allownooptions || !!option); 47 | }, 48 | message: `Value must be a valid option`, 49 | }, 50 | ]; 51 | 52 | return validators; 53 | } 54 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV5/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const opMap: { [key: string]: string } = { 18 | "run-notebook-using-papermill": 19 | "run_notebook_using_papermill_Runnotebookusingpapermill", 20 | "filter-text": "filter_text_using_shell_and_grep_Filtertext", 21 | "bash-operator_BashOperator": "bash_operator_BashOperator", 22 | "email-operator_EmailOperator": "email_operator_EmailOperator", 23 | "http-operator_SimpleHttpOperator": "http_operator_SimpleHttpOperator", 24 | "spark-sql-operator_SparkSqlOperator": "spark_sql_operator_SparkSqlOperator", 25 | "spark-submit-operator_SparkSubmitOperator": 26 | "spark_submit_operator_SparkSubmitOperator", 27 | "slack-operator_SlackAPIPostOperator": "slack_operator_SlackAPIPostOperator", 28 | }; 29 | 30 | function migrate(pipelineFlow: any) { 31 | for (const pipeline of pipelineFlow.pipelines) { 32 | for (const node of pipeline.nodes) { 33 | const newOp = opMap[node.op]; 34 | if (newOp) { 35 | node.op = newOp; 36 | } 37 | } 38 | } 39 | 40 | pipelineFlow.pipelines[0].app_data.version = 5; 41 | 42 | return pipelineFlow; 43 | } 44 | 45 | export default migrate; 46 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/FileWidget.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useCallback } from "react"; 18 | 19 | import { Widget } from "@rjsf/core"; 20 | 21 | // TODO: Make the file clearable 22 | export const FileWidget: Widget = (props) => { 23 | const handleChooseFile = useCallback(async () => { 24 | props.formContext.onFileRequested({ 25 | canSelectMany: false, 26 | defaultUri: props.value, 27 | filters: { File: props.uiSchema.extensions }, 28 | propertyID: props.id.replace("root_component_parameters_", ""), 29 | parentID: props.uiSchema?.parentID, 30 | }); 31 | }, [props]); 32 | 33 | return ( 34 |
35 | { 41 | console.log(e); 42 | }} 43 | disabled 44 | /> 45 | 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/pipeline-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elyra/pipeline-editor", 3 | "version": "1.12.1", 4 | "main": "dist/index.js", 5 | "homepage": "https://github.com/elyra-ai/pipeline-editor", 6 | "bugs": { 7 | "url": "https://github.com/elyra-ai/pipeline-editor/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/elyra-ai/pipeline-editor" 12 | }, 13 | "license": "Apache-2.0", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "clean": "rm -rf node_modules; rm -rf dist; yarn unlink || true", 19 | "build": "microbundle --css inline -f cjs", 20 | "watch": "microbundle --css inline -o dist/index.js --no-pkg-main -f modern --watch --no-compress", 21 | "link": "yarn link" 22 | }, 23 | "dependencies": { 24 | "@elyra/canvas": "^12.23.2", 25 | "@elyra/pipeline-services": "^1.12.1", 26 | "@rjsf/core": "^4.2.3", 27 | "@rjsf/utils": "^5.0.0-beta.2", 28 | "@rjsf/validator-ajv6": "^5.0.0-beta.2", 29 | "carbon-components": "^10.43.0", 30 | "carbon-components-react": "^7.48.0", 31 | "carbon-icons": "7.0.7", 32 | "downshift": "^6.1.2", 33 | "immer": "^9.0.7", 34 | "nanoid": "^3.1.20", 35 | "react-dom": "^17.0.1", 36 | "react-intl": "^5.0.0", 37 | "react-redux": "^7.0.0", 38 | "redux": "^4.0.5", 39 | "styled-components": "^5.2.1" 40 | }, 41 | "devDependencies": { 42 | "@testing-library/jest-dom": "^5.11.9", 43 | "@testing-library/react": "^11.2.5", 44 | "@testing-library/user-event": "^12.8.0", 45 | "@types/react": "^17.0.1", 46 | "@types/react-dom": "^17.0.0", 47 | "@types/react-intl": "^3.0.0", 48 | "@types/react-redux": "^7.1.16", 49 | "@types/styled-components": "^5.1.7", 50 | "react": "^17.0.1" 51 | }, 52 | "peerDependencies": { 53 | "react": ">=17.0.0" 54 | }, 55 | "publishConfig": { 56 | "access": "public" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/ThemeProvider/useSystemInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useEffect, useMemo, useState } from "react"; 18 | 19 | function useSystemInfo() { 20 | const [mode, setMode] = useState<"dark" | "light">( 21 | window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" 22 | ); 23 | 24 | useEffect(() => { 25 | const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); 26 | 27 | try { 28 | // Chrome & Firefox 29 | darkMediaQuery.addEventListener("change", (e) => { 30 | if (e.matches) { 31 | setMode("dark"); 32 | } else { 33 | setMode("light"); 34 | } 35 | }); 36 | } catch { 37 | try { 38 | // Old Safari 39 | darkMediaQuery.addListener((e) => { 40 | if (e.matches) { 41 | setMode("dark"); 42 | } else { 43 | setMode("light"); 44 | } 45 | }); 46 | } catch {} 47 | } 48 | }, []); 49 | 50 | const platform = useMemo<"mac" | "win" | "other">(() => { 51 | if (window.navigator.platform.startsWith("Mac")) { 52 | return "mac"; 53 | } 54 | if (window.navigator.platform.startsWith("Win")) { 55 | return "win"; 56 | } 57 | return "other"; 58 | }, []); 59 | 60 | return useMemo(() => ({ mode, platform }), [mode, platform]); 61 | } 62 | 63 | export default useSystemInfo; 64 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/NodeTooltip/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import styled from "styled-components"; 18 | 19 | import { hasValue, toPrettyString } from "./utils"; 20 | 21 | interface Props { 22 | error?: string; 23 | nodeLabel?: string; 24 | properties: { 25 | label: string; 26 | value: any; 27 | }[]; 28 | } 29 | 30 | const Container = styled.div` 31 | padding-top: 7px; 32 | text-align: left; 33 | `; 34 | 35 | const Key = styled.div` 36 | color: ${({ theme }) => theme.palette.text.primary}; 37 | font-weight: 600; 38 | `; 39 | 40 | const Value = styled.div` 41 | margin-left: 7px; 42 | margin-bottom: 7px; 43 | white-space: pre-wrap; 44 | `; 45 | 46 | const ErrorValue = styled(Value)` 47 | color: ${({ theme }) => theme.palette.text.error}; 48 | `; 49 | 50 | function NodeTooltip({ error, nodeLabel, properties }: Props) { 51 | return ( 52 | 53 | {error && ( 54 |
55 | Error 56 | {toPrettyString(error)} 57 |
58 | )} 59 | {nodeLabel} 60 | {properties 61 | .filter(({ value }) => hasValue(value)) 62 | .map(({ label, value }) => ( 63 |
64 | {label} 65 | {toPrettyString(value)} 66 |
67 | ))} 68 |
69 | ); 70 | } 71 | 72 | export default NodeTooltip; 73 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV2/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import rawMigrate from "./"; 20 | 21 | // wrap migrate functions in immer 22 | const migrate = produce((d: any) => rawMigrate(d)); 23 | 24 | it("should change all node paths to relative", () => { 25 | const v1 = { 26 | pipelines: [ 27 | { 28 | app_data: { 29 | name: "name", 30 | version: 1, 31 | }, 32 | nodes: [ 33 | { app_data: { filename: "/user/niko/project/notebook.ipynb" } }, 34 | ], 35 | }, 36 | ], 37 | }; 38 | const expected = { 39 | pipelines: [ 40 | { 41 | app_data: { 42 | name: "name", 43 | version: 2, 44 | }, 45 | nodes: [{ app_data: { filename: "notebook.ipynb" } }], 46 | }, 47 | ], 48 | }; 49 | const actual = migrate(v1); 50 | expect(actual).toEqual(expected); 51 | }); 52 | 53 | it("should handle missing node app_data", () => { 54 | const v1 = { 55 | pipelines: [ 56 | { 57 | app_data: { 58 | name: "name", 59 | version: 1, 60 | }, 61 | nodes: [{}], 62 | }, 63 | ], 64 | }; 65 | const expected = { 66 | pipelines: [ 67 | { 68 | app_data: { 69 | name: "name", 70 | version: 2, 71 | }, 72 | nodes: [{}], 73 | }, 74 | ], 75 | }; 76 | const actual = migrate(v1); 77 | expect(actual).toEqual(expected); 78 | }); 79 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Node } from "jsonc-parser"; 18 | 19 | import { Link } from "./types"; 20 | 21 | export function getLinks(pipeline: any) { 22 | let links: Link[] = []; 23 | for (const [n, node] of pipeline.nodes.entries()) { 24 | for (const [i, input] of node.inputs?.entries() ?? []) { 25 | if (input.links !== undefined) { 26 | for (const [l, link] of input.links.entries()) { 27 | if (findNode(pipeline, link.node_id_ref) !== undefined) { 28 | links.push({ 29 | id: link.id, 30 | trgNodeId: node.id, 31 | srcNodeId: link.node_id_ref, 32 | // TODO: handle link type 33 | type: "", 34 | path: ["nodes", n, "inputs", i, "links", l], 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | return links; 42 | } 43 | 44 | export function getNodes(pipeline: any): any[] { 45 | return pipeline.nodes; 46 | } 47 | 48 | export function findNode(pipeline: any, id: string) { 49 | return pipeline.nodes.find((node: any) => node.id === id); 50 | } 51 | 52 | export function rangeForLocation(location: Node | undefined) { 53 | const offset = location?.parent?.offset ?? 0; 54 | const length = (location?.parent?.colonOffset ?? 0) - offset; 55 | return { offset, length }; 56 | } 57 | 58 | export function getValue(app_data: any, key: string, pipelineDefaults?: any) { 59 | return ( 60 | app_data?.component_parameters?.[key] ?? 61 | pipelineDefaults?.[key] ?? 62 | app_data[key] 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/validators/string-array-validators.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Validator } from "."; 18 | 19 | export interface StringArrayValidatorOptions { 20 | uniqueItems?: boolean; 21 | minItems?: number; // for restricting array length 22 | maxItems?: number; // for restricting array length 23 | keyValueEntries?: boolean; // if array item are in in the key=value format 24 | } 25 | 26 | const isKeyValueFormat = (item: string): boolean => { 27 | const parts = item.split("="); 28 | return parts.length >= 2 && parts[0] !== ""; 29 | }; 30 | 31 | export function getStringArrayValidators({ 32 | uniqueItems, 33 | minItems, 34 | maxItems, 35 | keyValueEntries, 36 | }: StringArrayValidatorOptions) { 37 | const validators: Validator[] = [ 38 | { 39 | enabled: uniqueItems === true, 40 | isValid: (value: T) => new Set(value).size === value.length, 41 | message: "Array has duplicate items.", 42 | }, 43 | { 44 | enabled: minItems !== undefined, 45 | isValid: (value: T) => value.length < minItems!, 46 | message: `Array must have at least ${minItems} items.`, 47 | }, 48 | { 49 | enabled: maxItems !== undefined, 50 | isValid: (value: T) => value.length < maxItems!, 51 | message: `Array must have at most ${maxItems} items.`, 52 | }, 53 | { 54 | enabled: keyValueEntries === true, 55 | isValid: (value: T) => { 56 | return !value.some((item) => !isKeyValueFormat(item)); 57 | }, 58 | message: "Array items must be in key=value format.", 59 | }, 60 | ]; 61 | 62 | return validators; 63 | } 64 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface Link { 18 | id: string; 19 | trgNodeId: string; 20 | srcNodeId: string; 21 | type: string; 22 | path: any[]; 23 | } 24 | 25 | export interface CircularReferenceInfo { 26 | type: "circularReference"; 27 | pipelineID: string; 28 | linkID: string; 29 | } 30 | 31 | export interface MissingPropertyInfo { 32 | type: "missingProperty"; 33 | pipelineID: string; 34 | nodeID?: string; 35 | property: string; 36 | } 37 | 38 | export interface InvalidPropertyInfo { 39 | type: "invalidProperty"; 40 | pipelineID: string; 41 | nodeID?: string; 42 | property: string; 43 | message: string; 44 | } 45 | 46 | export interface MissingComponentInfo { 47 | type: "missingComponent"; 48 | pipelineID: string; 49 | nodeID: string; 50 | op: string; 51 | } 52 | 53 | export interface Problem { 54 | severity: 1 | 2 | 3 | 4 | undefined; 55 | range: { 56 | offset: number; 57 | length: number; 58 | }; 59 | message: string; 60 | info: 61 | | CircularReferenceInfo 62 | | MissingPropertyInfo 63 | | InvalidPropertyInfo 64 | | MissingComponentInfo; 65 | } 66 | 67 | export interface PartialProblem { 68 | message: string; 69 | path: any[]; 70 | info: 71 | | CircularReferenceInfo 72 | | MissingPropertyInfo 73 | | InvalidPropertyInfo 74 | | MissingComponentInfo; 75 | } 76 | 77 | export interface NestedEnumData { 78 | value: string; 79 | label: string; 80 | options?: { 81 | value: string; 82 | label: string; 83 | }[]; 84 | } 85 | 86 | export interface NestedEnumFlatData { 87 | value: string; 88 | option: string; 89 | } 90 | -------------------------------------------------------------------------------- /examples/create-react-app/src/App.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useState } from "react"; 18 | 19 | import { PipelineEditor, ThemeProvider } from "@elyra/pipeline-editor"; 20 | 21 | import { createNode } from "./node-utils"; 22 | import imagePath from "./node.svg"; 23 | import theme from "./theme"; 24 | 25 | const node = createNode({ 26 | op: "execute-node", 27 | description: "A simple node", 28 | label: "Node", 29 | image: imagePath, 30 | properties: [ 31 | { 32 | id: "label", 33 | type: "string", 34 | label: "Label", 35 | description: "The label that shows up on the node.", 36 | default: "", 37 | required: true, 38 | }, 39 | { 40 | id: "enum", 41 | type: "string", 42 | enum: ["one", "two"], 43 | label: "Choose", 44 | description: "Choose one or the other.", 45 | default: "one", 46 | }, 47 | { 48 | id: "yes-or-no", 49 | type: "boolean", 50 | label: "Yes or no?", 51 | description: "Something should happen when this node is run.", 52 | default: false, 53 | }, 54 | { 55 | id: "array", 56 | type: "string[]", 57 | label: "Things", 58 | description: "Add some things okay.", 59 | default: [], 60 | }, 61 | ], 62 | }); 63 | 64 | function App() { 65 | const [pipeline, setPipeline] = useState(); 66 | return ( 67 | 68 | 76 | 77 | ); 78 | } 79 | 80 | export default App; 81 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /// 18 | /// 19 | /// 20 | 21 | declare namespace NodeJS { 22 | interface ProcessEnv { 23 | readonly NODE_ENV: "development" | "production" | "test"; 24 | readonly PUBLIC_URL: string; 25 | } 26 | } 27 | 28 | declare module "*.avif" { 29 | const src: string; 30 | export default src; 31 | } 32 | 33 | declare module "*.bmp" { 34 | const src: string; 35 | export default src; 36 | } 37 | 38 | declare module "*.gif" { 39 | const src: string; 40 | export default src; 41 | } 42 | 43 | declare module "*.jpg" { 44 | const src: string; 45 | export default src; 46 | } 47 | 48 | declare module "*.jpeg" { 49 | const src: string; 50 | export default src; 51 | } 52 | 53 | declare module "*.png" { 54 | const src: string; 55 | export default src; 56 | } 57 | 58 | declare module "*.webp" { 59 | const src: string; 60 | export default src; 61 | } 62 | 63 | declare module "*.svg" { 64 | import * as React from "react"; 65 | 66 | export const ReactComponent: React.FunctionComponent< 67 | React.SVGProps & { title?: string } 68 | >; 69 | 70 | const src: string; 71 | export default src; 72 | } 73 | 74 | declare module "*.module.css" { 75 | const classes: { readonly [key: string]: string }; 76 | export default classes; 77 | } 78 | 79 | declare module "*.module.scss" { 80 | const classes: { readonly [key: string]: string }; 81 | export default classes; 82 | } 83 | 84 | declare module "*.module.sass" { 85 | const classes: { readonly [key: string]: string }; 86 | export default classes; 87 | } 88 | -------------------------------------------------------------------------------- /examples/create-react-app-with-typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useState } from "react"; 18 | 19 | import { PipelineEditor, ThemeProvider } from "@elyra/pipeline-editor"; 20 | 21 | import { createNode } from "./node-utils"; 22 | import imagePath from "./node.svg"; 23 | import theme from "./theme"; 24 | 25 | const node = createNode({ 26 | op: "execute-node", 27 | description: "A simple node", 28 | label: "Node", 29 | image: imagePath, 30 | properties: [ 31 | { 32 | id: "label", 33 | type: "string", 34 | label: "Label", 35 | description: "The label that shows up on the node.", 36 | default: "", 37 | required: true, 38 | }, 39 | { 40 | id: "enum", 41 | type: "string", 42 | enum: ["one", "two"], 43 | label: "Choose", 44 | description: "Choose one or the other.", 45 | default: "one", 46 | }, 47 | { 48 | id: "yes-or-no", 49 | type: "boolean", 50 | label: "Yes or no?", 51 | description: "Something should happen when this node is run.", 52 | default: false, 53 | }, 54 | { 55 | id: "array", 56 | type: "string[]", 57 | label: "Things", 58 | description: "Add some things okay.", 59 | default: [], 60 | }, 61 | ], 62 | }); 63 | 64 | function App() { 65 | const [pipeline, setPipeline] = useState(); 66 | return ( 67 | 68 | 76 | 77 | ); 78 | } 79 | 80 | export default App; 81 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/PipelineController/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "path"; 18 | 19 | interface Options { 20 | withExtension: boolean; 21 | } 22 | 23 | export function getFileName(file: string, { withExtension }: Options) { 24 | const extension = path.extname(file); 25 | return path.basename(file, withExtension ? undefined : extension); 26 | } 27 | 28 | export function nestedToPrefixed(app_data: { [key: string]: any }) { 29 | let current_parameters: any = {}; 30 | 31 | const { component_parameters, pipeline_defaults, ...system } = app_data; 32 | for (const [key, val] of Object.entries(system)) { 33 | current_parameters[key] = val; 34 | } 35 | const nestedParameters = component_parameters ?? pipeline_defaults; 36 | if (nestedParameters) { 37 | for (const [key, val] of Object.entries(nestedParameters)) { 38 | current_parameters[`elyra_${key}`] = val; 39 | } 40 | } 41 | 42 | return current_parameters; 43 | } 44 | 45 | export function prefixedToNested( 46 | current_parameters: { [key: string]: any }, 47 | pipelineDefaults?: boolean 48 | ) { 49 | let app_data: any = pipelineDefaults 50 | ? { 51 | pipeline_defaults: {}, 52 | } 53 | : { 54 | component_parameters: {}, 55 | }; 56 | 57 | for (const [key, val] of Object.entries(current_parameters)) { 58 | if (key.startsWith("elyra_")) { 59 | const strippedKey = key.replace(/^elyra_/, ""); 60 | if (pipelineDefaults) { 61 | app_data.pipeline_defaults[strippedKey] = val; 62 | } else { 63 | app_data.component_parameters[strippedKey] = val; 64 | } 65 | } else { 66 | app_data[key] = val; 67 | } 68 | } 69 | 70 | return app_data; 71 | } 72 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/PipelineEditor/useBlockEvents.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useEffect, useRef } from "react"; 18 | 19 | interface Options { 20 | wheel?: boolean; 21 | contextmenu?: boolean; 22 | } 23 | 24 | function blockEvent(e: Event) { 25 | e.stopPropagation(); 26 | } 27 | 28 | // Setting `capture` to `true` will capture the event before drilling 29 | // through the DOM nodes to our target. This is what allows us to block the 30 | // event. Since we are stoping propagation, we must have `passive` set to 31 | // `false`. 32 | const options = { passive: false, capture: true }; 33 | 34 | function useBlockEvents(events: Options) { 35 | const ref = useRef(null); 36 | 37 | useEffect(() => { 38 | // The canvas steals focus on mount, which causes a jarring scroll to the 39 | // element. Scrolling to (0, 0) seems to prevent that, but this is really 40 | // just a bandaid on the issue and could have unintended effects. 41 | window.scrollTo(0, 0); 42 | }, []); 43 | 44 | useEffect(() => { 45 | const el = ref.current; 46 | 47 | if (events.wheel) { 48 | el?.addEventListener("wheel", blockEvent, options); 49 | } 50 | 51 | return () => { 52 | if (events.wheel) { 53 | el?.removeEventListener("wheel", blockEvent, options); 54 | } 55 | }; 56 | }, [events.wheel]); 57 | 58 | useEffect(() => { 59 | const el = ref.current; 60 | 61 | if (events.contextmenu) { 62 | ref.current?.addEventListener("contextmenu", blockEvent, options); 63 | } 64 | 65 | return () => { 66 | if (events.contextmenu) { 67 | el?.removeEventListener("contextmenu", blockEvent, options); 68 | } 69 | }; 70 | }, [events.contextmenu]); 71 | 72 | return ref; 73 | } 74 | 75 | export default useBlockEvents; 76 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface Theme { 18 | mode: "dark" | "light"; 19 | platform: "mac" | "win" | "other"; 20 | palette: { 21 | focus: string; 22 | border: string; 23 | divider: string; 24 | hover: string; 25 | active: string; 26 | tabBorder: string; 27 | inputBorder: string; 28 | sash: string; 29 | primary: { 30 | main: string; 31 | hover: string; 32 | contrastText: string; 33 | }; 34 | secondary: { 35 | main: string; 36 | contrastText: string; 37 | }; 38 | error: { 39 | main: string; 40 | contrastText: string; 41 | }; 42 | errorMessage: { 43 | main: string; 44 | contrastText: string; 45 | errorBorder: string; 46 | }; 47 | text: { 48 | icon: string; 49 | primary: string; 50 | secondary: string; 51 | bold: string; 52 | inactive: string; 53 | disabled: string; 54 | link: string; 55 | error: string; 56 | }; 57 | background: { 58 | default: string; 59 | secondary: string; 60 | input: string; 61 | }; 62 | highlight: { 63 | border: string; 64 | hover: string; 65 | focus: string; 66 | }; 67 | }; 68 | shape: { 69 | borderRadius: string; 70 | }; 71 | typography: { 72 | fontFamily: string; 73 | fontWeight: string; 74 | fontSize: string; 75 | }; 76 | overrides?: { 77 | chevronDownIcon?: React.ReactNode; 78 | deleteIcon?: React.ReactNode; 79 | editIcon?: React.ReactNode; 80 | folderIcon?: React.ReactNode; 81 | closeIcon?: React.ReactNode; 82 | propertiesIcon?: React.ReactNode; 83 | pipelineIcon?: React.ReactNode; 84 | paletteIcon?: React.ReactNode; 85 | checkIcon?: React.ReactNode; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 18 |

Elyra Pipeline Editor

19 | 20 |

21 | Pipeline Editor 22 |

23 | 24 |

25 | A react component for editing pipeline files. Used across all Elyra applications and browser extensions. 26 |

27 | 28 |

29 | NPM Status 30 | Test Status 31 |

32 | 33 | ## Installation 34 | 35 | `@elyra/pipeline-editor` is available as an [npm package](https://www.npmjs.com/package/@elyra/pipeline-editor): 36 | 37 | ```sh 38 | // npm 39 | npm install @elyra/pipeline-editor 40 | 41 | // yarn 42 | yarn add @elyra/pipeline-editor 43 | ``` 44 | 45 | Or can be built and linked locally: 46 | 47 | ```sh 48 | git clone git@github.com:elyra-ai/pipeline-editor.git 49 | cd pipeline-editor 50 | 51 | make clean install dev-link 52 | ``` 53 | 54 | Then in the project you're using the local build run the following: 55 | 56 | ```sh 57 | yarn link @elyra/pipeline-editor @elyra/pipeline-services 58 | ``` 59 | 60 | or if you're running with Elyra you can use make: 61 | 62 | ```sh 63 | make clean dev-link install 64 | ``` 65 | 66 | ## Usage 67 | 68 | ```jsx 69 | import { PipelineEditor } from "@elyra/pipeline-editor"; 70 | 71 | function App() { 72 | const [pipeline, setPipeline] = useState(); 73 | return ( 74 | 75 | ); 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/validators/string-validators.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Validator } from "."; 18 | 19 | export interface StringValidatorOptions { 20 | required?: boolean; 21 | pattern?: string; // for restricting strings to a given regular expression 22 | patternErrorMessage?: string; // for giving a tailored error message when a pattern does not match 23 | minLength?: number; // for restricting string length 24 | maxLength?: number; // for restricting string length 25 | format?: "file" | "multiline"; // for restricting strings to well-known formats. Potential future formats: "date" | "time" | "ipv4" | "email" | "uri" 26 | } 27 | 28 | export function getStringValidators({ 29 | required, 30 | pattern, 31 | patternErrorMessage, 32 | maxLength, 33 | minLength, 34 | format, 35 | }: StringValidatorOptions) { 36 | const validators: Validator[] = [ 37 | { 38 | enabled: required === true, 39 | isValid: (value: T) => value !== "", 40 | }, 41 | { 42 | enabled: maxLength !== undefined, 43 | isValid: (value: T) => value.length <= maxLength!, 44 | message: `Value must be ${maxLength} or fewer characters long.`, 45 | }, 46 | { 47 | enabled: minLength !== undefined, 48 | isValid: (value: T) => value.length >= minLength!, 49 | message: `Value must be ${minLength} or more characters long.`, 50 | }, 51 | { 52 | enabled: pattern !== undefined, 53 | isValid: (value: T) => new RegExp(pattern!).test(value), 54 | message: patternErrorMessage ?? `Value must match regex \`${pattern}\`.`, 55 | }, 56 | // We don't have any format validation yet, but when we do they will go here 57 | { 58 | enabled: format === "file", 59 | isValid: () => true, 60 | }, 61 | ]; 62 | 63 | return validators; 64 | } 65 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV1/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const hasAppdataField = (node: any, fieldName: string): boolean => { 18 | return !!node.app_data?.hasOwnProperty(fieldName); 19 | }; 20 | 21 | const deleteAppdataField = (node: any, fieldName: string): void => { 22 | if (hasAppdataField(node, fieldName)) { 23 | delete node.app_data[fieldName]; 24 | } 25 | }; 26 | 27 | const renameAppdataField = ( 28 | node: any, 29 | currentFieldName: string, 30 | newFieldName: string 31 | ): void => { 32 | if (hasAppdataField(node, currentFieldName)) { 33 | node.app_data[newFieldName] = node.app_data[currentFieldName]; 34 | deleteAppdataField(node, currentFieldName); 35 | } 36 | }; 37 | 38 | function migrate(pipeline: any) { 39 | renameAppdataField(pipeline.pipelines[0], "title", "name"); 40 | deleteAppdataField(pipeline.pipelines[0], "export"); 41 | deleteAppdataField(pipeline.pipelines[0], "export_format"); 42 | deleteAppdataField(pipeline.pipelines[0], "export_path"); 43 | 44 | for (const node of pipeline.pipelines[0].nodes) { 45 | if (node.type === "pipeline_node") { 46 | node.type = "execution_node"; 47 | } 48 | node.op = "execute-notebook-node"; 49 | renameAppdataField(node, "notebook", "filename"); 50 | renameAppdataField(node, "artifact", "filename"); 51 | renameAppdataField(node, "docker_image", "runtime_image"); 52 | renameAppdataField(node, "image", "runtime_image"); 53 | renameAppdataField(node, "vars", "env_vars"); 54 | renameAppdataField(node, "file_dependencies", "dependencies"); 55 | renameAppdataField( 56 | node, 57 | "recursive_dependencies", 58 | "include_subdirectories" 59 | ); 60 | } 61 | 62 | if (pipeline.pipelines[0].app_data) { 63 | pipeline.pipelines[0].app_data.version = 1; 64 | } else { 65 | pipeline.pipelines[0].app_data = { version: 1 }; 66 | } 67 | 68 | return pipeline; 69 | } 70 | 71 | export default migrate; 72 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import migrateV1 from "./migrateV1"; 20 | import migrateV2 from "./migrateV2"; 21 | import migrateV3 from "./migrateV3"; 22 | import migrateV4 from "./migrateV4"; 23 | import migrateV5 from "./migrateV5"; 24 | import migrateV6 from "./migrateV6"; 25 | import migrateV7 from "./migrateV7"; 26 | import migrateV8 from "./migrateV8"; 27 | import { mockPaletteV7 } from "./utils"; 28 | 29 | export * from "./errors"; 30 | 31 | export function migrate( 32 | pipelineJSON: any, 33 | setNodePathsRelativeToPipelineV2?: (pipeline: any) => any 34 | ) { 35 | return produce(pipelineJSON, (draft: any) => { 36 | const version = draft.pipelines[0].app_data?.version ?? 0; 37 | if (version < 1) { 38 | console.debug("migrating pipeline from v0 to v1"); 39 | migrateV1(draft); 40 | } 41 | if (version < 2) { 42 | console.debug("migrating pipeline from v1 to v2"); 43 | migrateV2(draft, setNodePathsRelativeToPipelineV2); 44 | } 45 | if (version < 3) { 46 | console.debug("migrating pipeline from v2 to v3"); 47 | migrateV3(draft); 48 | } 49 | if (version < 4) { 50 | console.debug("migrating pipeline from v3 to v4"); 51 | migrateV4(draft); 52 | } 53 | if (version < 5) { 54 | console.debug("migrating pipeline from v4 to v5"); 55 | migrateV5(draft); 56 | } 57 | // Note: starting with v8 the palette changed, so we now use 58 | // a copy of the palette as it was at v7 to migrate to v6/v7 59 | if (version < 6) { 60 | console.debug("migrating pipeline from v5 to v6"); 61 | migrateV6(draft, mockPaletteV7); 62 | } 63 | if (version < 7) { 64 | console.debug("migrating pipeline from v6 to v7"); 65 | migrateV7(draft, mockPaletteV7); 66 | } 67 | if (version < 8) { 68 | console.debug("migrating pipeline from v7 to v8"); 69 | migrateV8(draft); 70 | } 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV7/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import { ComponentNotFoundError } from "../errors"; 20 | import { mockPaletteV7 } from "../utils"; 21 | import rawMigrate from "./"; 22 | 23 | // wrap migrate functions in immer 24 | const migrate = produce((f: any, p: any) => rawMigrate(f, p)); 25 | 26 | it("should error with no palette", () => { 27 | const v6 = { 28 | pipelines: [ 29 | { 30 | app_data: { 31 | name: "name", 32 | runtime_type: "KUBEFLOW_PIPELINES", 33 | version: 6, 34 | }, 35 | nodes: [ 36 | { 37 | type: "execution_node", 38 | op: "elyra-kfp-examples-catalog:61e6f4141f65", 39 | }, 40 | ], 41 | }, 42 | ], 43 | }; 44 | 45 | expect(() => migrate(v6)).toThrow(ComponentNotFoundError); 46 | }); 47 | 48 | it("should update property format for OneOfControl", () => { 49 | const component_parameters = { 50 | data: { 51 | value: "parent-id", 52 | option: "output_name", 53 | }, 54 | hash_algorithm: "some string", 55 | }; 56 | 57 | const new_component_parameters = { 58 | data: { 59 | value: "parent-id", 60 | option: "output_name", 61 | }, 62 | hash_algorithm: { 63 | activeControl: "StringControl", 64 | StringControl: "some string", 65 | }, 66 | }; 67 | 68 | const v6 = { 69 | pipelines: [ 70 | { 71 | app_data: { 72 | name: "name", 73 | runtime_type: "KUBEFLOW_PIPELINES", 74 | version: 6, 75 | }, 76 | nodes: [ 77 | { 78 | type: "execution_node", 79 | op: "elyra-kfp-examples-catalog:d68ec7fcdf46", 80 | app_data: { 81 | component_parameters, 82 | }, 83 | }, 84 | ], 85 | }, 86 | ], 87 | }; 88 | 89 | const actual = migrate(v6, mockPaletteV7); 90 | expect(actual.pipelines[0].nodes[0].app_data.component_parameters).toEqual( 91 | new_component_parameters 92 | ); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV7/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ComponentNotFoundError } from "../errors"; 18 | import { opMap } from "../migrateV6"; 19 | 20 | function migrate(pipelineFlow: any, palette: any) { 21 | let paletteNodes: any[] = []; 22 | for (const c of palette?.categories || []) { 23 | if (c.node_types) { 24 | paletteNodes.push(...c.node_types); 25 | } 26 | } 27 | const validOps = Object.values(opMap); 28 | if ( 29 | pipelineFlow.pipelines[0].app_data.runtime_type === "KUBEFLOW_PIPELINES" 30 | ) { 31 | for (const pipeline of pipelineFlow.pipelines) { 32 | for (const node of pipeline.nodes) { 33 | // Only run on valid node ops as migrated in v6 34 | if (validOps.includes(node.op)) { 35 | // update format of inputvalue properties to using OneOfControl format 36 | const nodePropertiesSchema = paletteNodes.find( 37 | (n: any) => n.op === node.op 38 | ); 39 | if (nodePropertiesSchema === undefined) { 40 | throw new ComponentNotFoundError(); 41 | } 42 | const propertyDefs = 43 | nodePropertiesSchema.app_data.properties.uihints.parameter_info; 44 | Object.keys(node.app_data.component_parameters ?? {}).forEach( 45 | (key) => { 46 | const propDef = propertyDefs.find( 47 | (p: any) => p.parameter_ref === "elyra_" + key 48 | ); 49 | if (propDef?.custom_control_id === "OneOfControl") { 50 | const activeControl: string = 51 | Object.keys(propDef.data.controls).find( 52 | (c: string) => c !== "NestedEnumControl" 53 | ) || ""; 54 | node.app_data.component_parameters[key] = { 55 | activeControl, 56 | [activeControl]: node.app_data.component_parameters[key], 57 | }; 58 | } 59 | } 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | 66 | pipelineFlow.pipelines[0].app_data.version = 7; 67 | 68 | return pipelineFlow; 69 | } 70 | 71 | export default migrate; 72 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/NodeTooltip/index.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { render } from "../test-utils"; 18 | import NodeTooltip from "./"; 19 | 20 | it("renders one item", () => { 21 | const { container } = render( 22 | 26 | ); 27 | expect(container.firstChild).toHaveTextContent(/label/i); 28 | expect(container.firstChild).toHaveTextContent(/some value/i); 29 | expect(container.firstChild).toHaveTextContent(/component type/i); 30 | }); 31 | 32 | it("renders multiple items", () => { 33 | const { container } = render( 34 | 41 | ); 42 | 43 | expect(container.firstChild).toHaveTextContent(/label/i); 44 | expect(container.firstChild).toHaveTextContent(/some value/i); 45 | expect(container.firstChild).toHaveTextContent(/component type/i); 46 | 47 | expect(container.firstChild).toHaveTextContent(/array/i); 48 | expect(container.firstChild).toHaveTextContent(/one/i); 49 | expect(container.firstChild).toHaveTextContent(/two/i); 50 | }); 51 | 52 | it("renders with errors", () => { 53 | const { container } = render( 54 | 59 | ); 60 | 61 | expect(container.firstChild).toHaveTextContent(/error/i); 62 | expect(container.firstChild).toHaveTextContent(/this is an error/i); 63 | expect(container.firstChild).toHaveTextContent(/component type/i); 64 | }); 65 | 66 | it("renders with errors and properties", () => { 67 | const { container } = render( 68 | 72 | ); 73 | 74 | expect(container.firstChild).toHaveTextContent(/error/i); 75 | expect(container.firstChild).toHaveTextContent(/this is an error/i); 76 | 77 | expect(container.firstChild).toHaveTextContent(/label/i); 78 | expect(container.firstChild).toHaveTextContent(/some value/i); 79 | }); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "pipeline-editor", 4 | "description": "", 5 | "version": "1.12.1", 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ] 10 | }, 11 | "main": "./packages/extension/dist/index.js", 12 | "devDependencies": { 13 | "@babel/core": "^7.13.8", 14 | "@storybook/addon-actions": "^6.4.9", 15 | "@storybook/addon-essentials": "^6.4.9", 16 | "@storybook/addon-links": "^6.4.9", 17 | "@storybook/react": "^6.4.9", 18 | "@testing-library/cypress": "^7.0.4", 19 | "@types/jest": "^26.0.20", 20 | "@typescript-eslint/eslint-plugin": "^4.5.0", 21 | "@typescript-eslint/parser": "^4.5.0", 22 | "babel-eslint": "^10.0.0", 23 | "babel-loader": "^8.2.2", 24 | "core-js": "^3.9.1", 25 | "cypress": "^9.2.1", 26 | "eslint": "^7.11.0", 27 | "eslint-config-react-app": "^6.0.0", 28 | "eslint-plugin-cypress": "^2.11.2", 29 | "eslint-plugin-flowtype": "^5.2.0", 30 | "eslint-plugin-header": "^3.1.0", 31 | "eslint-plugin-import": "^2.22.1", 32 | "eslint-plugin-jest": "^24.1.0", 33 | "eslint-plugin-jest-dom": "^3.6.5", 34 | "eslint-plugin-jsx-a11y": "^6.3.1", 35 | "eslint-plugin-react": "^7.21.5", 36 | "eslint-plugin-react-hooks": "^4.2.0", 37 | "eslint-plugin-testing-library": "^3.10.1", 38 | "husky": "^3.1.0", 39 | "jest": "^26.6.3", 40 | "lerna": "^3.22.1", 41 | "lint-staged": "^9.4.3", 42 | "microbundle": "^0.13.0", 43 | "prettier": "^2.2.1", 44 | "start-server-and-test": "^1.12.0", 45 | "ts-jest": "^26.5.1", 46 | "typescript": "4.1.3" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "lint-staged": { 54 | "**/*.{tsx,ts,js,md,css,html,json}": [ 55 | "prettier --write", 56 | "git add" 57 | ] 58 | }, 59 | "scripts": { 60 | "clean": "lerna run clean; rm -rf node_modules", 61 | "build": "lerna run build", 62 | "link-all": "lerna run link", 63 | "watch": "FORCE_COLOR=true lerna run watch --parallel --stream", 64 | "sb:start": "start-storybook -p 6006", 65 | "sb:ci": "start-storybook -p 6006 --ci", 66 | "test": "jest", 67 | "test:cover": "jest --coverage", 68 | "test:cypress:dev": "start-server-and-test sb:ci http://localhost:6006 cy:open", 69 | "test:cypress": "start-server-and-test sb:ci http://localhost:6006 cy:run", 70 | "cy:run": "cypress run", 71 | "cy:open": "cypress open", 72 | "lint": "eslint . --ignore-path .gitignore --ext .ts,.tsx,.js,.jsx --max-warnings=0", 73 | "lint:fix": "eslint . --fix --ignore-path .gitignore --ext .ts,.tsx,.js,.jsx", 74 | "format": "prettier --ignore-path .gitignore --write \"**/*.{tsx,ts,js,css,html,json}\"", 75 | "format:check": "prettier --ignore-path .gitignore --check \"**/*.{tsx,ts,js,css,html,json}\"", 76 | "preinstall": "npx force-resolutions -y" 77 | }, 78 | "dependencies": {}, 79 | "resolutions": { 80 | "immer": "9.0.7", 81 | "prismjs": "1.25.0", 82 | "trim": "0.0.3" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/PipelineEditor/index.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | render, 19 | screen, 20 | nodeSpec, 21 | samplePipeline, 22 | createPalette, 23 | } from "../test-utils"; 24 | import PipelineEditor from "./"; 25 | 26 | it("shows custom empty component for undefined pipeline", () => { 27 | render( 28 | custom empty message 29 | ); 30 | expect(screen.getByText(/custom empty message/i)).toBeInTheDocument(); 31 | }); 32 | 33 | it("shows custom empty component for null pipeline", () => { 34 | render(custom empty message); 35 | expect(screen.getByText(/custom empty message/i)).toBeInTheDocument(); 36 | }); 37 | 38 | it("renders a pipeline with two nodes", () => { 39 | const { container } = render(); 40 | 41 | expect(container.getElementsByClassName("d3-node-group")).toHaveLength(2); 42 | }); 43 | 44 | it("renders a pipeline with two nodes in readOnly mode", () => { 45 | const { container } = render( 46 | 47 | ); 48 | 49 | expect(container.getElementsByClassName("d3-node-group")).toHaveLength(2); 50 | }); 51 | 52 | it("throws error for invalid pipeline json", () => { 53 | const handleError = jest.fn(); 54 | render(); 55 | 56 | expect(handleError).toHaveBeenCalledTimes(1); 57 | expect(handleError).toHaveBeenCalledWith(expect.any(Error)); 58 | }); 59 | 60 | it("renders", () => { 61 | const handleError = jest.fn(); 62 | render( 63 | 68 | ); 69 | expect(handleError).not.toHaveBeenCalled(); 70 | }); 71 | 72 | it("can add node through imperative handle", async () => { 73 | let handle: any; 74 | const { container } = render( 75 | (handle = r)} 77 | pipeline={samplePipeline} 78 | palette={createPalette([nodeSpec as any])} 79 | /> 80 | ); 81 | 82 | expect(container.getElementsByClassName("d3-node-group")).toHaveLength(2); 83 | 84 | handle.addFile({ 85 | nodeTemplate: { op: "execute-notebook-node" }, 86 | path: "example.ipynb", 87 | }); 88 | 89 | expect(container.getElementsByClassName("d3-node-group")).toHaveLength(3); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/SplitPanelLayout/index.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { fireEvent, render, screen } from "../test-utils"; 18 | import SplitPanelLayout from "./"; 19 | 20 | it("renders only left panel when closed", () => { 21 | render( 22 | left panel
} 24 | right={
right panel
} 25 | mode="closed" 26 | /> 27 | ); 28 | 29 | expect(screen.getByText(/left panel/i)).toBeInTheDocument(); 30 | expect(screen.queryByText(/right panel/i)).not.toBeInTheDocument(); 31 | }); 32 | 33 | it("renders both panels when open", () => { 34 | render( 35 | left panel} 37 | right={
right panel
} 38 | mode="open" 39 | /> 40 | ); 41 | 42 | expect(screen.getByText(/left panel/i)).toBeInTheDocument(); 43 | expect(screen.getByText(/right panel/i)).toBeInTheDocument(); 44 | }); 45 | 46 | it("renders both panels when collapsed", () => { 47 | render( 48 | left panel} 50 | right={
right panel
} 51 | mode="collapsed" 52 | /> 53 | ); 54 | 55 | expect(screen.getByText(/left panel/i)).toBeInTheDocument(); 56 | expect(screen.getByText(/right panel/i)).toBeInTheDocument(); 57 | }); 58 | 59 | it("should adjust width when dragged", () => { 60 | render( 61 | left panel} 63 | right={
right panel
} 64 | mode="open" 65 | /> 66 | ); 67 | 68 | const before = screen.getByTestId("drag-handle").style.right; 69 | 70 | fireEvent.mouseDown(screen.getByTestId("drag-handle")); 71 | fireEvent.mouseMove(screen.getByTestId("drag-handle"), { clientX: 0 }); 72 | fireEvent.mouseUp(screen.getByTestId("drag-handle")); 73 | 74 | const after = screen.getByTestId("drag-handle").style.right; 75 | 76 | expect(after).not.toBe(before); 77 | }); 78 | 79 | it("should not adjust width when handle has not been clicked", () => { 80 | render( 81 | left panel} 83 | right={
right panel
} 84 | mode="open" 85 | /> 86 | ); 87 | 88 | const before = screen.getByTestId("drag-handle").style.right; 89 | 90 | fireEvent.mouseDown(screen.getByTestId("drag-handle")); 91 | fireEvent.mouseUp(screen.getByTestId("drag-handle")); 92 | 93 | fireEvent.mouseMove(screen.getByTestId("drag-handle"), { clientX: 0 }); 94 | 95 | const after = screen.getByTestId("drag-handle").style.right; 96 | 97 | expect(after).toBe(before); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/ThemeProvider/utils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { deepmerge } from "./utils"; 18 | 19 | describe("deepmerge", () => { 20 | it("returns target when source is empty", () => { 21 | const target = { 22 | one: { 23 | two: { 24 | three: "x", 25 | }, 26 | }, 27 | }; 28 | 29 | const source = {}; 30 | 31 | const actual = deepmerge(target, source); 32 | 33 | expect(actual).toEqual(target); 34 | }); 35 | 36 | it("adds missing objects", () => { 37 | interface Target { 38 | one: { 39 | two: { 40 | three: string; 41 | }; 42 | }; 43 | four?: { 44 | five?: string; 45 | six?: string; 46 | }; 47 | } 48 | 49 | const target: Target = { 50 | one: { 51 | two: { 52 | three: "x", 53 | }, 54 | }, 55 | }; 56 | 57 | const source = { 58 | four: { 59 | five: "y", 60 | six: "z", 61 | }, 62 | }; 63 | 64 | const expected = { 65 | one: { 66 | two: { 67 | three: "x", 68 | }, 69 | }, 70 | four: { 71 | five: "y", 72 | six: "z", 73 | }, 74 | }; 75 | 76 | const actual = deepmerge(target, source); 77 | 78 | expect(actual).toEqual(expected); 79 | }); 80 | 81 | it("updates values", () => { 82 | const target = { 83 | one: { 84 | two: { 85 | three: "x", 86 | }, 87 | }, 88 | }; 89 | 90 | const source = { 91 | one: { 92 | two: { 93 | three: "y", 94 | }, 95 | }, 96 | }; 97 | 98 | const expected = { 99 | one: { 100 | two: { 101 | three: "y", 102 | }, 103 | }, 104 | }; 105 | 106 | const actual = deepmerge(target, source); 107 | 108 | expect(actual).toEqual(expected); 109 | }); 110 | 111 | it("doesn't update if undefined", () => { 112 | const target = { 113 | one: { 114 | two: { 115 | three: "x", 116 | }, 117 | }, 118 | }; 119 | 120 | const source = { 121 | one: { 122 | two: { 123 | three: undefined, 124 | }, 125 | }, 126 | }; 127 | 128 | const expected = { 129 | one: { 130 | two: { 131 | three: "x", 132 | }, 133 | }, 134 | }; 135 | 136 | const actual = deepmerge(target, source); 137 | 138 | expect(actual).toEqual(expected); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | # Contributing 20 | 21 | Welcome to Elyra! If you are interested in contributing to the [Elyra code repo](README.md) 22 | then checkout the [Contributor's Guide](https://github.com/elyra-ai/community/blob/main/CONTRIBUTING.md) and 23 | the [Code of Conduct](https://github.com/elyra-ai/community/blob/main/CODE_OF_CONDUCT.md). 24 | 25 | The [Elyra community repo](https://github.com/elyra-ai/community) contains information on how the community 26 | is organized and other information that is pertinent to contributing. 27 | 28 | ## Community Roles 29 | 30 | ### Contributors 31 | 32 | Contributors are Members who are active/inactive contributors to the community. They can have issues and Pull Requests assigned to them. Contributors can be active in many ways including but not limited to: 33 | 34 | - Authoring or reviewing PRs on GitHub 35 | - Filing or commenting on issues on GitHub 36 | - Contributing community discussions (e.g. Slack, meetings, Gitter, email discussion forums, Stack Overflow, etc) 37 | - Creator of content, promotion and advocating 38 | 39 | Active Contributors are defined as anyone from the community with twenty (20) or more measurable contributions 40 | during the previous 12-month period, inclusive of code merged, code reviews performed, documentation page edits, 41 | (creation, modification, comments or attachments) 42 | 43 | ### Committers 44 | 45 | Contributors who give frequent and valuable contributions to a subproject of the Project can have their 46 | status promoted to that of a Committer for that subproject. A Committer has write access to the source code 47 | repository and gains voting rights allowing them to affect the future of the subproject. 48 | 49 | In order for a Contributor to become a Committer, another Committer can nominate that Contributor or the Contributor can ask for it. 50 | Once a Contributor is nominated, all Committers will be asked to vote. If there are at least 3 positive votes and no negative 51 | votes, the Contributor is converted into a Committer and given write access to the source code repository. 52 | 53 | ## Committers 54 | 55 | | Name | Company | GitHub ID | 56 | | :-------------: | :-----: | :-----------: | 57 | | Alan Chin | IBM | akchinSTC | 58 | | Alex Bozarth | IBM | ajbozarth | 59 | | Karla Spuldaro | IBM | karlaspuldaro | 60 | | Luciano Resende | Apple | lresende | 61 | | Martha Cryan | IBM | marthacryan | 62 | | Nick Bourdakos | | bourdakos1 | 63 | 64 | ## Contributors 65 | 66 | | Name | GitHub ID | 67 | | :-----------: | :----------: | 68 | | Rachael House | rachaelhouse | 69 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV5/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import rawMigrate from "./"; 20 | 21 | // wrap migrate functions in immer 22 | const migrate = produce((d: any) => rawMigrate(d)); 23 | 24 | it("should update old op name to new op name", () => { 25 | const v4 = { 26 | pipelines: [ 27 | { 28 | app_data: { 29 | name: "name", 30 | version: 4, 31 | }, 32 | nodes: [ 33 | { 34 | type: "execution_node", 35 | op: "run-notebook-using-papermill", 36 | app_data: {}, 37 | }, 38 | ], 39 | }, 40 | ], 41 | }; 42 | 43 | const actual = migrate(v4); 44 | expect(actual.pipelines[0].nodes[0].op).toEqual( 45 | "run_notebook_using_papermill_Runnotebookusingpapermill" 46 | ); 47 | }); 48 | 49 | it("should not update op name if already new op name", () => { 50 | const v4 = { 51 | pipelines: [ 52 | { 53 | app_data: { 54 | name: "name", 55 | version: 4, 56 | }, 57 | nodes: [ 58 | { 59 | type: "execution_node", 60 | op: "filter_text_using_shell_and_grep_Filtertext", 61 | app_data: {}, 62 | }, 63 | ], 64 | }, 65 | ], 66 | }; 67 | 68 | const actual = migrate(v4); 69 | expect(actual.pipelines[0].nodes[0].op).toEqual( 70 | "filter_text_using_shell_and_grep_Filtertext" 71 | ); 72 | }); 73 | 74 | it("should not update op name if not in update list", () => { 75 | const v4 = { 76 | pipelines: [ 77 | { 78 | app_data: { 79 | name: "name", 80 | version: 4, 81 | }, 82 | nodes: [ 83 | { 84 | type: "execution_node", 85 | op: "some_op_name", 86 | app_data: {}, 87 | }, 88 | ], 89 | }, 90 | ], 91 | }; 92 | 93 | const actual = migrate(v4); 94 | expect(actual.pipelines[0].nodes[0].op).toEqual("some_op_name"); 95 | }); 96 | 97 | it("should not error if op not set", () => { 98 | const v4 = { 99 | pipelines: [ 100 | { 101 | app_data: { 102 | name: "name", 103 | version: 4, 104 | }, 105 | nodes: [ 106 | { 107 | type: "execution_node", 108 | app_data: {}, 109 | }, 110 | ], 111 | }, 112 | ], 113 | }; 114 | 115 | const actual = migrate(v4); 116 | expect(actual.pipelines[0].nodes[0].op).toBeUndefined(); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/CustomFieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { FieldTemplateProps } from "@rjsf/core"; 18 | 19 | export const CustomFieldTemplate: React.FC = (props) => { 20 | let children = props.children; 21 | if (props.uiSchema["ui:field"] === "hidden") { 22 | return
; 23 | } 24 | if (props.uiSchema["ui:readonly"]) { 25 | children = ( 26 |
27 | {props.formData ?? props.schema.default} 28 |
29 | ); 30 | } 31 | if ( 32 | props.schema.uniqueItems && 33 | (props.schema.items as any)?.enum?.length === 0 34 | ) { 35 | children = ( 36 |
37 | No pipeline parameters are defined. 38 |
39 | ); 40 | } else if (props.schema.uniqueItems && props.formData) { 41 | const filteredItems = props.formData.filter((item: any) => { 42 | return (props.schema.items as any)?.enum?.includes(item); 43 | }); 44 | if (filteredItems.length !== props.formData.length) { 45 | props.onChange(filteredItems); 46 | } 47 | } 48 | const requiredError = props.required && props.formData === undefined; 49 | const hasError = props.rawErrors || requiredError; 50 | return ( 51 |
59 | {hasError &&
} 60 | {props.schema.title !== undefined && props.schema.title !== " " ? ( 61 |
66 | 69 | {props.schema.description && ( 70 |
71 |
?
72 |

77 | {props.schema.description} 78 |

79 |
80 | )} 81 |
82 | ) : undefined} 83 | {children} 84 | {props.errors} 85 | {requiredError &&
  • is a required property
  • } 86 |
    87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/components.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import styled from "styled-components"; 18 | 19 | export const EnumButton = styled.button.attrs({ type: "button" })` 20 | /* higher specificity to override button styles */ 21 | && { 22 | background-color: ${({ theme }) => theme.palette.secondary.main}; 23 | color: ${({ theme }) => theme.palette.secondary.contrastText}; 24 | border: 1px solid ${({ theme }) => theme.palette.inputBorder}; 25 | display: flex; 26 | width: 100%; 27 | height: 26px; 28 | align-items: center; 29 | justify-content: space-between; 30 | padding: 2px 8px; 31 | border-radius: ${({ theme }) => theme.shape.borderRadius}; 32 | } 33 | 34 | &&:hover { 35 | background-color: ${({ theme }) => theme.palette.secondary.main}; 36 | border: 1px solid ${({ theme }) => theme.palette.highlight.hover}; 37 | border-radius: ${({ theme }) => theme.shape.borderRadius}; 38 | } 39 | 40 | &&:focus { 41 | border: 1px solid ${({ theme }) => theme.palette.focus}; 42 | border-radius: ${({ theme }) => theme.shape.borderRadius}; 43 | } 44 | `; 45 | 46 | export const EnumLabel = styled.div` 47 | white-space: nowrap; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | `; 51 | 52 | export const EnumIcon = styled.div` 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | `; 57 | 58 | export const EnumMenu = styled.ul` 59 | z-index: 10; 60 | position: absolute; 61 | top: 26px; 62 | left: 0; 63 | right: 0; 64 | color: ${({ theme }) => theme.palette.secondary.contrastText}; 65 | background-color: ${({ theme }) => theme.palette.background.secondary}; 66 | padding: 2px; 67 | padding-bottom: 4px; 68 | margin: 0; 69 | list-style-type: none; 70 | border-radius: ${({ theme }) => theme.shape.borderRadius}; 71 | `; 72 | 73 | export const EnumMenuItem = styled.li` 74 | cursor: pointer; 75 | height: 18px; 76 | line-height: 18px; 77 | padding-left: 3.5px; 78 | color: ${({ theme }) => theme.palette.text.primary}; 79 | 80 | &:hover { 81 | background-color: ${({ theme }) => theme.palette.hover}; 82 | border-radius: ${({ theme }) => theme.shape.borderRadius}; 83 | } 84 | `; 85 | 86 | export const EnumContainer = styled.div<{ isOpen: boolean }>` 87 | position: relative; 88 | margin-top: 9px; 89 | width: 100%; 90 | max-width: 320px; 91 | 92 | & ${EnumButton}, & ${EnumMenu} { 93 | border: 1px solid ${({ theme }) => theme.palette.inputBorder}; 94 | border-radius: ${({ theme }) => theme.shape.borderRadius}; 95 | } 96 | 97 | & ${EnumMenu} { 98 | display: ${({ isOpen }) => (isOpen ? "block" : "none")}; 99 | } 100 | `; 101 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV8/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const widgetMap: { [key: string]: string } = { 18 | BooleanControl: "boolean", 19 | NumberControl: "number", 20 | NestedEnumControl: "inputpath", 21 | StringControl: "string", 22 | }; 23 | 24 | const regexMap: { [key: string]: RegExp } = { 25 | env_vars: /(?\w+)=?(?[^,]*?)(?= \w+=|$)/, 26 | mounted_volumes: /(?[\w/]+)=?(?[^,]*?)(?= \w+=|$)/, 27 | kubernetes_pod_annotations: /(?\w+)=?(?[^,]*?)(?= \w+=|$)/, 28 | kubernetes_secrets: /(?\w+)=?(?[^:,\n\]]*):?(?[^,]*?)(?= \w+=|$)/, 29 | kubernetes_tolerations: /\w+=(?.*?):(?[^,]*?):(?[^,]*?):(?[^,]*?)(?= \w+=|$)/, 30 | }; 31 | 32 | function migrate(pipelineFlow: any) { 33 | for (const pipeline of pipelineFlow.pipelines) { 34 | Object.keys(pipeline.app_data?.properties?.pipeline_defaults ?? {}).forEach( 35 | (key) => { 36 | // Update KeyValue arrays to dict arrays 37 | if (Object.keys(regexMap).includes(key)) { 38 | const new_items: any[] = []; 39 | for (const item of pipeline.app_data.properties.pipeline_defaults[ 40 | key 41 | ]) { 42 | const dict = item.match(regexMap[key]).groups; 43 | new_items.push(dict); 44 | } 45 | pipeline.app_data.properties.pipeline_defaults[key] = new_items; 46 | } 47 | } 48 | ); 49 | 50 | for (const node of pipeline.nodes) { 51 | Object.keys(node.app_data?.component_parameters ?? {}).forEach((key) => { 52 | // Update oneOf format 53 | const activeControl = 54 | node.app_data.component_parameters[key]?.activeControl; 55 | if (activeControl) { 56 | node.app_data.component_parameters[key] = { 57 | widget: widgetMap[activeControl], 58 | value: node.app_data.component_parameters[key][activeControl], 59 | }; 60 | } 61 | // Update inputpath format 62 | const propKeys = Object.keys( 63 | node.app_data.component_parameters[key] ?? {} 64 | ); 65 | if ( 66 | propKeys.length === 2 && 67 | propKeys.includes("option") && 68 | propKeys.includes("value") 69 | ) { 70 | node.app_data.component_parameters[key] = { 71 | widget: "inputpath", 72 | value: node.app_data.component_parameters[key], 73 | }; 74 | } 75 | // Update KeyValue arrays to dict arrays 76 | if ( 77 | Object.keys(regexMap).includes(key) && 78 | Array.isArray(node.app_data.component_parameters[key]) 79 | ) { 80 | const new_items: any[] = []; 81 | for (const item of node.app_data.component_parameters[key]) { 82 | const dict = item.match(regexMap[key]).groups; 83 | new_items.push(dict); 84 | } 85 | node.app_data.component_parameters[key] = new_items; 86 | } 87 | }); 88 | } 89 | } 90 | 91 | pipelineFlow.pipelines[0].app_data.version = 8; 92 | 93 | return pipelineFlow; 94 | } 95 | 96 | export default migrate; 97 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/ThemeProvider/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, { useMemo } from "react"; 18 | 19 | import { DeepPartial } from "redux"; 20 | import { ThemeProvider as StyledThemeProvider } from "styled-components"; 21 | 22 | import { Theme } from "../types"; 23 | import { CanvasOverrides } from "./styles"; 24 | import useSystemInfo from "./useSystemInfo"; 25 | import { deepmerge } from "./utils"; 26 | 27 | const defaultTheme: Omit = { 28 | palette: { 29 | focus: "#528bff", 30 | border: "#181a1f", 31 | divider: "rgba(128, 128, 128, 0.35)", 32 | hover: "#2c313a", 33 | active: "rgba(255, 255, 255, 0.18)", 34 | tabBorder: "#e7e7e7", 35 | inputBorder: "transparent", 36 | sash: "transparent", 37 | primary: { 38 | main: "#4d78cc", 39 | hover: "#6087cf", 40 | contrastText: "#fff", 41 | }, 42 | secondary: { 43 | main: "#353b45", 44 | contrastText: "#f0f0f0", 45 | }, 46 | error: { 47 | main: "#be1100", 48 | contrastText: "#fff", 49 | }, 50 | errorMessage: { 51 | main: "#be1100", 52 | contrastText: "#fff", 53 | errorBorder: "#be1100", 54 | }, 55 | text: { 56 | icon: "#c5c5c5", 57 | primary: "#cccccc", 58 | secondary: "#abb2bf", 59 | bold: "#e7e7e7", 60 | inactive: "rgba(231, 231, 231, 0.6)", 61 | disabled: "rgba(215, 218, 224, 0.25)", 62 | link: "#3794ff", 63 | error: "#f48771", 64 | }, 65 | background: { 66 | default: "#282c34", 67 | secondary: "#21252b", 68 | input: "#1b1d23", 69 | }, 70 | highlight: { 71 | border: "rgba(255, 255, 255, 0.12)", 72 | hover: "rgba(128, 128, 128, 0.07)", 73 | focus: "rgba(128, 128, 128, 0.14)", 74 | }, 75 | }, 76 | shape: { 77 | borderRadius: "0px", 78 | }, 79 | typography: { 80 | fontFamily: "-apple-system, system-ui, sans-serif", 81 | fontWeight: "normal", 82 | fontSize: "13px", 83 | }, 84 | }; 85 | 86 | function mergeThemes(systemInfo: { 87 | mode: "dark" | "light"; 88 | platform: "mac" | "win" | "other"; 89 | }) { 90 | return (overides: Partial): Theme => { 91 | return deepmerge( 92 | { ...defaultTheme, ...systemInfo }, 93 | overides as DeepPartial 94 | ); 95 | }; 96 | } 97 | 98 | const ThemeProvider: React.FC<{ theme: DeepPartial }> = ({ 99 | theme, 100 | children, 101 | }) => { 102 | return ( 103 | {children} 104 | ); 105 | }; 106 | 107 | export const InternalThemeProvider: React.FC = ({ children }) => { 108 | const systemInfo = useSystemInfo(); 109 | const theme = useMemo(() => mergeThemes(systemInfo), [systemInfo]); 110 | 111 | return ( 112 | 113 | 114 | {children} 115 | 116 | ); 117 | }; 118 | 119 | export function createTheme(theme: DeepPartial) { 120 | return theme; 121 | } 122 | 123 | export default ThemeProvider; 124 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/check-circular-references/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const TIMEOUT = 3000; 18 | 19 | interface Link { 20 | id: string; 21 | trgNodeId: string; 22 | srcNodeId: string; 23 | type: string; 24 | } 25 | 26 | export function checkCircularReferences(links: Link[]) { 27 | const startTime = Date.now(); 28 | 29 | // filter out comment links. 30 | links = links.filter((l) => l.type !== "commentLink"); 31 | 32 | // organize links into easily indexable map: 33 | // {srcNodeId: link[]} 34 | const linkMap: { [key: string]: Link[] } = {}; 35 | for (const l of links) { 36 | if (linkMap[l.srcNodeId] === undefined) { 37 | linkMap[l.srcNodeId] = []; 38 | } 39 | linkMap[l.srcNodeId].push(l); 40 | } 41 | 42 | let orderedChain: string[] = []; 43 | const taintedLinks = new Set(); 44 | const seen = new Set(); 45 | const stack: Link[] = []; 46 | for (const l of links) { 47 | if (seen.has(l.id)) { 48 | continue; 49 | } 50 | seen.add(l.id); 51 | orderedChain = []; 52 | orderedChain.push(l.id); 53 | const linksToVisit = linkMap[l.trgNodeId]; 54 | if (linksToVisit === undefined) { 55 | continue; 56 | } 57 | stack.push(...linksToVisit); 58 | const forkStack: number[] = []; 59 | const chainLength = orderedChain.length; 60 | forkStack.push(...linksToVisit.map(() => chainLength)); 61 | 62 | while (stack.length > 0 && Date.now() - startTime < TIMEOUT) { 63 | forkStack.pop(); 64 | const l = stack.pop()!; // we should be gauranteed an item. 65 | seen.add(l.id); 66 | const seenLinkIndex = orderedChain.indexOf(l.id); 67 | 68 | // We hit a link we've already seen in the chain. This means there is a 69 | // cycle from the seen link to the end of the chain. 70 | if (seenLinkIndex > -1) { 71 | for (const item of orderedChain.slice(seenLinkIndex)) { 72 | taintedLinks.add(item); 73 | } 74 | 75 | // This is completely useless, but it makes me feel better that this is 76 | // here. 77 | const position = forkStack.slice(-1).pop(); 78 | if (position !== undefined) { 79 | orderedChain = orderedChain.slice(0, position); 80 | } 81 | // if position is undefined we will set orderedChain to [] 82 | continue; 83 | } 84 | 85 | orderedChain.push(l.id); 86 | 87 | const linksToVisit = linkMap[l.trgNodeId]; 88 | 89 | // We reached the end of a chain. 90 | if (linksToVisit === undefined) { 91 | // This is 100% necessary unlike the other example. 92 | const position = forkStack.slice(-1).pop(); 93 | if (position !== undefined) { 94 | orderedChain = orderedChain.slice(0, position); 95 | } 96 | // if position is undefined we will set orderedChain to [] 97 | continue; 98 | } 99 | 100 | // Uncharted teritory, add it to the stack to be explored. 101 | stack.push(...linksToVisit); 102 | const chainLength = orderedChain.length; 103 | forkStack.push(...linksToVisit.map(() => chainLength)); 104 | } 105 | } 106 | 107 | return [...taintedLinks]; 108 | } 109 | 110 | export default checkCircularReferences; 111 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/validators/number-validators.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Validator } from "."; 18 | 19 | export interface NumberValidatorOptions { 20 | type: "number" | "integer"; 21 | multipleOf?: number; 22 | minimum?: number; // for restricting numeric values 23 | maximum?: number; // for restricting numeric values 24 | exclusiveMinimum?: boolean | number; 25 | exclusiveMaximum?: boolean | number; 26 | required?: boolean; 27 | } 28 | 29 | export function getNumberValidators({ 30 | type, 31 | multipleOf, 32 | minimum, 33 | maximum, 34 | exclusiveMinimum, 35 | exclusiveMaximum, 36 | required, 37 | }: NumberValidatorOptions) { 38 | // JSON schema allows `exclusive min/max` to be a boolean to indicate min/max 39 | // are exclusive or a number. 40 | let exclusiveMax: number | undefined; 41 | let exclusiveMin: number | undefined; 42 | 43 | if (typeof exclusiveMaximum === "boolean") { 44 | exclusiveMax = exclusiveMaximum ? maximum : undefined; 45 | } else { 46 | exclusiveMax = exclusiveMaximum; 47 | } 48 | 49 | if (typeof exclusiveMinimum === "boolean") { 50 | exclusiveMin = exclusiveMinimum ? minimum : undefined; 51 | } else { 52 | exclusiveMin = exclusiveMinimum; 53 | } 54 | 55 | const isNumber = (value: T) => !isNaN(+value) && !isNaN(parseFloat(value)); 56 | 57 | const validators: Validator[] = [ 58 | { 59 | enabled: required === true, 60 | isValid: (value: T) => value !== "", 61 | }, 62 | { 63 | enabled: true, 64 | isValid: (value: T) => value === "" || isNumber(value), 65 | message: "Value must be a number.", 66 | }, 67 | { 68 | enabled: 69 | exclusiveMax !== undefined && 70 | (maximum === undefined || exclusiveMax <= maximum), 71 | isValid: (value: T) => +value < exclusiveMax!, 72 | message: `Value must be strictly less than ${exclusiveMax}.`, 73 | }, 74 | { 75 | enabled: 76 | exclusiveMin !== undefined && 77 | (minimum === undefined || exclusiveMin >= minimum), 78 | isValid: (value: T) => +value > exclusiveMin!, 79 | message: `Value must be strictly greater than ${exclusiveMin}.`, 80 | }, 81 | 82 | { 83 | enabled: 84 | maximum !== undefined && 85 | (exclusiveMax === undefined || exclusiveMax > maximum), 86 | isValid: (value: T) => +value <= maximum!, 87 | message: `Value must be less than or equal to ${maximum}.`, 88 | }, 89 | { 90 | enabled: 91 | minimum !== undefined && 92 | (exclusiveMin === undefined || exclusiveMin < minimum), 93 | isValid: (value: T) => +value >= minimum!, 94 | message: `Value must be greater than or equal to ${minimum}.`, 95 | }, 96 | { 97 | enabled: multipleOf !== undefined, 98 | isValid: (value: T) => +value % multipleOf! === 0, 99 | message: `Value must be a multiple of ${multipleOf}.`, 100 | }, 101 | { 102 | enabled: type === "integer", 103 | // check if the string includes a decimal to prevent something like "10.0" 104 | isValid: (value: T) => +value % 1 === 0 && !value.includes("."), 105 | message: "Value must be an integer.", 106 | }, 107 | ]; 108 | 109 | return validators; 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018-2023 Elyra Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | name: Validate 17 | 18 | on: [push, pull_request] 19 | 20 | env: 21 | FORCE_COLOR: true 22 | 23 | jobs: 24 | prepare-yarn-cache: 25 | name: Prepare Cache 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v2 30 | with: 31 | node-version: "*" 32 | - uses: actions/cache@v2 33 | with: 34 | path: | 35 | node_modules 36 | */*/node_modules 37 | /home/runner/.cache/Cypress 38 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 39 | - name: Install 40 | run: yarn install --frozen-lockfile 41 | 42 | lint: 43 | name: Lint 44 | needs: prepare-yarn-cache 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: actions/setup-node@v2 49 | with: 50 | node-version: "*" 51 | - uses: actions/cache@v2 52 | with: 53 | path: | 54 | node_modules 55 | */*/node_modules 56 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 57 | - name: Install 58 | run: yarn install 59 | - name: Lint 60 | run: yarn lint 61 | - name: Check format 62 | run: yarn format:check 63 | 64 | test-coverage: 65 | name: Generate Coverage Report 66 | needs: prepare-yarn-cache 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v2 70 | - uses: actions/setup-node@v2 71 | with: 72 | node-version: "*" 73 | - uses: actions/cache@v2 74 | with: 75 | path: | 76 | node_modules 77 | */*/node_modules 78 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 79 | - name: Install 80 | run: yarn install 81 | - name: Build 82 | run: yarn build 83 | - name: Generate coverage report 84 | run: yarn test:cover 85 | - name: Upload coverage report 86 | uses: codecov/codecov-action@v1 87 | 88 | test: 89 | name: Test 90 | needs: prepare-yarn-cache 91 | runs-on: ubuntu-latest 92 | strategy: 93 | matrix: 94 | node-version: [15, 14, 12] 95 | steps: 96 | - uses: actions/checkout@v2 97 | - uses: actions/setup-node@v2 # Install dependencies with latest node version 98 | with: 99 | node-version: "*" 100 | - uses: actions/cache@v2 101 | with: 102 | path: | 103 | node_modules 104 | */*/node_modules 105 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 106 | - name: Install 107 | run: yarn install 108 | - uses: actions/setup-node@v2 109 | with: 110 | node-version: ${{ matrix.node-version }} 111 | - name: Build 112 | run: yarn build 113 | - name: Test 114 | run: yarn test 115 | 116 | test-integration: 117 | name: Run Integration Tests 118 | needs: prepare-yarn-cache 119 | runs-on: ubuntu-latest 120 | steps: 121 | - uses: actions/checkout@v2 122 | - uses: actions/setup-node@v2 123 | with: 124 | node-version: "*" 125 | - uses: actions/cache@v2 126 | with: 127 | path: | 128 | node_modules 129 | */*/node_modules 130 | /home/runner/.cache/Cypress 131 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 132 | - name: Install 133 | run: yarn install 134 | - name: Build 135 | run: yarn build 136 | - name: Cypress 137 | run: yarn test:cypress 138 | env: 139 | NODE_OPTIONS: --openssl-legacy-provider 140 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV1/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import rawMigrate from "./"; 20 | 21 | // wrap migrate functions in immer 22 | const migrate = produce((d: any) => rawMigrate(d)); 23 | 24 | it("should rename `title` key to `name`", () => { 25 | const v0 = { 26 | pipelines: [ 27 | { 28 | app_data: { 29 | title: "title", 30 | }, 31 | nodes: [], 32 | }, 33 | ], 34 | }; 35 | const expected = { 36 | pipelines: [ 37 | { 38 | app_data: { 39 | name: "title", 40 | version: 1, 41 | }, 42 | nodes: [], 43 | }, 44 | ], 45 | }; 46 | const actual = migrate(v0); 47 | expect(actual).toEqual(expected); 48 | }); 49 | 50 | it("should delete deprecated keys", () => { 51 | const v0 = { 52 | pipelines: [ 53 | { 54 | app_data: { 55 | title: "title", 56 | export: "export", 57 | export_format: "export_format", 58 | export_path: "export_path", 59 | }, 60 | nodes: [], 61 | }, 62 | ], 63 | }; 64 | const expected = { 65 | pipelines: [ 66 | { 67 | app_data: { 68 | name: "title", 69 | version: 1, 70 | }, 71 | nodes: [], 72 | }, 73 | ], 74 | }; 75 | const actual = migrate(v0); 76 | expect(actual).toEqual(expected); 77 | }); 78 | 79 | it("should rename all node keys", () => { 80 | const v0 = { 81 | pipelines: [ 82 | { 83 | app_data: { 84 | title: "title", 85 | }, 86 | nodes: [ 87 | { 88 | app_data: { 89 | artifact: "artifact", 90 | image: "image", 91 | vars: "vars", 92 | file_dependencies: "file_dependencies", 93 | recursive_dependencies: "recursive_dependencies", 94 | }, 95 | }, 96 | ], 97 | }, 98 | ], 99 | }; 100 | const expected = { 101 | pipelines: [ 102 | { 103 | app_data: { 104 | name: "title", 105 | version: 1, 106 | }, 107 | nodes: [ 108 | { 109 | app_data: { 110 | filename: "artifact", 111 | runtime_image: "image", 112 | env_vars: "vars", 113 | dependencies: "file_dependencies", 114 | include_subdirectories: "recursive_dependencies", 115 | }, 116 | op: "execute-notebook-node", 117 | }, 118 | ], 119 | }, 120 | ], 121 | }; 122 | const actual = migrate(v0); 123 | expect(actual).toEqual(expected); 124 | }); 125 | 126 | it("should handle missing app_data for pipeline", () => { 127 | const v0 = { 128 | pipelines: [ 129 | { 130 | nodes: [], 131 | }, 132 | ], 133 | }; 134 | const expected = { 135 | pipelines: [ 136 | { 137 | app_data: { 138 | version: 1, 139 | }, 140 | nodes: [], 141 | }, 142 | ], 143 | }; 144 | const actual = migrate(v0); 145 | expect(actual).toEqual(expected); 146 | }); 147 | 148 | it("should handle missing app_data for nodes", () => { 149 | const v0 = { 150 | pipelines: [ 151 | { 152 | app_data: { 153 | title: "title", 154 | }, 155 | nodes: [{}], 156 | }, 157 | ], 158 | }; 159 | const expected = { 160 | pipelines: [ 161 | { 162 | app_data: { 163 | name: "title", 164 | version: 1, 165 | }, 166 | nodes: [ 167 | { 168 | op: "execute-notebook-node", 169 | }, 170 | ], 171 | }, 172 | ], 173 | }; 174 | const actual = migrate(v0); 175 | expect(actual).toEqual(expected); 176 | }); 177 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const allExtensions = [".ts", ".tsx", ".d.ts", ".js", ".jsx"]; 18 | 19 | module.exports = { 20 | root: true, 21 | extends: [ 22 | "react-app", 23 | "plugin:jest/recommended", 24 | "plugin:jest/style", 25 | "plugin:testing-library/react", 26 | "plugin:jest-dom/recommended", 27 | ], 28 | plugins: ["import", "header"], 29 | rules: { 30 | "testing-library/prefer-screen-queries": ["warn"], 31 | "jest/expect-expect": ["off"], 32 | "jest/valid-title": ["off"], 33 | "header/header": [ 34 | "warn", 35 | "block", 36 | [ 37 | "", 38 | " * Copyright 2018-2023 Elyra Authors", 39 | " *", 40 | ' * Licensed under the Apache License, Version 2.0 (the "License");', 41 | " * you may not use this file except in compliance with the License.", 42 | " * You may obtain a copy of the License at", 43 | " *", 44 | " * http://www.apache.org/licenses/LICENSE-2.0", 45 | " *", 46 | " * Unless required by applicable law or agreed to in writing, software", 47 | ' * distributed under the License is distributed on an "AS IS" BASIS,', 48 | " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", 49 | " * See the License for the specific language governing permissions and", 50 | " * limitations under the License.", 51 | " ", 52 | ], 53 | 2, 54 | ], 55 | "import/newline-after-import": ["warn", { count: 1 }], 56 | "import/no-extraneous-dependencies": [ 57 | "warn", 58 | { 59 | devDependencies: false, 60 | optionalDependencies: false, 61 | peerDependencies: true, 62 | bundledDependencies: true, 63 | }, 64 | ], 65 | "import/order": [ 66 | "warn", 67 | { 68 | alphabetize: { 69 | order: "asc", 70 | caseInsensitive: true, 71 | }, 72 | "newlines-between": "always", 73 | groups: [ 74 | "builtin", 75 | "external", 76 | "internal", 77 | ["parent", "sibling", "index"], 78 | "object", 79 | ], 80 | pathGroups: [ 81 | { 82 | pattern: "react?(-dom)", 83 | group: "external", 84 | position: "before", 85 | }, 86 | { 87 | pattern: "@iris/**", 88 | group: "external", 89 | position: "after", 90 | }, 91 | ], 92 | pathGroupsExcludedImportTypes: ["builtin"], 93 | }, 94 | ], 95 | }, 96 | overrides: [ 97 | { 98 | files: ["cypress/**"], 99 | rules: { 100 | "testing-library/prefer-screen-queries": "off", 101 | }, 102 | }, 103 | { 104 | files: ["stories/**"], 105 | rules: { 106 | "import/no-anonymous-default-export": ["off"], 107 | "import/no-extraneous-dependencies": "off", 108 | }, 109 | }, 110 | { 111 | files: [ 112 | "webpack.*.js", 113 | "*.test.{ts,tsx}", 114 | "test-utils.{ts,tsx}", 115 | "cypress/**", 116 | ], 117 | rules: { 118 | "import/no-extraneous-dependencies": [ 119 | "warn", 120 | { 121 | devDependencies: true, 122 | optionalDependencies: false, 123 | peerDependencies: false, 124 | bundledDependencies: true, 125 | }, 126 | ], 127 | }, 128 | }, 129 | ], 130 | settings: { 131 | "import/extensions": allExtensions, 132 | "import/external-module-folders": ["node_modules", "node_modules/@types"], 133 | "import/parsers": { 134 | "@typescript-eslint/parser": [".ts", ".tsx", ".d.ts"], 135 | }, 136 | "import/resolver": { 137 | node: { 138 | extensions: allExtensions, 139 | }, 140 | }, 141 | }, 142 | }; 143 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/properties-panels/index.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import userEvent from "@testing-library/user-event"; 18 | import { IntlProvider } from "react-intl"; 19 | 20 | import { 21 | render, 22 | screen, 23 | nodeSpec, 24 | selectedNode, 25 | createPalette, 26 | } from "../test-utils"; 27 | import NodeProperties from "./NodeProperties"; 28 | 29 | const nodes = createPalette([nodeSpec]).categories![0].node_types![0]; 30 | 31 | it("renders with undefined nodes selected", () => { 32 | const { container } = render(); 33 | expect(container.firstChild).toHaveTextContent( 34 | /select a node to edit its properties/i 35 | ); 36 | }); 37 | 38 | it("renders with no nodes selected", () => { 39 | const { container } = render( 40 | 41 | ); 42 | expect(container.firstChild).toHaveTextContent( 43 | /select a node to edit its properties/i 44 | ); 45 | }); 46 | 47 | it("renders with multiple nodes selected", () => { 48 | const { container } = render( 49 | 50 | ); 51 | expect(container.firstChild).toHaveTextContent( 52 | /multiple nodes are selected/i 53 | ); 54 | }); 55 | 56 | it("renders with supernode selected", () => { 57 | const { container } = render( 58 | 59 | ); 60 | expect(container.firstChild).toHaveTextContent( 61 | /this node type doesn't have any editable properties/i 62 | ); 63 | }); 64 | 65 | it("renders if selected node op isn't defined in schema", () => { 66 | const { container } = render( 67 | 68 | ); 69 | expect(container.firstChild).toHaveTextContent( 70 | /This node uses a component that is not stored in your component registry/i 71 | ); 72 | }); 73 | 74 | it("renders common properties with one node selected", () => { 75 | render( 76 | 77 | 78 | 79 | ); 80 | expect(screen.getByText(/notebook label/i)).toBeInTheDocument(); 81 | }); 82 | 83 | it("calls onFileRequested when a browse button is pressed", async () => { 84 | const handleFileRequested = jest.fn().mockResolvedValue([]); 85 | render( 86 | 87 | 92 | 93 | ); 94 | userEvent.click(screen.getByText(/browse/i)); 95 | expect(handleFileRequested).toHaveBeenCalledTimes(1); 96 | expect(handleFileRequested).toHaveBeenCalledWith({ 97 | canSelectMany: false, 98 | defaultUri: "example.ipynb", 99 | filters: { File: undefined }, 100 | filename: "example.ipynb", 101 | propertyID: "filename", 102 | }); 103 | }); 104 | 105 | it("doesn't crash when a browse button is pressed and onFileRequested is undefined", async () => { 106 | render( 107 | 108 | 109 | 110 | ); 111 | userEvent.click(screen.getByText(/browse/i)); 112 | }); 113 | 114 | it("calls onChange when a field changes", async () => { 115 | const handleChange = jest.fn(); 116 | render( 117 | 118 | 123 | 124 | ); 125 | // UPDATE_PROPERTY is triggered on mount sometimes? 126 | const initialCalls = handleChange.mock.calls.length; 127 | userEvent.click(screen.getByRole("checkbox")); 128 | expect(handleChange).toHaveBeenCalledTimes(initialCalls + 1); 129 | }); 130 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/PalettePanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useCallback } from "react"; 18 | import ReactDOM from "react-dom"; 19 | 20 | import styled from "styled-components"; 21 | 22 | interface NodeProps { 23 | image: string; 24 | label: string; 25 | } 26 | 27 | export function Node({ image, label }: NodeProps) { 28 | return ( 29 | 30 | 31 | 32 | 33 | 37 | 45 | 52 |
    53 | {label} 54 |
    55 |
    56 | 57 | 61 | 62 |
    63 |
    64 |
    65 |
    66 | ); 67 | } 68 | 69 | const Container = styled.div` 70 | margin-top: 14px; 71 | `; 72 | 73 | const Item = styled.div.attrs({ draggable: true })` 74 | display: flex; 75 | align-items: center; 76 | cursor: grab; 77 | height: 40px; 78 | margin: 0 22px 8px; 79 | background-color: ${({ theme }) => theme.palette.background.secondary}; 80 | border: 1px solid transparent; 81 | `; 82 | 83 | const Icon = styled.img.attrs({ draggable: false, alt: "" })` 84 | height: 26px; 85 | margin: 0 7px; 86 | `; 87 | 88 | const Label = styled.div` 89 | font-family: ${({ theme }) => theme.typography.fontFamily}; 90 | font-weight: ${({ theme }) => theme.typography.fontWeight}; 91 | font-size: ${({ theme }) => theme.typography.fontSize}; 92 | color: ${({ theme }) => theme.palette.text.primary}; 93 | `; 94 | 95 | interface Props { 96 | nodes: { 97 | op: string; 98 | app_data: { 99 | ui_data?: { 100 | label?: string; 101 | image?: string; 102 | }; 103 | }; 104 | }[]; 105 | } 106 | 107 | function PalettePanel({ nodes }: Props) { 108 | const handleDragStart = useCallback((e, node) => { 109 | const evData = { 110 | operation: "addToCanvas", 111 | data: { 112 | editType: "createExternalNode", 113 | nodeTemplate: { 114 | op: node.op, 115 | }, 116 | }, 117 | }; 118 | 119 | const nodeGhost = document.createElement("div"); 120 | nodeGhost.style.position = "absolute"; 121 | nodeGhost.style.top = "-100px"; 122 | document.body.appendChild(nodeGhost); 123 | ReactDOM.render(, nodeGhost); 124 | 125 | e.dataTransfer.setDragImage(nodeGhost, 86, 20); 126 | e.dataTransfer.setData("text", JSON.stringify(evData)); 127 | }, []); 128 | 129 | return ( 130 | 131 | {nodes.map((n) => ( 132 | handleDragStart(e, n)}> 133 | 134 | 135 | 136 | ))} 137 | 138 | ); 139 | } 140 | 141 | export default PalettePanel; 142 | -------------------------------------------------------------------------------- /images/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | @elyra/pipeline-editor 23 | 24 | 32 | 40 | 48 | 56 | 59 | 62 | 69 | 70 | 77 | 78 | 85 | 86 | 93 | 94 | 100 | 101 | 109 | 110 | 117 | 118 | 119 | 122 | 129 | 130 | 137 | 138 | 145 | 146 | 153 | 154 | 160 | 161 | 169 | 170 | 177 | 178 | 182 | 183 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV4/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import produce from "immer"; 18 | 19 | import rawMigrate from "./"; 20 | 21 | // wrap migrate functions in immer 22 | const migrate = produce((d: any) => rawMigrate(d)); 23 | 24 | it("should move all properties to component_parameters", () => { 25 | const properties = { 26 | filename: "notebook.ipynb", 27 | random: 42, 28 | boolean: false, 29 | }; 30 | 31 | const v3 = { 32 | pipelines: [ 33 | { 34 | app_data: { 35 | name: "name", 36 | version: 3, 37 | }, 38 | nodes: [ 39 | { 40 | type: "execution_node", 41 | app_data: { 42 | ...properties, 43 | }, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }; 49 | 50 | const actual = migrate(v3); 51 | expect(actual.pipelines[0].nodes[0].app_data.component_parameters).toEqual( 52 | properties 53 | ); 54 | }); 55 | 56 | it("should create a new label property based off of ui_data label", () => { 57 | const expectedLabel = "ui_data label example"; 58 | const v3 = { 59 | pipelines: [ 60 | { 61 | app_data: { 62 | name: "name", 63 | version: 3, 64 | }, 65 | nodes: [ 66 | { 67 | type: "execution_node", 68 | app_data: { 69 | filename: "should-not-be-used.ipynb", 70 | ui_data: { 71 | label: expectedLabel, 72 | }, 73 | }, 74 | }, 75 | ], 76 | }, 77 | ], 78 | }; 79 | 80 | const actual = migrate(v3); 81 | 82 | expect(actual.pipelines[0].nodes[0].app_data.label).toEqual(expectedLabel); 83 | }); 84 | 85 | it("should migrate nodes that are not part of the default pipeline", () => { 86 | const v3 = { 87 | pipelines: [ 88 | { 89 | app_data: { 90 | name: "name", 91 | version: 3, 92 | }, 93 | nodes: [ 94 | { 95 | type: "execution_node", 96 | app_data: { 97 | subflow1: "value1", 98 | }, 99 | }, 100 | ], 101 | }, 102 | { 103 | nodes: [ 104 | { 105 | type: "execution_node", 106 | app_data: { 107 | subflow2: "value2", 108 | }, 109 | }, 110 | ], 111 | }, 112 | ], 113 | }; 114 | 115 | const actual = migrate(v3); 116 | 117 | expect(actual.pipelines[0].nodes[0].app_data.component_parameters).toEqual({ 118 | subflow1: "value1", 119 | }); 120 | expect(actual.pipelines[1].nodes[0].app_data.component_parameters).toEqual({ 121 | subflow2: "value2", 122 | }); 123 | }); 124 | 125 | it("should not effect old ui_data", () => { 126 | const ui_data = { 127 | label: "hello", 128 | x_pos: 100, 129 | y_pos: 56, 130 | fake: false, 131 | }; 132 | 133 | const v3 = { 134 | pipelines: [ 135 | { 136 | app_data: { 137 | name: "name", 138 | version: 3, 139 | }, 140 | nodes: [ 141 | { 142 | type: "execution_node", 143 | app_data: { 144 | prop1: "value1", 145 | prop2: "value2", 146 | ui_data, 147 | }, 148 | }, 149 | ], 150 | }, 151 | ], 152 | }; 153 | 154 | const actual = migrate(v3); 155 | 156 | expect(actual.pipelines[0].nodes[0].app_data.ui_data).toEqual(ui_data); 157 | }); 158 | 159 | it("should not effect not update non-execution nodes", () => { 160 | const supernode = { 161 | type: "super_node", 162 | app_data: { 163 | ui_data: { 164 | label: "Supernode Label", 165 | }, 166 | }, 167 | }; 168 | 169 | const v3 = { 170 | pipelines: [ 171 | { 172 | app_data: { 173 | name: "name", 174 | version: 3, 175 | }, 176 | nodes: [supernode], 177 | }, 178 | ], 179 | }; 180 | 181 | const actual = migrate(v3); 182 | 183 | expect(actual.pipelines[0].nodes[0]).toEqual(supernode); 184 | }); 185 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/CustomOneOf.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Field, FieldProps, utils, WidgetProps } from "@rjsf/core"; 18 | 19 | /** 20 | * A custom oneOf field to handle the 2 custom oneOf cases that Elyra has: 21 | * 1. Each oneOf entry represents a different widget: 22 | * Selecting the options based on widget type: assumes the value of the field 23 | * is { value, widget } so that the widget type can be saved in the json. 24 | * 2. "inputpath": Each oneOf entry represents a different set object 25 | * Selecting the options based on the default values of the given oneOf object. 26 | */ 27 | export const CustomOneOf: Field = (props) => { 28 | const { options, formData, registry } = props; 29 | const findOption = (): any => { 30 | // For inputpaths, expect a oneOf that has { value, option } for each entry 31 | if (props.uiSchema?.["inputpath"]) { 32 | for (const i in props.schema.oneOf ?? []) { 33 | const properties: any = props.schema.oneOf?.[i]; 34 | if ( 35 | formData?.value === (properties as any).properties.value.default && 36 | formData?.option === (properties as any).properties.option.default 37 | ) { 38 | return i; 39 | } 40 | } 41 | return 0; 42 | } 43 | // for other oneOfs, check for widget specified in the "value" uihints to match the saved widget 44 | for (const i in options as any[]) { 45 | const option = options[i]; 46 | if ( 47 | (formData?.widget && 48 | option.properties?.widget?.default === formData?.widget) || 49 | (formData?.type && option.properties?.type?.default === formData?.type) 50 | ) { 51 | return i; 52 | } 53 | } 54 | return 0; 55 | }; 56 | const onOptionChange = (option: any) => { 57 | const selectedOption = parseInt(option, 10); 58 | const { rootSchema } = props.registry; 59 | 60 | // Call getDefaultFormState to make sure defaults are populated on change. 61 | let defaults; 62 | try { 63 | defaults = utils.getDefaultFormState( 64 | options[selectedOption], 65 | undefined, 66 | rootSchema 67 | ); 68 | } catch {} 69 | props.onChange(defaults); 70 | }; 71 | 72 | const SchemaField = registry.fields.SchemaField as React.FC; 73 | const { widgets } = registry; 74 | const uiOptions = (utils.getUiOptions(props.uiSchema) ?? {}) as WidgetProps; 75 | const Widget = utils.getWidget( 76 | { type: "number" }, 77 | "select", 78 | widgets 79 | ) as React.FC; 80 | 81 | const option = options[findOption()] || null; 82 | let optionSchema; 83 | 84 | if (option) { 85 | // If the subschema doesn't declare a type, infer the type from the 86 | // parent schema 87 | optionSchema = option.type 88 | ? option 89 | : Object.assign({}, option, { type: props.baseType }); 90 | } 91 | 92 | optionSchema = { 93 | ...optionSchema, 94 | uihints: { 95 | ...optionSchema.uihints, 96 | value: { 97 | ...optionSchema.uihints.value, 98 | parentID: props.idSchema.$id.replace("root_component_parameters_", ""), 99 | }, 100 | }, 101 | }; 102 | 103 | const enumOptions = options.map((option: any, index: number) => ({ 104 | label: option.title || `Option ${index + 1}`, 105 | value: index, 106 | })); 107 | 108 | return ( 109 |
    110 |
    111 | 124 |
    125 | 126 | {option !== null && ( 127 | 132 | )} 133 |
    134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/CustomFormControls/CustomArray.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { useCallback } from "react"; 18 | 19 | import { ArrayFieldTemplateProps } from "@rjsf/core"; 20 | 21 | const renderDefaults = ( 22 | items: any[], 23 | props: any 24 | ): React.ReactElement | undefined => { 25 | const allRendered = []; 26 | if (items.length === 0) { 27 | return undefined; 28 | } else if (props.schema.items?.type === "object") { 29 | for (const item of items) { 30 | const itemRendered = []; 31 | for (const key in props.schema.items.properties ?? {}) { 32 | const propertySchema = props.schema.items.properties[key]; 33 | if (propertySchema.type === "boolean") { 34 | itemRendered.push( 35 |
    36 | 42 |
    43 | ); 44 | } else { 45 | itemRendered.push( 46 |
    47 | 48 | 49 |
    50 | ); 51 | } 52 | } 53 | allRendered.push( 54 |
    55 | 61 | {itemRendered} 62 |
    63 | ); 64 | } 65 | } else { 66 | for (const item of items) { 67 | if (item === null || item === undefined || item === "") { 68 | return undefined; 69 | } else { 70 | return ( 71 |
    72 |
    {item}
    73 |
    (pipeline default)
    74 |
    75 | ); 76 | } 77 | } 78 | } 79 | return
    {allRendered}
    ; 80 | }; 81 | 82 | /** 83 | * React component that allows for custom add / remove buttons in the array 84 | * field component. 85 | */ 86 | export const ArrayTemplate: React.FC = (props) => { 87 | const renderedDefaults = renderDefaults( 88 | props.uiSchema.pipeline_defaults ?? [], 89 | props 90 | ); 91 | const handleChooseFile = useCallback(async () => { 92 | props.formContext.onFileRequested({ 93 | canSelectMany: true, 94 | filters: { File: props.uiSchema.extensions }, 95 | propertyID: props.idSchema.$id.replace("root_component_parameters_", ""), 96 | }); 97 | }, [props]); 98 | return ( 99 |
    100 | {props.items.map((item) => { 101 | return ( 102 |
    103 | {item.children} 104 | 111 |
    112 | ); 113 | })} 114 | {renderedDefaults} 115 | {props.canAdd && ( 116 | 122 | )} 123 | {props.uiSchema.canRefresh && ( 124 | 135 | )} 136 | {props.uiSchema?.files && ( 137 | 144 | )} 145 |
    146 | ); 147 | }; 148 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/migration/migrateV6/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import path from "path"; 18 | 19 | import { ComponentNotFoundError } from "../errors"; 20 | 21 | export const opMap: { [key: string]: string } = { 22 | run_notebook_using_papermill_Runnotebookusingpapermill: 23 | "elyra-kfp-examples-catalog:61e6f4141f65", 24 | filter_text_using_shell_and_grep_Filtertext: 25 | "elyra-kfp-examples-catalog:737915b826e9", 26 | component_Downloaddata: "elyra-kfp-examples-catalog:a08014f9252f", 27 | component_Calculatedatahash: "elyra-kfp-examples-catalog:d68ec7fcdf46", 28 | bash_operator_BashOperator: "elyra-airflow-examples-catalog:3a55d015ea96", 29 | email_operator_EmailOperator: "elyra-airflow-examples-catalog:a043648d3897", 30 | http_operator_SimpleHttpOperator: 31 | "elyra-airflow-examples-catalog:b94cd49692e2", 32 | spark_sql_operator_SparkSqlOperator: 33 | "elyra-airflow-examples-catalog:3b639742748f", 34 | spark_submit_operator_SparkSubmitOperator: 35 | "elyra-airflow-examples-catalog:b29c25ec8bd6", 36 | slack_operator_SlackAPIPostOperator: 37 | "elyra-airflow-examples-catalog:16a204f716a2", 38 | }; 39 | 40 | const runtimeTypeMap: { [key: string]: string } = { 41 | kfp: "KUBEFLOW_PIPELINES", 42 | airflow: "APACHE_AIRFLOW", 43 | }; 44 | 45 | function migrate(pipelineFlow: any, palette: any) { 46 | let paletteNodes = []; 47 | for (const c of palette?.categories || []) { 48 | if (c.node_types) { 49 | paletteNodes.push(...c.node_types); 50 | } 51 | } 52 | 53 | // Add runtime type based on previous runtime property 54 | pipelineFlow.pipelines[0].app_data.runtime_type = 55 | runtimeTypeMap[pipelineFlow.pipelines[0].app_data.runtime]; 56 | delete pipelineFlow.pipelines[0].app_data.runtime; 57 | 58 | for (const pipeline of pipelineFlow.pipelines) { 59 | for (const node of pipeline.nodes) { 60 | const newOp = opMap[node.op]; 61 | if (newOp) { 62 | // update op string 63 | node.op = newOp; 64 | 65 | // update component_source from string to json 66 | const opParts = newOp.split(":"); 67 | const catalog_type = opParts[0]; 68 | const component_ref: { [key: string]: any } = {}; 69 | component_ref["component-id"] = path.basename( 70 | node.app_data.component_source 71 | ); 72 | // handle the two cases where the filename changed 73 | if (component_ref["component-id"] === "component.yaml") { 74 | switch (opParts[1]) { 75 | case "a08014f9252f": 76 | component_ref["component-id"] = "download_data.yaml"; 77 | break; 78 | case "d68ec7fcdf46": 79 | component_ref["component-id"] = "calculate_hash.yaml"; 80 | } 81 | } 82 | node.app_data.component_source = JSON.stringify({ 83 | catalog_type, 84 | component_ref, 85 | }); 86 | 87 | // update format of values that have switch to using OneOfControl 88 | // running this inside the if since only those nodes use OneOfControl 89 | // only running on airflow components since kfp migration is handled in v7 90 | if ( 91 | pipelineFlow.pipelines[0].app_data.runtime_type === "APACHE_AIRFLOW" 92 | ) { 93 | const nodePropertiesSchema = paletteNodes.find( 94 | (n: any) => n.op === node.op 95 | ); 96 | if (nodePropertiesSchema === undefined) { 97 | throw new ComponentNotFoundError(); 98 | } 99 | const propertyDefs = 100 | nodePropertiesSchema.app_data.properties.uihints.parameter_info; 101 | Object.keys(node.app_data.component_parameters ?? {}).forEach( 102 | (key) => { 103 | const propDef = propertyDefs.find( 104 | (p: any) => p.parameter_ref === "elyra_" + key 105 | ); 106 | if (propDef?.custom_control_id === "OneOfControl") { 107 | const activeControl: string = 108 | Object.keys(propDef.data.controls).find( 109 | (c: string) => c !== "NestedEnumControl" 110 | ) || ""; 111 | node.app_data.component_parameters[key] = { 112 | activeControl, 113 | [activeControl]: node.app_data.component_parameters[key], 114 | }; 115 | } 116 | } 117 | ); 118 | } 119 | } 120 | } 121 | } 122 | 123 | pipelineFlow.pipelines[0].app_data.version = 6; 124 | 125 | return pipelineFlow; 126 | } 127 | 128 | export default migrate; 129 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/NodeTooltip/utils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { hasValue, toPrettyString } from "./utils"; 18 | 19 | describe("toPrettyString", () => { 20 | it("returns the same string for a string", () => { 21 | const result = toPrettyString("hello world"); 22 | expect(result).toEqual("hello world"); 23 | }); 24 | 25 | it("returns a string for a number", () => { 26 | const result = toPrettyString(42); 27 | expect(result).toEqual("42"); 28 | }); 29 | 30 | it("returns a string for a simple string array", () => { 31 | const result = toPrettyString(["one", "two", "three"]); 32 | expect(result).toEqual("one\ntwo\nthree"); 33 | }); 34 | 35 | it("returns a string for a simple array with numbers", () => { 36 | const result = toPrettyString([1, 2, 3]); 37 | expect(result).toEqual("1\n2\n3"); 38 | }); 39 | 40 | it("returns a string for a simple array with null holes", () => { 41 | const result = toPrettyString([null, null, null]); 42 | expect(result).toEqual("null\nnull\nnull"); 43 | }); 44 | 45 | it("returns a string for a simple array with undefined holes", () => { 46 | const result = toPrettyString([undefined, undefined, undefined]); 47 | expect(result).toEqual("undefined\nundefined\nundefined"); 48 | }); 49 | 50 | it("returns an empty string for an empty array", () => { 51 | const result = toPrettyString([]); 52 | expect(result).toEqual(""); 53 | }); 54 | 55 | it("returns a string for a sparse array", () => { 56 | // eslint-disable-next-line no-sparse-arrays 57 | const result = toPrettyString(["one", , , "two", , "three"]); 58 | expect(result).toEqual("one\n\n\ntwo\n\nthree"); 59 | }); 60 | 61 | it("returns 'Yes' for a true boolean", () => { 62 | const result = toPrettyString(true); 63 | expect(result).toEqual("Yes"); 64 | }); 65 | 66 | it("returns 'No' for a false boolean", () => { 67 | const result = toPrettyString(false); 68 | expect(result).toEqual("No"); 69 | }); 70 | 71 | it("returns a string for undefined", () => { 72 | const result = toPrettyString(undefined); 73 | expect(result).toEqual("undefined"); 74 | }); 75 | 76 | it("returns a string for null", () => { 77 | const result = toPrettyString(null); 78 | expect(result).toEqual("null"); 79 | }); 80 | 81 | it("returns a string for NaN", () => { 82 | const result = toPrettyString(NaN); 83 | expect(result).toEqual("NaN"); 84 | }); 85 | 86 | it("returns a string for an object", () => { 87 | const result = toPrettyString({ one: 1, two: 2, three: 3 }); 88 | expect(result).toEqual("one: 1\ntwo: 2\nthree: 3"); 89 | }); 90 | 91 | it("returns a string for an error", () => { 92 | const result = toPrettyString(new Error("this is an error")); 93 | expect(result).toEqual("Error: this is an error"); 94 | }); 95 | }); 96 | 97 | describe("hasValue", () => { 98 | it("returns true for a true value", () => { 99 | const result = hasValue(true); 100 | expect(result).toEqual(true); 101 | }); 102 | 103 | it("returns true for a false value", () => { 104 | const result = hasValue(true); 105 | expect(result).toEqual(true); 106 | }); 107 | 108 | it("returns true for a string", () => { 109 | const result = hasValue("hello"); 110 | expect(result).toEqual(true); 111 | }); 112 | 113 | it("returns true for an array", () => { 114 | const result = hasValue(["one", "two", "three"]); 115 | expect(result).toEqual(true); 116 | }); 117 | 118 | it("returns true for an object", () => { 119 | const result = hasValue({ one: 1, two: 2, three: 3 }); 120 | expect(result).toEqual(true); 121 | }); 122 | 123 | it("returns true for a number", () => { 124 | const result = hasValue(42); 125 | expect(result).toEqual(true); 126 | }); 127 | 128 | it("returns true for zero", () => { 129 | const result = hasValue(0); 130 | expect(result).toEqual(true); 131 | }); 132 | 133 | it("returns true for NaN", () => { 134 | const result = hasValue(NaN); 135 | expect(result).toEqual(true); 136 | }); 137 | 138 | it("returns false for an empty string", () => { 139 | const result = hasValue(""); 140 | expect(result).toEqual(false); 141 | }); 142 | 143 | it("returns false for an empty array", () => { 144 | const result = hasValue([]); 145 | expect(result).toEqual(false); 146 | }); 147 | 148 | it("returns false for a null value", () => { 149 | const result = hasValue(null); 150 | expect(result).toEqual(false); 151 | }); 152 | 153 | it("returns false for an undefined value", () => { 154 | const result = hasValue(undefined); 155 | expect(result).toEqual(false); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /packages/pipeline-services/src/validation/utils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Node } from "jsonc-parser"; 18 | 19 | import { getLinks, getNodes, findNode, rangeForLocation } from "./utils"; 20 | 21 | describe("findNode", () => { 22 | it("returns undefined if node doesn't exist", () => { 23 | const pipeline = { nodes: [{ id: "node-1" }] }; 24 | const node = findNode(pipeline, "node-2"); 25 | expect(node).toBeUndefined(); 26 | }); 27 | 28 | it("returns the node if it exists", () => { 29 | const pipeline = { nodes: [{ id: "node-1" }] }; 30 | const node = findNode(pipeline, "node-1"); 31 | expect(node.id).toBe("node-1"); 32 | }); 33 | }); 34 | 35 | describe("getNodes", () => { 36 | it("gets a list of nodes", () => { 37 | const pipeline = { nodes: ["I am a node"] }; 38 | const nodes = getNodes(pipeline); 39 | expect(nodes).toEqual(["I am a node"]); 40 | }); 41 | }); 42 | 43 | describe("getLinks", () => { 44 | it("returns an empty list when there are no nodes", () => { 45 | const pipeline = { nodes: [] }; 46 | const links = getLinks(pipeline); 47 | expect(links).toHaveLength(0); 48 | }); 49 | 50 | it("returns an empty list when the source node doesn't exist", () => { 51 | const pipeline = { 52 | nodes: [ 53 | { 54 | id: "node-1", 55 | type: "execution_node", 56 | inputs: [ 57 | { 58 | links: [ 59 | { 60 | id: "link-1", 61 | node_id_ref: "node-2", 62 | }, 63 | ], 64 | }, 65 | ], 66 | }, 67 | ], 68 | }; 69 | const links = getLinks(pipeline); 70 | expect(links).toHaveLength(0); 71 | }); 72 | 73 | it("returns an empty list when there are no links", () => { 74 | const pipeline = { 75 | nodes: [ 76 | { 77 | id: "node-1", 78 | type: "execution_node", 79 | inputs: [ 80 | { 81 | links: [], 82 | }, 83 | ], 84 | }, 85 | ], 86 | }; 87 | const links = getLinks(pipeline); 88 | expect(links).toHaveLength(0); 89 | }); 90 | 91 | it("returns an empty list when links are undefined", () => { 92 | const pipeline = { 93 | nodes: [ 94 | { 95 | id: "node-1", 96 | type: "execution_node", 97 | inputs: [ 98 | { 99 | links: undefined, 100 | }, 101 | ], 102 | }, 103 | ], 104 | }; 105 | const links = getLinks(pipeline); 106 | expect(links).toHaveLength(0); 107 | }); 108 | 109 | it("returns an empty list when there are no inputs", () => { 110 | const pipeline = { 111 | nodes: [ 112 | { 113 | id: "node-1", 114 | type: "execution_node", 115 | inputs: [], 116 | }, 117 | ], 118 | }; 119 | const links = getLinks(pipeline); 120 | expect(links).toHaveLength(0); 121 | }); 122 | 123 | it("returns an empty list when inputs are undefined", () => { 124 | const pipeline = { 125 | nodes: [ 126 | { 127 | id: "node-1", 128 | type: "execution_node", 129 | inputs: undefined, 130 | }, 131 | ], 132 | }; 133 | const links = getLinks(pipeline); 134 | expect(links).toHaveLength(0); 135 | }); 136 | 137 | it("returns link between two nodes", () => { 138 | const pipeline = { 139 | nodes: [ 140 | { 141 | id: "node-1", 142 | type: "execution_node", 143 | inputs: [ 144 | { 145 | links: [ 146 | { 147 | id: "link-1", 148 | node_id_ref: "node-2", 149 | }, 150 | ], 151 | }, 152 | ], 153 | }, 154 | { 155 | id: "node-2", 156 | type: "execution_node", 157 | }, 158 | ], 159 | }; 160 | const links = getLinks(pipeline); 161 | expect(links).toHaveLength(1); 162 | expect(links[0].id).toBe("link-1"); 163 | expect(links[0].srcNodeId).toBe("node-2"); 164 | expect(links[0].trgNodeId).toBe("node-1"); 165 | }); 166 | }); 167 | 168 | describe("rangeForLocation", () => { 169 | it("returns 0,0 for undefined", () => { 170 | const range = rangeForLocation(undefined); 171 | expect(range).toEqual({ offset: 0, length: 0 }); 172 | }); 173 | 174 | it("returns range for range", () => { 175 | const range = rangeForLocation({ 176 | parent: { offset: 10, colonOffset: 15 }, 177 | } as Node); 178 | expect(range).toEqual({ offset: 10, length: 5 }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/TabbedPanelLayout/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | 19 | import styled, { useTheme } from "styled-components"; 20 | 21 | import IconButton from "../IconButton"; 22 | 23 | interface Props { 24 | tabs: { 25 | id: string; 26 | label: string; 27 | title?: string; 28 | icon?: React.ReactNode; 29 | content: React.ReactNode; 30 | }[]; 31 | collapsed?: boolean; 32 | showCloseButton?: boolean; 33 | currentTab?: string; 34 | onClose?: () => any; 35 | onTabClick?: (id: string) => any; 36 | } 37 | 38 | const VerticalTabGroup = styled.div` 39 | padding: 7px 0; 40 | display: flex; 41 | flex-direction: column; 42 | align-items: center; 43 | `; 44 | 45 | const HorizontalTabGroup = styled.div` 46 | display: flex; 47 | `; 48 | 49 | const Tab = styled.div` 50 | ${VerticalTabGroup} & { 51 | margin: 3px 0; 52 | } 53 | ${VerticalTabGroup} &:hover { 54 | background-color: ${({ theme }) => theme.palette.hover}; 55 | } 56 | ${VerticalTabGroup} &:active { 57 | background-color: ${({ theme }) => theme.palette.active}; 58 | } 59 | ${HorizontalTabGroup} & { 60 | cursor: pointer; 61 | user-select: none; 62 | text-transform: uppercase; 63 | padding: 4px 10px 3px; 64 | } 65 | `; 66 | 67 | const ActionBar = styled.div` 68 | padding: 0 8px; 69 | display: flex; 70 | justify-content: space-between; 71 | `; 72 | 73 | const Content = styled.div` 74 | position: absolute; 75 | top: 35px; 76 | bottom: 0; 77 | overflow: auto; 78 | width: 100%; 79 | `; 80 | 81 | const StyledIconButton = styled(IconButton)` 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | height: 35px; 86 | line-height: 35px; 87 | min-width: 28px; 88 | margin-right: 4px; 89 | `; 90 | 91 | interface LabelProps { 92 | active: boolean; 93 | } 94 | 95 | const Label = styled.div` 96 | line-height: 27px; 97 | font-family: ${({ theme }) => theme.typography.fontFamily}; 98 | font-weight: ${({ theme }) => theme.typography.fontWeight}; 99 | font-size: 11px; 100 | 101 | color: ${({ active, theme }) => 102 | active ? theme.palette.text.bold : theme.palette.text.inactive}; 103 | 104 | border-bottom: 1px solid 105 | ${({ active, theme }) => (active ? theme.palette.tabBorder : "transparent")}; 106 | 107 | &:hover { 108 | color: ${({ theme }) => theme.palette.text.bold}; 109 | } 110 | `; 111 | 112 | const TabIcon = styled.div` 113 | cursor: pointer; 114 | user-select: none; 115 | color: ${({ theme }) => theme.palette.text.icon}; 116 | display: flex; 117 | justify-content: center; 118 | align-items: center; 119 | height: 35px; 120 | line-height: 35px; 121 | min-width: 28px; 122 | margin-right: 4px; 123 | `; 124 | 125 | function TabbedPanelLayout({ 126 | currentTab, 127 | onTabClick, 128 | tabs, 129 | showCloseButton, 130 | collapsed, 131 | onClose, 132 | }: Props) { 133 | const theme = useTheme(); 134 | 135 | if (collapsed === true) { 136 | return ( 137 | 138 | {tabs.map((t) => ( 139 | 140 | { 144 | onTabClick?.(t.id); 145 | }} 146 | > 147 | {t.icon} 148 | 149 | 150 | ))} 151 | 152 | ); 153 | } 154 | 155 | const resolvedCurrentTab = currentTab === undefined ? tabs[0].id : currentTab; 156 | 157 | return ( 158 | 159 | 160 | 161 | {tabs.map((t) => ( 162 | 163 | 172 | 173 | ))} 174 | 175 | {showCloseButton === true && ( 176 | { 180 | onClose?.(); 181 | }} 182 | > 183 | {theme.overrides?.closeIcon} 184 | 185 | )} 186 | 187 | 188 | {tabs.find((t) => t.id === resolvedCurrentTab)?.content ?? 189 | "Invalid tab id."} 190 | 191 | 192 | ); 193 | } 194 | 195 | export default TabbedPanelLayout; 196 | -------------------------------------------------------------------------------- /packages/pipeline-editor/src/properties-panels/PropertiesPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 Elyra Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Form, { UiSchema, Widget, AjvError } from "@rjsf/core"; 18 | import { produce } from "immer"; 19 | import styled from "styled-components"; 20 | 21 | import { 22 | FileWidget, 23 | CustomFieldTemplate, 24 | ArrayTemplate, 25 | CustomOneOf, 26 | } from "../CustomFormControls"; 27 | 28 | export const Message = styled.div` 29 | margin-top: 14px; 30 | padding: 0 22px; 31 | font-family: ${({ theme }) => theme.typography.fontFamily}; 32 | font-weight: ${({ theme }) => theme.typography.fontWeight}; 33 | font-size: ${({ theme }) => theme.typography.fontSize}; 34 | color: ${({ theme }) => theme.palette.text.primary}; 35 | opacity: 0.5; 36 | `; 37 | 38 | const widgets: { [id: string]: Widget } = { 39 | file: FileWidget, 40 | }; 41 | 42 | interface Props { 43 | data: any; 44 | schema?: any; 45 | onChange?: (data: any) => any; 46 | onFileRequested?: (options: any) => any; 47 | onPropertiesUpdateRequested?: (options: any) => any; 48 | noValidate?: boolean; 49 | id?: string; 50 | } 51 | 52 | export function PropertiesPanel({ 53 | data, 54 | schema, 55 | onChange, 56 | onFileRequested, 57 | onPropertiesUpdateRequested, 58 | noValidate, 59 | id, 60 | }: Props) { 61 | if (schema === undefined) { 62 | return No properties defined.; 63 | } 64 | 65 | let uiSchema: UiSchema = {}; 66 | for (const field in schema.properties) { 67 | uiSchema[field] = {}; 68 | const properties = schema.properties[field]; 69 | if (properties.type === "object") { 70 | for (const subField in properties.properties) { 71 | const subProps = properties.properties[subField]; 72 | if (typeof subProps !== "boolean" && subProps.uihints) { 73 | uiSchema[field][subField] = subProps.uihints; 74 | } 75 | } 76 | } 77 | if (typeof properties !== "boolean" && properties.uihints) { 78 | uiSchema[field] = properties.uihints; 79 | } 80 | } 81 | uiSchema = { 82 | ...uiSchema, 83 | ...schema.uihints, 84 | }; 85 | 86 | return ( 87 |
    { 92 | const newFormData = e.formData; 93 | const params = schema.properties?.component_parameters?.properties; 94 | for (const field in params) { 95 | if (params[field].oneOf) { 96 | for (const option of params[field].oneOf) { 97 | if (option.widget?.const !== undefined) { 98 | newFormData.component_parameters[field].widget = 99 | option.widget.const; 100 | } 101 | } 102 | } 103 | } 104 | onChange?.(e.formData); 105 | }} 106 | formContext={{ 107 | onFileRequested: async (args: any, fieldName: string) => { 108 | const values = await onFileRequested?.({ 109 | ...args, 110 | filename: data.component_parameters.filename, 111 | }); 112 | const newFormData = produce(data, (draft: any) => { 113 | if (args.canSelectMany) { 114 | draft.component_parameters[args.propertyID] = [ 115 | ...draft.component_parameters[args.propertyID], 116 | ...values, 117 | ]; 118 | } else { 119 | if (args.parentID) { 120 | draft.component_parameters[args.parentID].value = values?.[0]; 121 | } else { 122 | draft.component_parameters[args.propertyID] = values?.[0]; 123 | } 124 | } 125 | }); 126 | onChange?.(newFormData ?? data); 127 | }, 128 | onPropertiesUpdateRequested: async (args: any) => { 129 | const newData = await onPropertiesUpdateRequested?.(args); 130 | onChange?.(newData); 131 | }, 132 | formData: data, 133 | }} 134 | id={id} 135 | widgets={widgets} 136 | fields={{ 137 | OneOfField: CustomOneOf, 138 | }} 139 | liveValidate={!noValidate} 140 | ArrayFieldTemplate={ArrayTemplate} 141 | noHtml5Validate 142 | FieldTemplate={CustomFieldTemplate} 143 | className={"elyra-formEditor"} 144 | transformErrors={(errors: AjvError[]) => { 145 | // Suppress the "oneof" validation because we're using oneOf in a custom way. 146 | const transformed = []; 147 | for (const error of errors) { 148 | if ( 149 | error.message !== "should match exactly one schema in oneOf" && 150 | !(error as any).schemaPath?.includes("oneOf") && 151 | error.message !== "should be object" && 152 | error.message !== "should be string" 153 | ) { 154 | transformed.push(error); 155 | } 156 | } 157 | return transformed; 158 | }} 159 | /> 160 | ); 161 | } 162 | --------------------------------------------------------------------------------