├── .circleci └── config.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG.yml │ ├── EXTERNAL_SUPPORT.yml │ └── FEATURE_REQUEST.yml ├── PULL_REQUEST_TEMPLATE │ └── PULL_REQUEST.md └── preview.png ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── craco.config.js ├── cypress.config.cjs ├── cypress └── e2e │ └── spec.cy.js ├── index.html ├── package.json ├── postcss.config.cjs ├── public └── favicon.ico ├── src ├── App.tsx ├── components │ ├── atoms │ │ ├── AddButton.tsx │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── ComponentInfo.tsx │ │ ├── ConnectionLine.tsx │ │ ├── Definition.tsx │ │ ├── Edge.tsx │ │ ├── Empty.tsx │ │ ├── Footer.tsx │ │ ├── Info.tsx │ │ ├── OpenConfig.tsx │ │ ├── Select.tsx │ │ ├── Toast.tsx │ │ ├── Tooltip.tsx │ │ ├── WorkflowSelector.tsx │ │ ├── form │ │ │ ├── AdjacentStepListItem.tsx │ │ │ ├── ExecutorProperty.tsx │ │ │ ├── InspectorProperty.tsx │ │ │ ├── ListProperty.tsx │ │ │ ├── MatrixProperty.tsx │ │ │ └── StepListItem.tsx │ │ ├── nodes │ │ │ └── JobNode.tsx │ │ └── summaries │ │ │ ├── CommandSummary.tsx │ │ │ ├── ExecutorSummary.tsx │ │ │ ├── JobSummary.tsx │ │ │ └── ParameterSummary.tsx │ ├── containers │ │ ├── BreadCrumbs.tsx │ │ ├── CollapsibleList.tsx │ │ ├── ConfirmationModal.tsx │ │ ├── DefinitionsContainer.tsx │ │ ├── DropdownContainer.tsx │ │ ├── ExternalLinks.tsx │ │ ├── FilterPreviewContainer.tsx │ │ ├── GuideContainer.tsx │ │ ├── HeaderMenu.tsx │ │ ├── KBarList.tsx │ │ ├── OrbImportsContainer.tsx │ │ ├── ParamListContainer.tsx │ │ ├── ParametersContainer.tsx │ │ ├── PreviewToolbox.tsx │ │ ├── WorkflowContainer.tsx │ │ └── inspector │ │ │ ├── CommandInspector.tsx │ │ │ ├── ExecutorInspector.tsx │ │ │ ├── JobInspector.tsx │ │ │ ├── ParameterInspector.tsx │ │ │ └── subtypes │ │ │ ├── CommandSubtypes.tsx │ │ │ ├── ExecutorSubtypes.tsx │ │ │ └── ParameterSubtypes.tsx │ ├── menus │ │ ├── SubTypeMenu.tsx │ │ ├── TabbedMenu.tsx │ │ ├── definitions │ │ │ ├── DefinitionsMenu.tsx │ │ │ ├── InspectorDefinitionMenu.tsx │ │ │ ├── OrbDefinitionsMenu.tsx │ │ │ ├── OrbImportMenu.tsx │ │ │ ├── StepDefinitionMenu.tsx │ │ │ └── subtypes │ │ │ │ ├── ExecutorTypePage.tsx │ │ │ │ ├── ParameterTypePage.tsx │ │ │ │ └── StepTypePage.tsx │ │ └── stage │ │ │ ├── StagedFilterMenu.tsx │ │ │ └── StagedJobMenu.tsx │ └── panes │ │ ├── EditorPane.tsx │ │ ├── NavigationPane.tsx │ │ └── WorkflowsPane.tsx ├── examples │ ├── blogpost.json │ ├── index.ts │ └── readme.json ├── icons │ ├── IconProps.tsx │ ├── components │ │ ├── CommandIcon.tsx │ │ ├── ExecutorIcon.tsx │ │ ├── JobIcon.tsx │ │ ├── JobOnHoldIcon.tsx │ │ ├── OrbIcon.tsx │ │ ├── ParameterIcon.tsx │ │ └── WorkflowIcon.tsx │ └── ui │ │ ├── AddIcon.tsx │ │ ├── BranchIcon.tsx │ │ ├── BreadCrumbArrowIcon.tsx │ │ ├── CircleCI.tsx │ │ ├── CopyIcon.tsx │ │ ├── DeleteItemIcon.tsx │ │ ├── DragItemIcon.tsx │ │ ├── EditIcon.tsx │ │ ├── ExpandIcon.tsx │ │ ├── FilterIcon.tsx │ │ ├── InfoIcon.tsx │ │ ├── Loading.tsx │ │ ├── Logo.tsx │ │ ├── MinusIcon.tsx │ │ ├── MoreIcon.tsx │ │ ├── NewWindowIcon.tsx │ │ ├── OpenIcon.tsx │ │ ├── PlusIcon.tsx │ │ ├── Switch.tsx │ │ ├── TagIcon.tsx │ │ └── ToolTipPointerIcon.tsx ├── index.css ├── main.tsx ├── mappings │ ├── GenerableMapping.tsx │ ├── InspectableMapping.tsx │ └── components │ │ ├── CommandMapping.tsx │ │ ├── ExecutorMapping.tsx │ │ ├── JobMapping.tsx │ │ ├── ParameterMapping.tsx │ │ └── WorkflowMapping.tsx ├── state │ ├── DefinitionStore.tsx │ ├── Hooks.tsx │ └── Store.tsx ├── version.json └── vite-env.d.ts ├── tailwind.config.cjs ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@4.7.0 4 | cypress: cypress-io/cypress@2.1.0 5 | 6 | commands: 7 | update-version: 8 | description: Updates the version in the package.json file to match the tag. 9 | steps: 10 | - run: 11 | name: Set Package Version 12 | command: | 13 | # If "$CIRCLE_TAG" is not set, then set to dev version 14 | if [ -z "$CIRCLE_TAG" ]; then 15 | CIRCLE_TAG="0.0.0-development" 16 | fi 17 | # Set package-version.json version to match tag 18 | echo "{\"version\": \"$CIRCLE_TAG\"}" > ./src/version.json 19 | 20 | jobs: 21 | deploy-page: 22 | executor: 23 | name: node/default 24 | tag: 14.15.0 25 | steps: 26 | - checkout 27 | - add_ssh_keys: 28 | fingerprints: 29 | - 'ee:27:9d:5f:61:1b:e7:83:ac:80:fb:ce:c5:4f:37:f1' 30 | - node/install-packages: 31 | pkg-manager: yarn 32 | cache-version: v2 33 | - update-version 34 | - run: 35 | name: Build Page 36 | command: yarn build 37 | - run: 38 | name: Deploy Pages 39 | command: | 40 | cd build 41 | git config --global user.email "community-partner@circleci.com" 42 | git config --global user.name "orb-publisher" 43 | git init 44 | git add -A 45 | git commit -m "Deploy-<>-<> [ci skip]" 46 | git push -f git@github.com:CircleCI-Public/visual-config-editor.git master:gh-pages 47 | 48 | workflows: 49 | build-deploy: 50 | jobs: 51 | - cypress/run: 52 | name: test 53 | executor: cypress/browsers-chrome99-ff97 54 | yarn: true 55 | no-workspace: true 56 | command: yarn test 57 | - deploy-page: 58 | context: cci-config-sdk-publishing 59 | requires: 60 | - test 61 | filters: 62 | branches: 63 | ignore: /.*/ 64 | tags: 65 | only: /^v\d+\.\d+\.\d+$/ -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @KyleTryon 2 | @CircleCI-Public/cpeng -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug Report" 2 | description: Report any identified bugs. 3 | title: 'Bug: ' 4 | labels: [bug] 5 | # assignees: '' 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: "Is there an existing issue for this?" 10 | description: "Please search [here](https://github.com/CircleCI-Public/circleci-config-sdk-ts/issues?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 11 | options: 12 | - label: "I have searched the existing issues" 13 | required: true 14 | 15 | - type: textarea 16 | validations: 17 | required: true 18 | attributes: 19 | label: "Current behavior" 20 | description: "How does the issue manifest? **Please consider submitting a video showcasing the issue, with steps to reproduce.** [How to submit video](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files)" 21 | 22 | - type: input 23 | validations: 24 | required: true 25 | attributes: 26 | label: "Minimum reproduction code" 27 | description: "An URL to some git repository or gist which contains the minimum needed code to reproduce the error" 28 | placeholder: "https://github.com/..." 29 | 30 | - type: textarea 31 | attributes: 32 | label: "Steps to reproduce" 33 | description: | 34 | Detail the steps to take to replicate the issue. 35 | You may leave this blank if you have covered the issue in the minimum reproduction code above. 36 | placeholder: | 37 | 1. `npm i` 38 | 2. `npm start:dev` 39 | 3. See error... 40 | 41 | - type: textarea 42 | validations: 43 | required: true 44 | attributes: 45 | label: "Expected behavior" 46 | description: "A clear and concise description of what you expected to happen (or code)" 47 | 48 | - type: markdown 49 | attributes: 50 | value: | 51 | --- 52 | 53 | - type: input 54 | attributes: 55 | label: "CircleCI Config SDK version" 56 | description: | 57 | Which version of `@circleci/circleci-config-sdk` are you using? 58 | placeholder: "0.4.0" 59 | 60 | - type: input 61 | attributes: 62 | label: "Node.js version" 63 | description: "Which version of Node.js are you using?" 64 | placeholder: "14.17.6" 65 | 66 | - type: checkboxes 67 | attributes: 68 | label: "In which operating systems have you tested?" 69 | options: 70 | - label: macOS 71 | - label: Windows 72 | - label: Linux 73 | 74 | - type: markdown 75 | attributes: 76 | value: | 77 | --- 78 | 79 | - type: textarea 80 | attributes: 81 | label: "Other" 82 | description: | 83 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 84 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in. 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/EXTERNAL_SUPPORT.yml: -------------------------------------------------------------------------------- 1 | 2 | name: "\U0001f4e7 External Support" 3 | description: Unable to open an issue? Contact support@circleci.com. 4 | title: 'DO NOT SUBMIT' 5 | labels: [email] 6 | # assignees: '' 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: "If you are unable to open a GitHub issue, allow us to help! Contact CircleCI support via email and we will create a ticket for you. support@circleci.com." 11 | - type: checkboxes 12 | id: confirmation 13 | attributes: 14 | label: "Confirmation: This issue will be automatically deleted if accidentally submitted. If you are able to open an issue on GitHub, please use a different template." 15 | description: I understand this will be removed. 16 | options: 17 | - label: I understand this will be removed. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A1 Feature Request" 2 | description: Have an idea for a new feature? Begin by submitting a Feature Request 3 | title: 'Request: ' 4 | labels: [feature_request] 5 | # assignees: '' 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: "Is there an existing issue that is already proposing this?" 10 | description: "Please search [here](https://github.com/CircleCI-Public/circleci-config-sdk-ts/issues?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 11 | options: 12 | - label: "I have searched the existing issues" 13 | required: true 14 | - type: textarea 15 | id: contact 16 | attributes: 17 | label: "Is your feature request related to a problem? Please describe it" 18 | description: "A clear and concise description of what the problem is" 19 | placeholder: | 20 | I have an issue when ... 21 | validations: 22 | required: false 23 | - type: textarea 24 | validations: 25 | required: true 26 | attributes: 27 | label: "Describe the solution you'd like" 28 | description: "A clear and concise description of what you want to happen. Add any considered drawbacks" 29 | - type: textarea 30 | validations: 31 | required: true 32 | attributes: 33 | label: "Teachability, documentation, adoption, migration strategy" 34 | description: "If you can, explain how users will be able to use this and possibly write out a version the docs." 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: "What is the motivation / use case for changing the behavior?" 40 | description: "Describe the motivation or the concrete use case" -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our contributor [guidelines](https://github.com/CircleCI-Public/visual-config-editor/blob/main/CONTRIBUTING.md). 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Documentation has been added or updated where needed. 7 | 8 | ## PR Type 9 | What kind of change does this PR introduce? 10 | 11 | 12 | 13 | - [ ] Bug fix 14 | - [ ] Feature 15 | - [ ] Code style update (formatting, local variables) 16 | - [ ] Refactoring (no functional changes, no api changes) 17 | - [ ] Build related changes 18 | - [ ] CI related changes 19 | - [ ] Other... Please describe: 20 | 21 | > more details 22 | 23 | ## What issues are resolved by this PR? 24 | 25 | - #[00] 26 | 27 | ## Describe the new behavior. 28 | 29 | 30 | > Description 31 | 32 | ## Does this PR introduce a breaking change? 33 | 34 | - [ ] Yes 35 | - [ ] No 36 | 37 | 38 | 39 | ## Other information 40 | 41 | 42 | > More information (optional) 43 | -------------------------------------------------------------------------------- /.github/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircleCI-Archived/visual-config-editor/c95f04068f9b4511d72cfdb2cccfd9e75e10885d/.github/preview.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | cypress/screenshots 27 | cypress/videos 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.15.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | .github 5 | .husky 6 | .circleci 7 | docs -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | proseWrap: 'always', 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircleCI Visual Configuration Editor 2 | 3 | [![License](https://img.shields.io/github/license/CircleCI-Public/visual-config-editor)](https://github.com/CircleCI-Public/visual-config-editor/blob/main/LICENSE) 4 | [![CircleCI](https://img.shields.io/circleci/build/gh/CircleCI-Public/visual-config-editor/main?logo=circleci)](https://app.circleci.com/pipelines/github/CircleCI-Public/visual-config-editor) 5 | 6 | Generate your CircleCI configuration files by building a visual map of your 7 | project's workflows. No YAML? No problem. 8 | 9 | The core features of the VCE include: 10 | 11 | - Config Definition Creation, Editing 12 | - Visual Workflow Orchestration 13 | - Public Orb Support 14 | - Circularly Load Configs Created With the VCE 15 | 16 | [Read more about introduction of the VCE](https://circleci.com/blog/visual-config-editor/) 17 | 18 | # 19 | 20 | **[Try it out](https://circleci-public.github.io/visual-config-editor/)** for 21 | yourself! Fork the repo and **[contribute](CONTRIBUTING.md)** to help us make 22 | this amazing! [Join our discord](https://discord.gg/Vx8wMGdtce) and discuss work 23 | in progress! 24 | 25 | ## Preview 26 | 27 |

28 | Preview of the CircleCI Visual Config Editor 29 |

30 | 31 | ## Run Development Server 32 | 33 | ### With Docker 34 | 35 | Using npm: 36 | 37 | ```shell 38 | $ npm run start-docker 39 | ``` 40 | 41 | Using yarn: 42 | 43 | ```shell 44 | $ yarn start-docker 45 | ``` 46 | 47 | ### Without Docker 48 | 49 | **Install** 50 | 51 | Using yarn: 52 | 53 | ```shell 54 | $ yarn install 55 | ``` 56 | 57 | After installing your dependencies, ensure you are using the proper version of 58 | node by running NVM: 59 | 60 | ```shell 61 | $ nvm use 62 | ``` 63 | 64 | **Start dev server** 65 | 66 | Using yarn: 67 | 68 | ```shell 69 | $ yarn dev 70 | ``` 71 | 72 | ## Example Generated Config 73 | 74 | Click here to open this [example in the VCE](https://circleci-public.github.io/visual-config-editor?example=readme) 75 | 76 | ```yml 77 | # This configuration has been automatically generated by the CircleCI Config SDK. 78 | # For more information, see https://github.com/CircleCI-Public/circleci-config-sdk-ts 79 | # SDK Version: 0.9.0-alpha.15 80 | # VCE Version: v0.10.1 81 | # Modeled with the CircleCI visual config editor. 82 | # For more information, see https://github.com/CircleCI-Public/visual-config-editor 83 | 84 | version: 2.1 85 | setup: false 86 | jobs: 87 | build: 88 | steps: 89 | - checkout 90 | - run: 91 | command: yarn build 92 | - persist_to_workspace: 93 | root: ../ 94 | paths: 95 | - build 96 | docker: 97 | - image: cimg/node:16.11.1 98 | resource_class: medium 99 | test: 100 | steps: 101 | - attach_workspace: 102 | at: . 103 | - run: 104 | command: yarn test 105 | working_directory: ~/project/build 106 | - persist_to_workspace: 107 | root: . 108 | paths: 109 | - build 110 | docker: 111 | - image: cimg/node:16.11.1 112 | resource_class: medium 113 | deploy: 114 | steps: 115 | - attach_workspace: 116 | at: . 117 | - run: 118 | command: yarn deploy 119 | working_directory: ~/project/build 120 | docker: 121 | - image: cimg/node:16.11.1 122 | resource_class: medium 123 | workflows: 124 | build-and-test: 125 | jobs: 126 | - build 127 | - test: 128 | requires: 129 | - build 130 | - deploy: 131 | requires: 132 | - test 133 | ``` 134 | 135 | ## Contributing 136 | 137 | This repository welcomes community contributions! See our 138 | [CONTRIBUTING.md](CONTRIBUTING.md) for guidance on configuring your development 139 | environment and how to submit quality pull requests. 140 | 141 | ## Built with 142 | 143 | [CircleCI Config SDK](https://github.com/CircleCI-Public/circleci-config-sdk-ts) 144 | 145 | --- 146 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 2 | 3 | module.exports = { 4 | style: { 5 | postcss: { 6 | plugins: [require('tailwindcss'), require('autoprefixer')], 7 | }, 8 | }, 9 | webpack: { 10 | plugins: [new MonacoWebpackPlugin()], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /cypress.config.cjs: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:5173', 6 | supportFile: false, 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | describe('Components Mount', () => { 2 | it('Should have a title header', () => { 3 | cy.visit('http://127.0.0.1:5173/'); 4 | cy.get('#root > section > section').find('h1').then((h1) => { 5 | expect(h1.text()).equal('Visual Config Editor'); 6 | }); 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Visual Config Editor - CircleCI 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visual-config-editor", 3 | "version": "0.0.0-development", 4 | "homepage": "https://circleci-public.github.io/visual-config-editor/", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "cy:run": "yarn cypress run", 11 | "test": "start-server-and-test dev http://localhost:5173 cy:run" 12 | }, 13 | "dependencies": { 14 | "@circleci/circleci-config-parser": "^0.10.0-alpha.3", 15 | "@circleci/circleci-config-sdk": "^0.10.1", 16 | "@monaco-editor/react": "^4.4.5", 17 | "algoliasearch": "^4.13.1", 18 | "easy-peasy": "^5.0.3", 19 | "formik": "^2.2.9", 20 | "kbar": "0.1.0-beta.36", 21 | "react": "^18.2.0", 22 | "react-aria": "3.19.0", 23 | "react-beautiful-dnd": "^13.1.0", 24 | "react-dom": "^18.2.0", 25 | "react-flow-renderer": "9.6.7", 26 | "react-instantsearch-hooks-web": "^6.29.0", 27 | "react-redux": "^7.2.6", 28 | "start-server-and-test": "^1.14.0", 29 | "uuid": "^8.3.2" 30 | }, 31 | "devDependencies": { 32 | "@reduxjs/toolkit": "^1.6.2", 33 | "@testing-library/jest-dom": "^5.11.4", 34 | "@testing-library/react": "^12.1.2", 35 | "@testing-library/user-event": "^13.5.0", 36 | "@types/algoliasearch": "^4.0.0", 37 | "@types/jest": "^27.0.2", 38 | "@types/node": "^16.11.35", 39 | "@types/react": "^18.0.17", 40 | "@types/react-beautiful-dnd": "^13.1.2", 41 | "@types/react-dom": "^18.0.6", 42 | "@types/react-tabs": "^2.3.3", 43 | "@types/uuid": "^8.3.1", 44 | "@vitejs/plugin-react": "^2.1.0", 45 | "autoprefixer": "^10.4.12", 46 | "cypress": "^10.11.0", 47 | "eslint-plugin-react-hooks": "^4.3.0", 48 | "postcss": "^8.4.16", 49 | "tailwindcss": "^3.1.8", 50 | "typescript": "^4.6.4", 51 | "vite": "^3.1.0", 52 | "vite-plugin-environment": "^1.1.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircleCI-Archived/visual-config-editor/c95f04068f9b4511d72cfdb2cccfd9e75e10885d/public/favicon.ico -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch'; 2 | import { createStore, StoreProvider } from 'easy-peasy'; 3 | import { useRef } from 'react'; 4 | import Toast from './components/atoms/Toast'; 5 | import ToolTip from './components/atoms/Tooltip'; 6 | import ConfirmationModal from './components/containers/ConfirmationModal'; 7 | import KBarList from './components/containers/KBarList'; 8 | import EditorPane from './components/panes/EditorPane'; 9 | import NavigationPane from './components/panes/NavigationPane'; 10 | import WorkflowsPane from './components/panes/WorkflowsPane'; 11 | import './index.css'; 12 | import useWindowDimensions, { useStoreState } from './state/Hooks'; 13 | import Store from './state/Store'; 14 | export const store = createStore(Store); 15 | export const inspectorWidth = 400; 16 | 17 | export const searchClient = algoliasearch( 18 | 'U0RXNGRK45', 19 | '798b0e1407310a2b54b566250592b3fd', 20 | ); 21 | 22 | const Pinned = () => { 23 | const tooltip = useStoreState((state) => state.tooltip); 24 | return ( 25 |
26 | {tooltip && ( 27 | 28 |

{tooltip.description}

29 |
30 | )} 31 | 35 |
36 | ); 37 | }; 38 | 39 | const App = () => { 40 | const appWidth = useWindowDimensions(); 41 | const editorPane = useRef(null); 42 | 43 | return ( 44 | 45 | 46 |
47 |
51 | 52 | 53 |
54 | 55 |
56 |
57 | 58 |
59 | 60 |
61 | ); 62 | }; 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /src/components/atoms/AddButton.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | import AddIcon from '../../icons/ui/AddIcon'; 3 | 4 | const AddButton = (props: ButtonHTMLAttributes) => { 5 | return ( 6 | 16 | ); 17 | }; 18 | 19 | export default AddButton; 20 | -------------------------------------------------------------------------------- /src/components/atoms/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | const styles = { 4 | dangerous: { 5 | default: 'bg-circle-red-dangerous text-white', 6 | active: 'hover:bg-circle-red-dangerous-dark ', 7 | }, 8 | secondary: { 9 | default: 'bg-circle-gray-250', 10 | active: 'hover:bg-circle-gray-300', 11 | }, 12 | flat: { 13 | default: 'text-circle-gray-400', 14 | active: 'hover:bg-circle-gray-300 text-white', 15 | }, 16 | primary: { 17 | default: 'bg-circle-blue text-white', 18 | active: 'hover:bg-circle-blue-dark', 19 | }, 20 | }; 21 | 22 | export type ButtonVariant = keyof typeof styles; 23 | 24 | export const Button = ({ 25 | variant, 26 | className, 27 | margin, 28 | ...props 29 | }: ButtonHTMLAttributes & { 30 | variant: ButtonVariant; 31 | margin?: string; 32 | }) => { 33 | return ( 34 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/atoms/Card.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from './Button'; 2 | 3 | export interface CardProps { 4 | description?: string; 5 | title: string; 6 | icon?: React.ReactElement; 7 | pinned?: React.ReactElement; 8 | truncate?: number; 9 | onClick: React.MouseEventHandler; 10 | } 11 | 12 | const Card = ({ truncate, description, ...props }: CardProps) => { 13 | return ( 14 |
15 |
16 |
17 |
18 | {props.icon} 19 |

{props.title}

20 |
21 | {description && ( 22 |

23 | {truncate && description.length > truncate 24 | ? description?.slice(0, truncate) + '...' 25 | : description} 26 |

27 | )} 28 |
29 |
30 | {props.pinned && ( 31 |
{props.pinned}
32 | )} 33 |
34 | 43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default Card; 51 | -------------------------------------------------------------------------------- /src/components/atoms/ComponentInfo.tsx: -------------------------------------------------------------------------------- 1 | import InspectableMapping, { 2 | GenerableInfoType, 3 | } from '../../mappings/InspectableMapping'; 4 | 5 | const ComponentInfo = ({ 6 | type, 7 | docsInfo, 8 | }: { 9 | type?: InspectableMapping; 10 | docsInfo?: GenerableInfoType; 11 | }) => { 12 | const docInfo = type?.docsInfo || docsInfo; 13 | const parts = docInfo?.description.split('%s'); 14 | 15 | return ( 16 | <> 17 | {parts && ( 18 |

19 | {parts[0]} 20 | 25 | {type?.name.singular} 26 | 27 | {parts[1]} 28 |

29 | )} 30 | 31 | ); 32 | }; 33 | 34 | export default ComponentInfo; 35 | -------------------------------------------------------------------------------- /src/components/atoms/ConnectionLine.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ConnectionLineComponentProps, 3 | useStoreState as flowState, 4 | } from 'react-flow-renderer'; 5 | import { useStoreState } from '../../state/Hooks'; 6 | 7 | const getPos = (isSource: boolean, bounds: DOMRect, transform: number[]) => { 8 | let x, y; 9 | 10 | if (isSource) { 11 | x = (bounds.x + bounds.width - transform[0]) / transform[2]; 12 | } else { 13 | x = (bounds.x - transform[0]) / transform[2]; 14 | } 15 | 16 | y = (bounds.y + bounds.height / 2 - 60 - transform[1]) / transform[2]; 17 | 18 | return { x, y }; 19 | }; 20 | 21 | const ConnectionLine = ({ targetX, targetY }: ConnectionLineComponentProps) => { 22 | const connecting = useStoreState((state) => state.connecting); 23 | const altAction = useStoreState((state) => state.altAction); 24 | const transform = flowState((state: any) => state.transform); 25 | const handle = connecting?.start?.ref?.current as Element; 26 | const isSource = connecting?.start?.id.connectionHandleType === 'source'; 27 | 28 | if (!handle) { 29 | return null; 30 | } 31 | 32 | const bounds = handle.getBoundingClientRect(); 33 | const start = getPos(isSource, bounds, transform); 34 | let end = { x: targetX, y: targetY }; 35 | const dist = 30 * (isSource ? 1 : -1); 36 | const invalid = isSource ? end.x < start.x : end.y > start.x; 37 | let color = '#76CDFF'; 38 | 39 | if (!invalid && connecting?.end?.ref) { 40 | const snapTo = connecting?.end.ref?.current as Element; 41 | 42 | if (snapTo && connecting?.end?.ref.current !== handle) { 43 | const snapToBounds = snapTo.getBoundingClientRect(); 44 | end = getPos(!isSource, snapToBounds, transform); 45 | color = '#0078CA'; 46 | } 47 | } 48 | 49 | color = invalid || altAction ? '#F24646' : color; 50 | 51 | return ( 52 | 53 | 64 | 72 | 80 | 81 | ); 82 | }; 83 | 84 | export default ConnectionLine; 85 | -------------------------------------------------------------------------------- /src/components/atoms/Definition.tsx: -------------------------------------------------------------------------------- 1 | import * as CircleCI from '@circleci/circleci-config-sdk'; 2 | import { Generable } from '@circleci/circleci-config-sdk/dist/src/lib/Components'; 3 | import { AnyParameterLiteral } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Parameters/types/CustomParameterLiterals.types'; 4 | import { 5 | OrbImport, 6 | OrbRef, 7 | } from '@circleci/circleci-config-sdk/dist/src/lib/Orb'; 8 | import InspectableMapping from '../../mappings/InspectableMapping'; 9 | import { useStoreActions } from '../../state/Hooks'; 10 | import { InspectorDefinitionMenuNav } from '../menus/definitions/InspectorDefinitionMenu'; 11 | 12 | export const flattenGenerable = (data: Generable, nested?: boolean) => { 13 | // this generated object should always have a single key 14 | const generated = data.generate(); 15 | 16 | if (typeof generated === 'string') { 17 | return { name: generated }; 18 | } 19 | 20 | /** 21 | * Flattens the keys of the input. 22 | * Will nest under parameters if nested option is set 23 | * For nested example, see WorkflowStage 24 | */ 25 | return Object.entries(generated as Record).map(([key, value]) => 26 | nested 27 | ? { 28 | name: key, 29 | parameters: value, 30 | } 31 | : { 32 | name: key, 33 | ...value, 34 | }, 35 | )[0]; 36 | }; 37 | 38 | const Definition = (props: { 39 | data: Generable | OrbRef; 40 | type: InspectableMapping; 41 | index: number; 42 | orb?: OrbImport; 43 | }) => { 44 | const Summary = props.type.components.summary; 45 | const navigateTo = useStoreActions((actions) => actions.navigateTo); 46 | const setDragging = useStoreActions((actions) => actions.setDragging); 47 | 48 | return ( 49 | 81 | ); 82 | }; 83 | 84 | export default Definition; 85 | -------------------------------------------------------------------------------- /src/components/atoms/Edge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EdgeProps } from 'react-flow-renderer'; 3 | 4 | export default function Edge({ 5 | id, 6 | sourceX, 7 | sourceY, 8 | targetX, 9 | targetY, 10 | sourcePosition, 11 | targetPosition, 12 | style = {}, 13 | data, 14 | arrowHeadType, 15 | markerEndId, 16 | }: EdgeProps) { 17 | const gap = 45; 18 | 19 | return ( 20 | 21 | 31 | {/* 32 | 38 | {'requires'} 39 | 40 | */} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/atoms/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { IconProps } from '../../icons/IconProps'; 3 | 4 | export type EmptyProps = { 5 | label: string; 6 | Logo?: React.FunctionComponent; 7 | description?: string | ReactElement; 8 | }; 9 | 10 | export const Empty = ({ label, Logo, description }: EmptyProps) => { 11 | return ( 12 |
13 | {Logo && } 14 |

{label}

15 | {description && ( 16 |
{description}
17 | )} 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/atoms/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { inspectorWidth } from '../../App'; 3 | 4 | export const Footer = ({ 5 | children, 6 | centered, 7 | className, 8 | }: PropsWithChildren & { centered?: boolean }) => { 9 | return ( 10 |
14 |
15 | {children} 16 |
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/atoms/Info.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import InfoIcon from '../../icons/ui/InfoIcon'; 3 | import { useStoreActions } from '../../state/Hooks'; 4 | 5 | export const Info = ({ description }: { description: string }) => { 6 | const updateTooltip = useStoreActions((actions) => actions.updateTooltip); 7 | const ref = useRef(null); 8 | 9 | return ( 10 |
{ 14 | updateTooltip({ description, ref, facing: 'bottom' }); 15 | }} 16 | onMouseLeave={() => updateTooltip(undefined)} 17 | > 18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/atoms/OpenConfig.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import OpenIcon from '../../icons/ui/OpenIcon'; 3 | import { 4 | useConfigParser, 5 | useStoreActions, 6 | useStoreState, 7 | } from '../../state/Hooks'; 8 | import { Button } from '../atoms/Button'; 9 | 10 | export const OpenConfig = () => { 11 | const inputFile = useRef(null); 12 | const config = useStoreState((state) => state.config); 13 | const loadConfig = useStoreActions((actions) => actions.loadConfig); 14 | const parseConfig = useConfigParser(); 15 | 16 | return ( 17 | <> 18 | { 24 | if (!e.target.files) { 25 | console.error('File upload failed'); 26 | return; 27 | } 28 | 29 | e.target.files[0].text().then((yml) => { 30 | parseConfig(yml, loadConfig); 31 | }); 32 | }} 33 | /> 34 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/atoms/Select.tsx: -------------------------------------------------------------------------------- 1 | import { FieldHelperProps, FieldInputProps, FieldMetaProps } from 'formik'; 2 | import React, { OptionHTMLAttributes, ReactElement, useState } from 'react'; 3 | import ExpandIcon from '../../icons/ui/ExpandIcon'; 4 | import DropdownContainer from '../containers/DropdownContainer'; 5 | 6 | type OptionElement = ReactElement>; 7 | 8 | type SelectProps = { 9 | placeholder?: string; 10 | value?: any; 11 | className?: string; 12 | dropdownClassName?: string; 13 | onChange?: (value: any) => void; 14 | children: OptionElement[] | OptionElement; 15 | icon?: ReactElement; 16 | borderless?: boolean; 17 | }; 18 | 19 | type SelectFieldProps = SelectProps & { 20 | name: string; 21 | field: FieldInputProps; 22 | meta: FieldMetaProps; 23 | helper: FieldHelperProps; 24 | transform?: (value: any) => any; 25 | }; 26 | 27 | const SelectField = ({ 28 | name, 29 | field, 30 | meta, 31 | helper, 32 | ...props 33 | }: SelectFieldProps) => { 34 | const { value, initialValue } = meta; 35 | const { setValue } = helper; 36 | 37 | return ( 38 | 48 | updateFilter('type', value, toolbox.filter.preview) 49 | } 50 | > 51 | 52 | 53 | 54 |

Sample

55 | { 60 | setFilter(e.target.value); 61 | }} 62 | /> 63 | 64 |
65 | 76 | 85 |
86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/containers/inspector/CommandInspector.tsx: -------------------------------------------------------------------------------- 1 | import { FormikValues } from 'formik'; 2 | import CommandIcon from '../../../icons/components/CommandIcon'; 3 | import { CommandMapping } from '../../../mappings/components/CommandMapping'; 4 | import { DefinitionsModel } from '../../../state/DefinitionStore'; 5 | import { useStoreActions } from '../../../state/Hooks'; 6 | import AddButton from '../../atoms/AddButton'; 7 | import { Empty } from '../../atoms/Empty'; 8 | import InspectorProperty from '../../atoms/form/InspectorProperty'; 9 | import ListProperty from '../../atoms/form/ListProperty'; 10 | import StepListItem from '../../atoms/form/StepListItem'; 11 | import { StepDefinitionMenu } from '../../menus/definitions/StepDefinitionMenu'; 12 | import StepTypePageNav from '../../menus/definitions/subtypes/StepTypePage'; 13 | import { navSubTypeMenu } from '../../menus/SubTypeMenu'; 14 | 15 | const CommandInspector = ( 16 | props: FormikValues & { definitions: DefinitionsModel }, 17 | ) => { 18 | const navigateTo = useStoreActions((actions) => actions.navigateTo); 19 | 20 | return ( 21 |
22 | 28 | 29 | 41 | Add a step by clicking the button above. 42 |
43 | At least one step is required. 44 | 45 | } 46 | /> 47 | } 48 | listItem={StepListItem} 49 | pinned={ 50 | { 53 | navigateTo( 54 | navSubTypeMenu( 55 | { 56 | typePage: StepTypePageNav, 57 | menuPage: StepDefinitionMenu, 58 | passThrough: { dataType: CommandMapping }, 59 | }, 60 | props.values, 61 | ), 62 | ); 63 | }} 64 | /> 65 | } 66 | /> 67 |
68 | ); 69 | }; 70 | 71 | export default CommandInspector; 72 | -------------------------------------------------------------------------------- /src/components/containers/inspector/ExecutorInspector.tsx: -------------------------------------------------------------------------------- 1 | import { FormikValues } from 'formik'; 2 | import { DefinitionsModel } from '../../../state/DefinitionStore'; 3 | import InspectorProperty from '../../atoms/form/InspectorProperty'; 4 | import { executorSubtypes } from './subtypes/ExecutorSubtypes'; 5 | 6 | const ExecutorInspector = ( 7 | props: FormikValues & { 8 | definitions: DefinitionsModel; 9 | subtype?: string; 10 | }, 11 | ) => { 12 | if (!props.subtype) { 13 | return

Something went wrong!

; 14 | } 15 | 16 | return ( 17 | <> 18 | 19 | 24 | 30 | {executorSubtypes[props.subtype]?.resourceClasses?.map( 31 | (resourceClass: any) => ( 32 | 35 | ), 36 | )} 37 | 38 | {executorSubtypes[props.subtype]?.fields} 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default ExecutorInspector; 46 | -------------------------------------------------------------------------------- /src/components/containers/inspector/ParameterInspector.tsx: -------------------------------------------------------------------------------- 1 | import { FormikValues } from 'formik'; 2 | import { DefinitionsModel } from '../../../state/DefinitionStore'; 3 | import InspectorProperty from '../../atoms/form/InspectorProperty'; 4 | import { parameterSubtypes } from './subtypes/ParameterSubtypes'; 5 | 6 | const ParameterInspector = ( 7 | props: FormikValues & { 8 | definitions: DefinitionsModel; 9 | subtype?: string; 10 | }, 11 | ) => { 12 | const fields = props.subtype 13 | ? parameterSubtypes[props.subtype]?.fields 14 | : undefined; 15 | 16 | return ( 17 |
18 |
28 | ); 29 | }; 30 | 31 | export default ParameterInspector; 32 | -------------------------------------------------------------------------------- /src/components/containers/inspector/subtypes/ExecutorSubtypes.tsx: -------------------------------------------------------------------------------- 1 | import { executors } from '@circleci/circleci-config-sdk'; 2 | import { SubTypeMapping } from '../../../../mappings/GenerableMapping'; 3 | import InspectorProperty from '../../../atoms/form/InspectorProperty'; 4 | 5 | export interface ExecutorSubTypes { 6 | [type: string]: SubTypeMapping & { 7 | resourceClasses: string[]; 8 | }; 9 | } 10 | 11 | const executorSubtypes: ExecutorSubTypes = { 12 | docker: { 13 | text: 'Docker', 14 | component: executors.DockerExecutor, 15 | resourceClasses: [ 16 | 'small', 17 | 'medium', 18 | 'medium+', 19 | 'large', 20 | 'xlarge', 21 | '2xlarge', 22 | '2xlarge+', 23 | ], 24 | fields: , 25 | docsLink: 'https://circleci.com/docs/2.0/executor-types/#using-docker', 26 | description: 'Steps run in container with provided image', 27 | }, 28 | machine: { 29 | text: 'Machine', 30 | component: executors.MachineExecutor, 31 | resourceClasses: ['medium', 'large', 'xlarge', '2xlarge'], 32 | fields: , 33 | docsLink: 'https://circleci.com/docs/2.0/executor-types/#using-machine', 34 | description: 'Steps run on Linux Virtual Machine', 35 | }, 36 | macos: { 37 | text: 'MacOS', 38 | component: executors.MacOSExecutor, 39 | resourceClasses: ['medium', 'large'], 40 | fields: , 41 | docsLink: 'https://circleci.com/docs/2.0/executor-types/#using-macos', 42 | description: 43 | 'Steps run on macOS Virtual Machine with specific Xcode version', 44 | }, 45 | windows: { 46 | text: 'Windows', 47 | component: executors.WindowsExecutor, 48 | resourceClasses: [ 49 | 'windows.medium', 50 | 'windows.large', 51 | 'windows.xlarge', 52 | 'windows.2xlarge', 53 | ], 54 | fields: , 55 | docsLink: 56 | 'https://circleci.com/docs/2.0/executor-types/#using-the-windows-executor', 57 | description: 'Steps run on Windows Virtual Machine', 58 | }, 59 | }; 60 | 61 | export { executorSubtypes }; 62 | -------------------------------------------------------------------------------- /src/components/containers/inspector/subtypes/ParameterSubtypes.tsx: -------------------------------------------------------------------------------- 1 | import { ReusableExecutor } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Reusable'; 2 | import { SubTypeMapping } from '../../../../mappings/GenerableMapping'; 3 | import InspectorProperty from '../../../atoms/form/InspectorProperty'; 4 | import ListProperty from '../../../atoms/form/ListProperty'; 5 | 6 | export interface ComponentParameterType { 7 | types: string[]; 8 | } 9 | 10 | export interface ParameterTypes { 11 | [key: string]: SubTypeMapping; 12 | } 13 | 14 | export interface ComponentParameterMapping { 15 | pipeline: ComponentParameterType; 16 | executor: ComponentParameterType; 17 | job: ComponentParameterType; 18 | command: ComponentParameterType; 19 | } 20 | 21 | const parameterSubtypes: ParameterTypes = { 22 | string: { 23 | text: 'String', 24 | description: `Sequence of characters that are optionally enlcosed by quotes. 25 | Empty strings are treated as falsey while in evaluation of when clauses, and non-empty strings are treated as truthy.`, 26 | fields: ( 27 | 28 | ), 29 | }, 30 | boolean: { 31 | text: 'Boolean', 32 | description: `A Boolean represents a true/false value. Such as "on" or "off".`, 33 | 34 | fields: ( 35 | 41 | 42 | 43 | 44 | 45 | ), 46 | }, 47 | integer: { 48 | text: 'Integer', 49 | description: `A whole number.`, 50 | /** 51 | * Or have various formats such as having a leading “0x” to signal hexadecimal, 52 | a leading “0b” to indicate binary bits (base 2), or have leading “0” to signal an octal base. 53 | The use of “:” allows expressing integers in base 60, which is convenient for time/angle values. 54 | */ 55 | 56 | fields: ( 57 | 58 | ), 59 | }, 60 | enum: { 61 | text: 'Enum', 62 | description: `Enums allow to define a list of any values, 63 | which are useful in the case to enforce that the value must be one from a specific set of string values`, 64 | 65 | fields: (props) => ( 66 | <> 67 | ( 74 | { 79 | props.setValue(e.target.value); 80 | }} 81 | /> 82 | )} 83 | > 84 | 90 | 91 | {props.values.enum.map((value: string, index: number) => ( 92 | 95 | ))} 96 | 97 | 98 | ), 99 | }, 100 | executor: { 101 | text: 'Executor', 102 | fields: (props: { executors: ReusableExecutor[] }) => ( 103 | 109 | {props.executors?.map((executor) => ( 110 | 113 | ))} 114 | 115 | ), 116 | }, 117 | steps: { 118 | text: 'Steps', 119 | fields: , 120 | }, 121 | env_var_name: { 122 | text: 'Environment Variable Name', 123 | fields: , 124 | }, 125 | }; 126 | 127 | const componentParametersSubtypes: ComponentParameterMapping = { 128 | pipeline: { 129 | types: ['string', 'boolean', 'integer', 'enum'], 130 | }, 131 | executor: { types: ['string', 'boolean', 'integer', 'enum'] }, 132 | job: { 133 | types: [ 134 | 'string', 135 | 'boolean', 136 | 'integer', 137 | 'enum', 138 | /*'executor', 139 | 'steps',*/ 140 | 'env_var_name', 141 | ], 142 | }, 143 | command: { 144 | types: [ 145 | 'string', 146 | 'boolean', 147 | 'integer', 148 | 'enum', 149 | /*'steps',*/ 'env_var_name', 150 | ], 151 | }, 152 | }; 153 | 154 | export { componentParametersSubtypes, parameterSubtypes }; 155 | -------------------------------------------------------------------------------- /src/components/menus/SubTypeMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { v4 } from 'uuid'; 3 | import { DefinitionSubscriptions } from '../../state/DefinitionStore'; 4 | import { NavigationComponent } from '../../state/Store'; 5 | 6 | export type SubTypeMenuProps = { 7 | typePage: NavigationComponent; 8 | typeProps?: object; 9 | menuPage: (props: SubTypeMenuPageProps & any) => JSX.Element; 10 | menuProps?: object; 11 | passThrough?: any; 12 | }; 13 | export type SubTypeReference = T; 14 | export type SubTypeSelectPageProps = { 15 | setSubtype: (subtype: SubTypeReference) => void; 16 | }; 17 | export type SubTypeMenuPageProps = { 18 | subtype: SubTypeReference; 19 | selectSubtype: () => void; 20 | }; 21 | export interface SelectedSubType { 22 | current?: SubTypeReference; 23 | previous?: SubTypeReference; 24 | } 25 | 26 | const SubTypeMenu = ( 27 | props: SubTypeMenuProps & { nonce: string }, 28 | ) => { 29 | const [subtype, setSubtype] = useState< 30 | Record> 31 | >({}); 32 | 33 | const current = subtype[props.nonce]?.current; 34 | 35 | const updateSubtype = (selected: SubTypeReference) => { 36 | setSubtype({ 37 | ...subtype, 38 | [props.nonce]: { 39 | current: selected, 40 | previous: current, 41 | }, 42 | }); 43 | }; 44 | 45 | const navBack = () => { 46 | setSubtype({ 47 | ...subtype, 48 | [props.nonce]: { 49 | current: undefined, 50 | previous: current, 51 | }, 52 | }); 53 | }; 54 | 55 | const SubTypeSelectPage = props.typePage.Component as React.FunctionComponent< 56 | SubTypeSelectPageProps 57 | >; 58 | const SubTypeMenuPage = props.menuPage; 59 | 60 | return ( 61 |
62 | {current ? ( 63 | 68 | ) : ( 69 | 70 | )} 71 |
72 | ); 73 | }; 74 | 75 | const SubTypeMenuNav: NavigationComponent = { 76 | Component: SubTypeMenu, 77 | Label: (props: SubTypeMenuProps) => 78 | props.typePage.Label(props), 79 | Icon: (props: SubTypeMenuProps) => 80 | props.typePage.Icon ? props.typePage.Icon(props) : null, 81 | }; 82 | 83 | export const navSubTypeMenu = ( 84 | props: SubTypeMenuProps, 85 | values?: any, 86 | subscriptions?: DefinitionSubscriptions[], 87 | ) => { 88 | return { 89 | component: SubTypeMenuNav, 90 | props: { ...props, nonce: v4() }, 91 | values, 92 | subscriptions, 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/menus/TabbedMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export interface TabbedPaneProps { 4 | tabs: string[]; 5 | activeTab?: number; 6 | /** 7 | * Tracks to see if this component needs to be refreshed 8 | * Useful if the tab needs to be updated but this component 9 | * is already mounted 10 | */ 11 | id?: string; 12 | children: React.ReactNode | React.ReactNode[]; 13 | onChange?: (index: number) => void; 14 | className?: string; 15 | } 16 | 17 | const TabbedMenu = (props: TabbedPaneProps) => { 18 | const tabKey = props.id || 'default'; 19 | const [id, setId] = useState(props.id); 20 | const [activeTab, setActiveTab] = useState({ 21 | [tabKey]: props.activeTab || 0, 22 | }); 23 | 24 | useEffect(() => { 25 | if (id !== tabKey) { 26 | setId(tabKey); 27 | 28 | if (activeTab[tabKey] === undefined) { 29 | setActiveTab({ ...activeTab, [tabKey]: props.activeTab || 0 }); 30 | } 31 | } 32 | }, [tabKey, id, activeTab, setActiveTab, setId, props.activeTab]); 33 | 34 | return ( 35 |
36 |
37 | {props.tabs.map((tab, index) => ( 38 | 56 | ))} 57 |
58 | {Array.isArray(props.children) 59 | ? props.children[activeTab[tabKey]] 60 | : props.children} 61 |
62 | ); 63 | }; 64 | 65 | export default TabbedMenu; 66 | -------------------------------------------------------------------------------- /src/components/menus/definitions/DefinitionsMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Formik } from 'formik'; 2 | import WorkflowIcon from '../../../icons/components/WorkflowIcon'; 3 | import { WorkflowStage } from '../../../mappings/components/WorkflowMapping'; 4 | import { dataMappings } from '../../../mappings/GenerableMapping'; 5 | import InspectableMapping from '../../../mappings/InspectableMapping'; 6 | import { useStoreActions, useStoreState } from '../../../state/Hooks'; 7 | import { NavigationComponent } from '../../../state/Store'; 8 | import { Button } from '../../atoms/Button'; 9 | import { Footer } from '../../atoms/Footer'; 10 | import InspectorProperty from '../../atoms/form/InspectorProperty'; 11 | import { WorkflowSelector } from '../../atoms/WorkflowSelector'; 12 | import DefinitionsContainer from '../../containers/DefinitionsContainer'; 13 | import OrbImportsContainer from '../../containers/OrbImportsContainer'; 14 | import TabbedMenu from '../TabbedMenu'; 15 | 16 | /** 17 | * The main menu for inspecting the app's contents. 18 | */ 19 | const DefinitionsMenu = (props: { expanded: boolean[] }) => { 20 | const workflowGraphs = useStoreState((state) => state.definitions.workflows); 21 | const selectedWorkflowId = useStoreState((state) => state.selectedWorkflowId); 22 | const updateWorkflow = useStoreActions((actions) => actions.update_workflows); 23 | const config = useStoreState((state) => state.config); 24 | const updateConfig = useStoreActions((actions) => actions.generateConfig); 25 | const persistProps = useStoreActions((actions) => actions.persistProps); 26 | const workflow = workflowGraphs[selectedWorkflowId].value; 27 | 28 | return ( 29 |
33 |
34 | 35 |

{workflow.name}

36 | 37 |
38 | 39 |
43 | 44 | {dataMappings.map((mapping, index) => { 45 | const dataType = mapping.mapping as InspectableMapping; 46 | 47 | return ( 48 | { 52 | persistProps({ 53 | ...props, 54 | expanded: props.expanded.map((expanded, i) => 55 | i === index ? isExpanded : expanded, 56 | ), 57 | }); 58 | }} 59 | key={dataType.name.plural} 60 | /> 61 | ); 62 | })} 63 |
64 | {config && ( 65 | 74 | )} 75 |
76 |
77 |
78 | { 82 | updateWorkflow({ 83 | old: workflow, 84 | new: new WorkflowStage( 85 | values.name, 86 | workflow.id, 87 | workflow.jobs, 88 | workflow.when, 89 | workflow.elements, 90 | ), 91 | }); 92 | }} 93 | > 94 | {(formikProps) => ( 95 |
96 | 97 |
98 | 106 |
107 | 108 | )} 109 |
110 |
111 |
112 |
113 | ); 114 | }; 115 | 116 | const DefinitionsMenuNav: NavigationComponent = { 117 | Component: DefinitionsMenu, 118 | Label: (props: { expanded: boolean[] }, curProps) => ( 119 |

{curProps.overrideRoot || 'Definitions'}

120 | ), 121 | }; 122 | 123 | export default DefinitionsMenuNav; 124 | -------------------------------------------------------------------------------- /src/components/menus/definitions/subtypes/ExecutorTypePage.tsx: -------------------------------------------------------------------------------- 1 | import ExecutorIcon from '../../../../icons/components/ExecutorIcon'; 2 | import { NavigationComponent } from '../../../../state/Store'; 3 | import Card from '../../../atoms/Card'; 4 | import BreadCrumbs from '../../../containers/BreadCrumbs'; 5 | import { executorSubtypes } from '../../../containers/inspector/subtypes/ExecutorSubtypes'; 6 | import { SubTypeSelectPageProps } from '../../SubTypeMenu'; 7 | 8 | const ExecutorTypePage = (props: SubTypeSelectPageProps) => { 9 | return ( 10 |
11 |
12 | 13 |
14 | 15 |

New Executor

16 |
17 |
18 |
21 | TYPE 22 |
23 |
24 |
25 |
26 | {Object.keys(executorSubtypes).map((subtype) => ( 27 | } 30 | description={executorSubtypes[subtype].description} 31 | title={executorSubtypes[subtype].text} 32 | onClick={() => { 33 | props.setSubtype(subtype); 34 | }} 35 | /> 36 | ))} 37 |
38 |
39 | ); 40 | }; 41 | 42 | const ExecutorTypePageNav: NavigationComponent = { 43 | Component: ExecutorTypePage, 44 | Label: (props: SubTypeSelectPageProps) =>

New Executor

, 45 | Icon: (props: SubTypeSelectPageProps) => ( 46 | 47 | ), 48 | }; 49 | 50 | export default ExecutorTypePageNav; 51 | -------------------------------------------------------------------------------- /src/components/menus/definitions/subtypes/ParameterTypePage.tsx: -------------------------------------------------------------------------------- 1 | import ParameterIcon from '../../../../icons/components/ParameterIcon'; 2 | import InspectableMapping from '../../../../mappings/InspectableMapping'; 3 | import { NavigationComponent } from '../../../../state/Store'; 4 | import Card from '../../../atoms/Card'; 5 | import BreadCrumbs from '../../../containers/BreadCrumbs'; 6 | import { 7 | componentParametersSubtypes, 8 | parameterSubtypes, 9 | } from '../../../containers/inspector/subtypes/ParameterSubtypes'; 10 | import { SubTypeSelectPageProps } from '../../SubTypeMenu'; 11 | 12 | const ParameterTypePage = ( 13 | props: SubTypeSelectPageProps & { component: InspectableMapping }, 14 | ) => { 15 | const parameters = 16 | props.component?.parameters || componentParametersSubtypes.pipeline; 17 | 18 | return ( 19 |
20 |
21 | {/* */} 22 | 23 |
24 | 25 |

New Parameter

26 |
27 |
28 |
31 | TYPE 32 |
33 |
34 |
35 |
36 | {parameters?.types && 37 | parameters.types.map((subtype: any) => ( 38 | { 43 | props.setSubtype(subtype); 44 | }} 45 | pinned={ 46 |
47 | {parameterSubtypes[subtype].docsLink && ( 48 | { 53 | e.stopPropagation(); 54 | }} 55 | > 56 | Learn More 57 | 58 | )} 59 |
60 | } 61 | /> 62 | // 63 | ))} 64 |
65 |
66 | ); 67 | }; 68 | 69 | const ParameterTypePageNav: NavigationComponent = { 70 | Component: ParameterTypePage, 71 | Label: (props: SubTypeSelectPageProps) =>

New Parameter

, 72 | Icon: (props: SubTypeSelectPageProps) => ( 73 | 74 | ), 75 | }; 76 | 77 | export default ParameterTypePageNav; 78 | -------------------------------------------------------------------------------- /src/components/menus/definitions/subtypes/StepTypePage.tsx: -------------------------------------------------------------------------------- 1 | import { useStoreState } from '../../../../state/Hooks'; 2 | import { NavigationComponent } from '../../../../state/Store'; 3 | import BreadCrumbs from '../../../containers/BreadCrumbs'; 4 | import { commandSubtypes } from '../../../containers/inspector/subtypes/CommandSubtypes'; 5 | import TabbedMenu from '../../TabbedMenu'; 6 | import { SubTypeSelectPageProps } from '../../SubTypeMenu'; 7 | import { mapDefinitions } from '../../../../state/DefinitionStore'; 8 | import { orb, reusable } from '@circleci/circleci-config-sdk'; 9 | import Card from '../../../atoms/Card'; 10 | import { Empty } from '../../../atoms/Empty'; 11 | import OrbIcon from '../../../../icons/components/OrbIcon'; 12 | import CommandIcon from '../../../../icons/components/CommandIcon'; 13 | import CollapsibleList from '../../../containers/CollapsibleList'; 14 | import { CommandParameterLiteral } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Parameters/types/CustomParameterLiterals.types'; 15 | import { OrbImportWithMeta } from '../OrbDefinitionsMenu'; 16 | 17 | const StepTypePage = ( 18 | props: SubTypeSelectPageProps< 19 | string | reusable.ReusableCommand | orb.OrbRef 20 | >, 21 | ) => { 22 | const definitions = useStoreState((state) => state.definitions); 23 | 24 | return ( 25 |
26 |
27 | {/* */} 28 | 29 |

Step Type

30 |
31 | 32 |
33 | {Object.entries(commandSubtypes).map(([name, subtype]) => ( 34 | { 39 | props.setSubtype(name); 40 | }} 41 | pinned={ 42 | <> 43 | {subtype.docsLink && ( 44 | { 49 | e.stopPropagation(); 50 | }} 51 | > 52 | Learn More 53 | 54 | )} 55 | 56 | } 57 | /> 58 | ))} 59 |
60 |
61 | {Object.values(definitions.commands).length > 0 ? ( 62 | mapDefinitions( 63 | definitions.commands, 64 | (command) => ( 65 | 78 | ), 79 | ) 80 | ) : ( 81 | 86 | )} 87 |
88 |
89 | {Object.values(definitions.orbs).length > 0 ? ( 90 | mapDefinitions(definitions.orbs, (orb) => ( 91 | 94 | {orb.version} 95 |

96 | } 97 | alwaysShowPinned 98 | title={ 99 |
100 | {`${orb.name} 105 |

106 | {orb.namespace}/ 107 |

108 | {orb.name} 109 |
110 | } 111 | > 112 |
113 | {orb.commands && 114 | Object.values(orb.commands)?.map((command) => ( 115 | { 119 | props.setSubtype(command); 120 | }} 121 | /> 122 | ))} 123 |
124 |
125 | )) 126 | ) : ( 127 | 132 | )} 133 |
134 |
135 |
136 | ); 137 | }; 138 | 139 | const StepTypePageNav: NavigationComponent = { 140 | Component: StepTypePage, 141 | Label: () =>

New Step

, 142 | }; 143 | 144 | export default StepTypePageNav; 145 | -------------------------------------------------------------------------------- /src/components/menus/stage/StagedFilterMenu.tsx: -------------------------------------------------------------------------------- 1 | import { FilterParameter } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Parameters/types'; 2 | import { WorkflowJob } from '@circleci/circleci-config-sdk/dist/src/lib/Components/Workflow'; 3 | import { Form, Formik, FormikValues } from 'formik'; 4 | import { useStoreActions } from '../../../state/Hooks'; 5 | import { NavigationComponent } from '../../../state/Store'; 6 | import { Button } from '../../atoms/Button'; 7 | import ListProperty, { 8 | ListItemChildProps, 9 | } from '../../atoms/form/ListProperty'; 10 | import BreadCrumbs from '../../containers/BreadCrumbs'; 11 | import TabbedMenu from '../TabbedMenu'; 12 | 13 | type WorkflowJobMenuProps = { 14 | job: WorkflowJob; 15 | values: any; 16 | }; 17 | 18 | type FilterListProps = { 19 | type: 'Only' | 'Ignore'; 20 | } & FormikValues; 21 | 22 | const FilterItem = ({ item, setValue }: ListItemChildProps) => { 23 | return ( 24 |