├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── github_config_load.png ├── github_connect.png ├── homepage.png ├── logo.png └── visualized_file.png ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.tsx ├── components │ ├── CodeEditor │ │ └── CodeEditor.tsx │ ├── GitHub │ │ └── GitHub.tsx │ ├── PropertiesPane │ │ ├── CommandProperties │ │ │ └── CommandProperties.tsx │ │ ├── ExecutorProperties │ │ │ └── ExecutorProperties.tsx │ │ ├── JobProperties │ │ │ ├── EnvironmentVars.tsx │ │ │ ├── JobProperties.tsx │ │ │ ├── Parameters.tsx │ │ │ └── Steps.tsx │ │ ├── OrbProperties │ │ │ └── OrbProperties.tsx │ │ ├── PropertiesPane.tsx │ │ └── WorkflowProperties │ │ │ ├── Jobs.tsx │ │ │ └── WorkflowProperties.tsx │ ├── ToolBar │ │ └── ToolBar.tsx │ ├── VisualEditor │ │ ├── CustomNodes │ │ │ ├── CommandNode.tsx │ │ │ ├── ExecutorNode.tsx │ │ │ ├── JobNode.tsx │ │ │ ├── OrbNode.tsx │ │ │ ├── WorkflowNode.tsx │ │ │ ├── checkIfCommandExistsInJob.ts │ │ │ ├── checkIfExecutorExistsInJob.ts │ │ │ ├── checkIfJobExistsInWorkflow.ts │ │ │ └── getAllWorkflowsForAJob.ts │ │ └── VisualEditor.tsx │ └── Widgets │ │ ├── Buttons │ │ ├── IconOnlyButton.tsx │ │ ├── PrimaryButton.tsx │ │ └── SecondaryButton.tsx │ │ ├── ConfirmDialog │ │ └── ConfirmDialog.tsx │ │ ├── Divider │ │ └── Divider.tsx │ │ ├── InputBox │ │ └── InputBox.tsx │ │ ├── Loading │ │ └── Loading.tsx │ │ ├── Menu │ │ └── Menu.tsx │ │ ├── SelectBox │ │ └── SelectBox.tsx │ │ └── ToggleSwitch │ │ └── ToggleSwitch.tsx ├── data │ └── configReference.json ├── index.css ├── index.tsx ├── pages │ ├── Development │ │ └── Development.tsx │ └── Home │ │ └── Home.tsx ├── react-app-env.d.ts ├── redux │ ├── activeEntity │ │ └── activeEntitySlice.ts │ ├── darkMode │ │ └── darkModeSlice.ts │ ├── data │ │ └── dataSlice.ts │ ├── githubData │ │ └── githubDataSlice.ts │ ├── selectedEntity │ │ └── selectedEntitySlice.ts │ ├── store.ts │ └── visibleEntities │ │ └── visibleEntitiesSlice.ts └── utils │ ├── checkEmptyArray.ts │ ├── checkEmptyObj.ts │ ├── checkIfArray.ts │ └── objToArrayConverter.ts ├── tailwind.config.js ├── tsconfig.json └── types └── react-resizable-panels └── index.d.ts /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy app to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '20' 20 | 21 | - name: Install dependencies 22 | run: npm install --force 23 | 24 | - name: Build app 25 | run: npm run build 26 | env: 27 | CI: false 28 | 29 | - name: Deploy to GitHub Pages 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | /test 4 | 5 | # dependencies 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | .env -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ospo@syngenta.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | We want to make contributing to this repo as easy and transparent as possible. 3 | 4 | So let's go: 5 | 6 | 1. **Pushing directly to main branch isn't allowed.** Before contributing, make sure you are creating a new branch and making changes there. The branch name should be in this format: 7 | **`Feature/Bug_fix-Title_of_the_feature_or_bug_fix`**\ 8 | For example: `Feature-Added_Custom_Nodes`, `Bug_fix-Workflow_Node_fix`. These are some valid branching names which would help us understand your contributions better. 9 | 10 | 2. Add meaningful descriptions in your PRs to get a clear idea about the changes you are making. 11 | 12 | 3. We welcome any type of contribution(s), but any contribution(s) you make will be under the MIT software license! 13 | 14 | 4. Consider using Nodejs version `16` to avoid any further and existing dependency issues. 15 | 16 | 5. When submitting a bug, try to include a detailed description including the situation when it occurrs, the files or part of code causing it (if able to find), etc. 17 | 18 | 19 | 20 | ### Your Ideas are always welcomed! 💡 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Syngenta Group Co. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | [![Deploy app to GitHub Pages](https://github.com/syngenta/circleci-config-visualizer/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/syngenta/circleci-config-visualizer/actions/workflows/deploy.yml) 6 | [![Stars](https://img.shields.io/github/stars/syngenta/circleci-config-visualizer)](https://img.shields.io/github/stars/syngenta/circleci-config-visualizer) 7 | 8 | [![MIT License](https://img.shields.io/github/license/syngenta/circleci-config-visualizer)](https://img.shields.io/github/license/syngenta/circleci-config-visualizer) 9 | [![GitHub Release](https://img.shields.io/github/v/release/syngenta/circleci-config-visualizer)](https://img.shields.io/github/v/release/syngenta/circleci-config-visualizer) 10 | [![Open Issues](https://img.shields.io/github/issues/syngenta/circleci-config-visualizer)](https://img.shields.io/github/issues/syngenta/circleci-config-visualizer) 11 | [![Pull Requests](https://img.shields.io/github/issues-pr/syngenta/circleci-config-visualizer)](https://img.shields.io/github/issues-pr/syngenta/circleci-config-visualizer) 12 | 13 | [![Open Source](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://opensource.org/) 14 | 15 | 16 | # CircleCI Config Visualizer 17 |
18 | 19 |
20 | 21 | The `circleci-config-visualizer` is a webapp that can be used to visualize circlei `config.yml` files. It provides a clear overview of CI workflows and jobs in the config files. 22 | 23 | 24 | Built using `Reactjs`, `Redux`, and `Typescript`. 25 | 26 |
27 | 28 | ## How to use 📝 29 | 1. Go to **https://syngenta.github.io/circleci-config-visualizer** 30 | 31 | 2. Click on **Upload** button and choose your config file to visualize: 32 | 33 | ![Homepage](https://github.com/syngenta/circleci-config-visualizer/blob/main/images/homepage.png) 34 | 35 | 3. Alternatively, you can now connect your GitHub account using a Personal Access Token (PAT) and load config files directly from your repositories: 36 | 37 | ![GitHub Connect](https://github.com/syngenta/circleci-config-visualizer/blob/main/images/github_connect.png) 38 | 39 | If `config.yml` is present in the repo, the status will be shown. Click on **Load Config** button to load the config: 40 | 41 | ![GitHub Config Load](https://github.com/syngenta/circleci-config-visualizer/blob/main/images/github_config_load.png) 42 | 43 | 4. The file gets opened in a visual editor: 44 | 45 | ![Visualized file](https://github.com/syngenta/circleci-config-visualizer/blob/main/images/visualized_file.png) 46 | 47 |
48 | 49 | ## Folder structure 📁 50 | ``` 51 | my-app/ 52 | ├─ node_modules/ 53 | ├─ public/ 54 | ├─ src/ 55 | │ ├─ components/ 56 | | | ├─ ... 57 | | | ├─ Widgets/ 58 | │ ├─ data/ 59 | │ ├─ pages/ 60 | │ ├─ redux/ 61 | │ ├─ utils/ 62 | │ ├─ index.css 63 | │ ├─ index.tsx 64 | │ ├─ App.tsx 65 | │ ├─ App.css 66 | ├─ .gitignore 67 | ├─ package.json 68 | ├─ README.md 69 | ├─ ... 70 | ``` 71 | 72 | ### The `src` directory: 73 | 1. `components` - Contains all react components. Contains a `Widgets` folder where all reusable widgets like toggle switches, inputs, buttons, etc. are kept. 74 | 2. `data` - Contains all files that serve static data in the application. 75 | 3. `pages` - Contains actual pages that compose of components and are used in react router navigation. 76 | 4. `redux` - Contains all the files related to redux, store, slices, reducers, etc. 77 | 5. `utils` - Contains reusable utility functions which are frequently used anywhere and serve a specific purpose. 78 | 79 |
80 | 81 | ## Contributing 📌 82 | See **[CONTRIBUTING.md](./CONTRIBUTING.md)** file for understanding how to contribute. 83 | 84 |
85 | 86 | ## License 🔐 87 | This project is licensed under the MIT License. See the **[LICENSE](./LICENSE)** file for more details. -------------------------------------------------------------------------------- /images/github_config_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/images/github_config_load.png -------------------------------------------------------------------------------- /images/github_connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/images/github_connect.png -------------------------------------------------------------------------------- /images/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/images/homepage.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/images/logo.png -------------------------------------------------------------------------------- /images/visualized_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/images/visualized_file.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circleci-config-visualizer", 3 | "version": "0.1.0", 4 | "description": "A tool to visualize CircleCI config files", 5 | "homepage": "https://syngenta.github.io/circleci-config-visualizer", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "@monaco-editor/react": "^4.6.0", 10 | "@reduxjs/toolkit": "^2.2.7", 11 | "@testing-library/jest-dom": "^5.17.0", 12 | "@testing-library/react": "^13.4.0", 13 | "@testing-library/user-event": "^13.5.0", 14 | "@types/jest": "^27.5.2", 15 | "@types/js-yaml": "^4.0.9", 16 | "@types/node": "^16.18.104", 17 | "@types/react": "^18.3.3", 18 | "@types/react-dom": "^18.3.0", 19 | "@types/react-router": "^5.1.20", 20 | "@types/react-router-dom": "^5.3.3", 21 | "@xyflow/react": "^12.0.4", 22 | "aos": "^2.3.4", 23 | "axios": "^1.7.9", 24 | "buffer": "^6.0.3", 25 | "html-to-image": "^1.11.11", 26 | "js-yaml": "^4.1.0", 27 | "monaco-themes": "^0.4.4", 28 | "octokit": "^4.0.2", 29 | "react": "^18.3.1", 30 | "react-dom": "^18.3.1", 31 | "react-icons": "^5.2.1", 32 | "react-json-view": "^1.21.3", 33 | "react-loader-spinner": "^6.1.6", 34 | "react-redux": "^9.1.2", 35 | "react-resizable-panels": "^2.0.22", 36 | "react-router": "^6.26.1", 37 | "react-router-dom": "^6.26.1", 38 | "react-scripts": "5.0.1", 39 | "react-select": "^5.8.0", 40 | "redux": "^5.0.1", 41 | "typescript": "^4.9.5", 42 | "web-vitals": "^2.1.4" 43 | }, 44 | "scripts": { 45 | "start": "react-scripts start", 46 | "build": "react-scripts build", 47 | "test": "react-scripts test", 48 | "eject": "react-scripts eject" 49 | }, 50 | "eslintConfig": { 51 | "extends": [ 52 | "react-app", 53 | "react-app/jest" 54 | ] 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 23 | 24 | 33 | CircleCI Config Visualizer 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syngenta/circleci-config-visualizer/bd0c6f9b5599088559a43885ed815c34b123fffa/src/App.css -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./App.css"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import Home from "./pages/Home/Home"; 5 | import { Routes, Route, HashRouter } from "react-router-dom"; 6 | import ToolBar from "./components/ToolBar/ToolBar"; 7 | import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; 8 | import VisualEditor from "./components/VisualEditor/VisualEditor"; 9 | import PropertiesPane from "./components/PropertiesPane/PropertiesPane"; 10 | import CodeEditor from "./components/CodeEditor/CodeEditor"; 11 | import { getDarkMode, setDarkMode } from "./redux/darkMode/darkModeSlice"; 12 | import AOS from "aos"; 13 | import "aos/dist/aos.css"; 14 | import { setDataReducer } from "./redux/data/dataSlice"; 15 | import Development from "./pages/Development/Development"; 16 | import { ReactFlowProvider } from "@xyflow/react"; 17 | import { setGithubData } from "./redux/githubData/githubDataSlice"; 18 | import { Buffer } from "buffer"; 19 | AOS.init(); 20 | 21 | function App() { 22 | const [takingScreenshot, setTakingScreenshot] = useState(false); 23 | const dispatch = useDispatch(); 24 | const darkMode = useSelector(getDarkMode); 25 | 26 | useEffect(() => { 27 | const currentFile = localStorage.getItem("currentFile"); 28 | currentFile && dispatch(setDataReducer(JSON.parse(currentFile))); 29 | 30 | if (localStorage.getItem("darkMode") === "true") { 31 | dispatch(setDarkMode(true)); 32 | document.documentElement.classList.add("dark"); 33 | } else { 34 | dispatch(setDarkMode(false)); 35 | document.documentElement.classList.remove("dark"); 36 | } 37 | const githubData = localStorage.getItem("githubData"); 38 | if (githubData) { 39 | dispatch( 40 | setGithubData( 41 | Buffer.from(githubData, "base64").toString("binary") 42 | ) 43 | ); 44 | } else { 45 | dispatch(setGithubData(null)); 46 | } 47 | }, []); 48 | 49 | return ( 50 |
51 | 52 | 53 | } /> 54 | } /> 55 | 59 | 63 | 64 | 65 |
66 |
67 | 68 | 72 | 73 |
74 | 75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 |
84 | 85 | } 86 | /> 87 |
88 |
89 |
90 | ); 91 | } 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /src/components/CodeEditor/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import Editor, { useMonaco } from "@monaco-editor/react"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | getAllCommands, 5 | getAllExecutors, 6 | getAllJobs, 7 | getAllOrbs, 8 | getAllWorkflows, 9 | } from "../../redux/data/dataSlice"; 10 | import yaml from "js-yaml"; 11 | import checkEmptyObj from "../../utils/checkEmptyObj"; 12 | import { getDarkMode } from "../../redux/darkMode/darkModeSlice"; 13 | 14 | type Code = { 15 | version: string | number; 16 | executors?: any; 17 | orbs?: any; 18 | commands?: any; 19 | jobs?: any; 20 | workflows?: any; 21 | }; 22 | 23 | export default function CodeEditor() { 24 | const monaco: any = useMonaco(); 25 | const orbs = useSelector(getAllOrbs); 26 | const executors = useSelector(getAllExecutors); 27 | const commands = useSelector(getAllCommands); 28 | const jobs = useSelector(getAllJobs); 29 | const workflows = useSelector(getAllWorkflows); 30 | const darkMode = useSelector(getDarkMode); 31 | 32 | const generateCode = (): string | Code => { 33 | const orbsObj = {}, 34 | executorsObj = {}, 35 | commandsObj = {}, 36 | jobsObj = {}, 37 | workflowsObj = {}; 38 | orbs.map((orb) => { 39 | orbsObj[orb[0]] = orb[1]; 40 | }); 41 | executors.map((executor) => { 42 | executorsObj[executor[0]] = executor[1]; 43 | }); 44 | commands.map((command) => { 45 | commandsObj[command[0]] = command[1]; 46 | }); 47 | jobs.map((job) => { 48 | jobsObj[job[0]] = job[1]; 49 | }); 50 | workflows.map((workflow) => { 51 | workflowsObj[workflow[0]] = workflow[1]; 52 | }); 53 | const code: Code = { 54 | version: 2.1, 55 | orbs: orbsObj, 56 | executors: executorsObj, 57 | commands: commandsObj, 58 | jobs: jobsObj, 59 | workflows: workflowsObj, 60 | }; 61 | checkEmptyObj(orbsObj) && delete code.orbs; 62 | checkEmptyObj(executorsObj) && delete code.executors; 63 | checkEmptyObj(commandsObj) && delete code.commands; 64 | checkEmptyObj(jobsObj) && delete code.jobs; 65 | checkEmptyObj(workflowsObj) && delete code.workflows; 66 | return yaml.dump(code, { 67 | noArrayIndent: false, 68 | lineWidth: -1, 69 | noRefs: true, 70 | }); 71 | }; 72 | 73 | // function handleEditorChange(value: any, event: any) { 74 | // console.log("here is the current model value:", value); 75 | // } 76 | 77 | return ( 78 |
79 | 86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/GitHub/GitHub.tsx: -------------------------------------------------------------------------------- 1 | import { Octokit } from "octokit"; 2 | import { Buffer } from "buffer"; 3 | import { VscDebugDisconnect } from "react-icons/vsc"; 4 | import { FaSave } from "react-icons/fa"; 5 | import InputBox from "../Widgets/InputBox/InputBox"; 6 | import { useEffect, useState } from "react"; 7 | import PrimaryButton from "../Widgets/Buttons/PrimaryButton"; 8 | import { useSelector, useDispatch } from "react-redux"; 9 | import { 10 | getGithubData, 11 | setGithubData, 12 | } from "../../redux/githubData/githubDataSlice"; 13 | import { PiPlugsConnectedFill } from "react-icons/pi"; 14 | import { FaCircleCheck } from "react-icons/fa6"; 15 | import { FaDownload } from "react-icons/fa"; 16 | import { MdSmsFailed } from "react-icons/md"; 17 | import { setDataReducer } from "../../redux/data/dataSlice"; 18 | import { useNavigate } from "react-router"; 19 | import yaml from "js-yaml"; 20 | import { IoMdArrowRoundBack } from "react-icons/io"; 21 | import IconOnlyButton from "../Widgets/Buttons/IconOnlyButton"; 22 | import SecondaryButton from "../Widgets/Buttons/SecondaryButton"; 23 | import { IoClose } from "react-icons/io5"; 24 | import Loading from "../Widgets/Loading/Loading"; 25 | import { HiOutlineRefresh } from "react-icons/hi"; 26 | 27 | type GitHubProps = { 28 | viewGithubWindow: boolean; 29 | setViewGithubWindow: React.Dispatch>; 30 | }; 31 | 32 | export default function GitHub({ 33 | viewGithubWindow, 34 | setViewGithubWindow, 35 | }: GitHubProps) { 36 | const [repos, setRepos] = useState([]); 37 | const [filteredRepos, setFilteredRepos] = useState([]); 38 | const [searchedRepo, setSearchedRepo] = useState(""); 39 | const [selectedRepo, setSelectedRepo] = useState({ 40 | name: null, 41 | config: null, 42 | description: null, 43 | private: null, 44 | }); 45 | const [PAT, setPAT] = useState(""); 46 | const [invalidPAT, setInvalidPAT] = useState(false); 47 | const [loading, setLoading] = useState(false); 48 | const githubData = useSelector(getGithubData); 49 | const dispatch = useDispatch(); 50 | const navigate = useNavigate(); 51 | 52 | useEffect(() => { 53 | const cachedGithubRepos = localStorage.getItem("cachedGithubRepos"); 54 | if (githubData?.token) { 55 | if (cachedGithubRepos) { 56 | setRepos(JSON.parse(cachedGithubRepos)); 57 | setFilteredRepos(JSON.parse(cachedGithubRepos)); 58 | } else { 59 | getAllRepos(); 60 | } 61 | } 62 | }, [githubData?.token]); 63 | 64 | const getAllRepos = async () => { 65 | setLoading(true); 66 | const octokit = new Octokit({ auth: githubData.token }); 67 | const data = await octokit.paginate(`GET /user/repos`, { 68 | per_page: 100, 69 | type: "all", 70 | // visibility: "private", 71 | sort: "updated", 72 | headers: { 73 | "X-GitHub-Api-Version": "2022-11-28", 74 | }, 75 | }); 76 | setLoading(false); 77 | setRepos(data); 78 | setFilteredRepos(data); 79 | localStorage.setItem("cachedGithubRepos", JSON.stringify(data)); 80 | }; 81 | 82 | const readRepoConfig = async ( 83 | repo: string, 84 | description: string, 85 | isPrivate: boolean, 86 | owner: string 87 | ) => { 88 | setLoading(true); 89 | const octokit = new Octokit({ auth: githubData.token }); 90 | 91 | try { 92 | const data = await octokit.request( 93 | `GET /repos/${owner}/${repo}/contents/.circleci/config.yml`, 94 | { 95 | owner: owner, 96 | repo: repo, 97 | path: ".circleci/config.yml", 98 | headers: { 99 | "X-GitHub-Api-Version": "2022-11-28", 100 | }, 101 | } 102 | ); 103 | const decodedFile = Buffer.from(data.data.content, "base64").toString( 104 | "binary" 105 | ); 106 | setSelectedRepo({ 107 | name: repo, 108 | config: decodedFile, 109 | description: description, 110 | private: isPrivate, 111 | }); 112 | setLoading(false); 113 | } catch (e: any) { 114 | setLoading(false); 115 | if (e.message.includes("Error reading file: Not Found")) { 116 | setSelectedRepo({ 117 | name: repo, 118 | config: null, 119 | description: description, 120 | private: isPrivate, 121 | }); 122 | } 123 | } 124 | }; 125 | 126 | const saveGitHubToken = async () => { 127 | try { 128 | const octokit = new Octokit({ auth: PAT }); 129 | setInvalidPAT(false); 130 | const userData = await octokit.request("GET /user", { 131 | headers: { 132 | "X-GitHub-Api-Version": "2022-11-28", 133 | }, 134 | }); 135 | const data = JSON.stringify({ 136 | token: PAT, 137 | username: userData.data.login, 138 | icon: userData.data.avatar_url, 139 | email: userData.data.email, 140 | }); 141 | dispatch(setGithubData(data)); 142 | const encodedData = Buffer.from(data, "binary").toString("base64"); 143 | localStorage.setItem("githubData", encodedData); 144 | } catch (e: any) { 145 | if (e.message.includes("Bad credentials")) { 146 | setInvalidPAT(true); 147 | } 148 | } 149 | }; 150 | 151 | return ( 152 |
155 |
160 | {selectedRepo.name && ( 161 | 167 | } 168 | onClick={() => { 169 | setSelectedRepo({ 170 | name: null, 171 | config: null, 172 | description: null, 173 | private: null, 174 | }); 175 | }} 176 | /> 177 | )} 178 | 184 | } 185 | onClick={() => { 186 | setViewGithubWindow(false); 187 | }} 188 | /> 189 | {githubData ? ( 190 |
193 |
194 | 198 | 202 |
203 | } 207 | onClick={() => { 208 | localStorage.removeItem("githubData"); 209 | localStorage.removeItem("cachedGithubRepos"); 210 | dispatch(setGithubData(null)); 211 | setRepos([]); 212 | setFilteredRepos([]); 213 | }} 214 | /> 215 |
216 | ) : ( 217 | 218 | )} 219 | {githubData ? ( 220 |
221 |

222 | GitHub Profile Connected 223 |

224 |
225 |

226 | Username: 227 |

228 |

229 | {githubData.username} 230 |

231 |
232 |
233 |

234 | Email: 235 |

236 |

237 | {githubData.email} 238 |

239 |
240 |
241 | ) : ( 242 |
243 |

244 | Connect your GitHub Profile 245 |

246 |

247 | Connect your Account by adding your Personal Access Token (PAT) 248 | and sync config files directly from your GitHub 249 |

250 |
251 |
252 | { 257 | setPAT(e.target.value); 258 | }} 259 | disabled={githubData} 260 | /> 261 |
262 | } 264 | label={`Save`} 265 | className="px-4" 266 | color="bg-green-600" 267 | disabled={!PAT || githubData} 268 | onClick={saveGitHubToken} 269 | /> 270 |
271 |
272 | {invalidPAT && ( 273 |

274 | Invalid Token 275 |

276 | )} 277 |
278 |

279 | Create a Personal Access Token (PAT) with these scopes: 'repo', 280 | 'read:org' and 'user', in Developer settings in your GitHub 281 | account. 282 |

283 |

284 | Refer here:{" "} 285 | 290 | https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic 291 | 292 |

293 |
294 | )} 295 | 296 | {githubData && 297 | (!selectedRepo.name ? ( 298 |
299 |

300 | {`Found ${repos.length} repositories:`} 301 |

302 |
303 |
304 | { 309 | setSearchedRepo(e.target.value); 310 | setFilteredRepos( 311 | repos.filter( 312 | (repo: any) => 313 | repo.name 314 | .toLowerCase() 315 | .indexOf(e.target.value.toLowerCase()) !== -1 316 | ) 317 | ); 318 | }} 319 | placeholder="Search repo name here..." 320 | disabled={false} 321 | /> 322 |
323 | } onClick={()=>{setRepos([]);setFilteredRepos([]);getAllRepos();}} /> 324 |
325 |
330 | {loading && } 331 | {filteredRepos.map((repo: any) => ( 332 |
{ 335 | setSelectedRepo({ 336 | ...selectedRepo, 337 | name: repo.name, 338 | description: repo.description, 339 | private: repo.private, 340 | }); 341 | await readRepoConfig( 342 | repo.name, 343 | repo.description, 344 | repo.private, 345 | repo.owner.login 346 | ); 347 | }} 348 | > 349 |
350 |

351 | {repo.name} 352 |

353 | {repo.private && ( 354 |

355 | Private 356 |

357 | )} 358 |
359 |

360 | {repo.description} 361 |

362 |
363 | ))} 364 |
365 |
366 | ) : ( 367 |
368 |
369 |
370 |

371 | {selectedRepo.name} 372 |

373 | {selectedRepo.private && ( 374 |

375 | Private 376 |

377 | )} 378 |
379 |

380 | {selectedRepo.description} 381 |

382 |
383 | {loading && } 384 | {selectedRepo.config ? ( 385 |
386 |
387 |

388 | {`Found config.yml in the repo`} 389 |

390 | 391 |
392 | } 394 | label={`Load config`} 395 | className="px-4 mt-4" 396 | color="bg-green-600" 397 | onClick={() => { 398 | const yamlData = yaml.load(selectedRepo.config); 399 | localStorage.setItem( 400 | "currentFile", 401 | JSON.stringify(yamlData) 402 | ); 403 | dispatch(setDataReducer(yamlData)); 404 | navigate("/editor"); 405 | }} 406 | /> 407 |
408 | ) : ( 409 | !loading && ( 410 |
411 |

412 | {`config.yml not found in the repo`} 413 |

414 | 415 |
416 | ) 417 | )} 418 |
419 | ))} 420 |
421 |
422 | ); 423 | } 424 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/CommandProperties/CommandProperties.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { getSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 3 | import ReactJson from "react-json-view"; 4 | import { getDarkMode } from "../../../redux/darkMode/darkModeSlice"; 5 | 6 | export default function CommandProperties() { 7 | const selectedEntity = useSelector(getSelectedEntity); 8 | const darkMode = useSelector(getDarkMode); 9 | 10 | const commandName = selectedEntity.entity[0]; 11 | const commandData = selectedEntity.entity[1]; 12 | const commandDescription = commandData.description; 13 | const commandStep = commandData.steps[0]; 14 | 15 | return ( 16 |
17 |

18 | Commands 19 |

20 |
23 | 24 |

{commandName}

25 |
26 | <> 27 |

28 | {commandDescription} 29 |

30 |
31 | 42 |
43 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/ExecutorProperties/ExecutorProperties.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { getSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 3 | import InputBox from "../../Widgets/InputBox/InputBox"; 4 | 5 | export default function ExecutorProperties() { 6 | const selectedEntity = useSelector(getSelectedEntity); 7 | 8 | return ( 9 |
10 |

11 | Executor 12 |

13 |

14 | {selectedEntity.entity[0]} 15 |

16 |
17 |

18 | Image 19 |

20 | 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/JobProperties/EnvironmentVars.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import InputBox from "../../Widgets/InputBox/InputBox"; 3 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 4 | import checkEmptyArray from "../../../utils/checkEmptyArray"; 5 | 6 | type EnvironmentVarsProps = { 7 | selectedEntity: any; 8 | envVarsProps: any[]; 9 | parameters: any[]; 10 | }; 11 | 12 | export default function EnvironmentVars({ 13 | selectedEntity, 14 | envVarsProps, 15 | parameters, 16 | }: EnvironmentVarsProps) { 17 | const [envVars, setEnvVars] = useState([]); 18 | const [newEnvVar, setNewEnvVar] = useState<{ 19 | name?: string | undefined; 20 | value?: string | undefined; 21 | } | null>(null); 22 | const envVarsContainerRef = useRef(null); 23 | 24 | useEffect(() => { 25 | setNewEnvVar(null); 26 | }, [selectedEntity]); 27 | 28 | useEffect(() => { 29 | setEnvVars(objToArrayConverter(envVarsProps)); 30 | }, [envVarsProps]); 31 | 32 | useEffect(() => { 33 | if (envVarsContainerRef.current) { 34 | envVarsContainerRef.current.scrollTop = 35 | envVarsContainerRef.current.scrollHeight; 36 | } 37 | }, [newEnvVar]); 38 | 39 | return ( 40 | <> 41 | {checkEmptyArray(envVars) && ( 42 |
43 | 44 | Environment variables 45 | 46 |
50 | {envVars.map((env: any, key: number) => { 51 | return ( 52 |
56 | { 60 | setEnvVars( 61 | envVars.map((envVar) => { 62 | if (envVar === env) { 63 | return [e.target.value, env[1]]; 64 | } 65 | return envVar; 66 | }) 67 | ); 68 | }} 69 | /> 70 | >", "") 76 | ) 77 | ? "@" + env[1].split(".")[1].replace(">>", "") 78 | : env[1] 79 | } 80 | onKeyDown={(e) => { 81 | if ( 82 | e?.key === "Backspace" && 83 | (env[1].includes("< { 88 | if (envVar === env) { 89 | return [env[0], ""]; 90 | } 91 | return envVar; 92 | }) 93 | ); 94 | } 95 | }} 96 | onChange={(e) => { 97 | setEnvVars( 98 | envVars.map((envVar) => { 99 | if (envVar === env) { 100 | return [env[0], e.target.value]; 101 | } 102 | return envVar; 103 | }) 104 | ); 105 | }} 106 | style={ 107 | env[1].includes("< 116 |
117 | ); 118 | })} 119 |
120 |
121 | )} 122 | 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/JobProperties/JobProperties.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputBox from "../../Widgets/InputBox/InputBox"; 3 | import Divider from "../../Widgets/Divider/Divider"; 4 | import { useSelector } from "react-redux"; 5 | import { getSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 6 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 7 | import Parameters from "./Parameters"; 8 | import EnvironmentVars from "./EnvironmentVars"; 9 | import Steps from "./Steps"; 10 | 11 | export default function JobProperties() { 12 | const selectedEntity = useSelector(getSelectedEntity); 13 | const [tabs, setTabs] = useState(["Properties", "Steps"]); 14 | const [openTab, setOpenTab] = useState("Properties"); 15 | 16 | return ( 17 |
18 |

Job

19 |

20 | {selectedEntity.entity[0]} 21 |

22 |
23 |

24 | {selectedEntity.entity[1].executor ? "Executor:" : "Docker:"} 25 |

26 | {}} 34 | /> 35 |
36 |
37 | {tabs.map((tab, key) => ( 38 | 41 | ))} 42 |
43 | 44 | {openTab === "Properties" ? ( 45 | <> 46 | 50 | 51 | 52 | 53 | { 59 | return param[0]; 60 | })} 61 | /> 62 | 63 | ):} 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/JobProperties/Parameters.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import SelectBox from "../../Widgets/SelectBox/SelectBox"; 3 | import InputBox from "../../Widgets/InputBox/InputBox"; 4 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 5 | import checkEmptyArray from "../../../utils/checkEmptyArray"; 6 | 7 | const parameterTypes = [ 8 | { value: "string", label: "string" }, 9 | { value: "integer", label: "integer" }, 10 | { value: "boolean", label: "boolean" }, 11 | ]; 12 | 13 | type ParametersProps = { 14 | selectedEntity: any; 15 | parametersProps: any[]; 16 | }; 17 | 18 | export default function Parameters({ 19 | parametersProps, 20 | selectedEntity, 21 | }: ParametersProps) { 22 | const [parameters, setParameters] = useState([]); 23 | const [newParameter, setNewParameter] = useState<{ 24 | name?: string | undefined; 25 | type?: string | undefined; 26 | } | null>(null); 27 | const paramsContainerRef = useRef(null); 28 | 29 | useEffect(() => { 30 | setNewParameter(null); 31 | }, [selectedEntity]); 32 | 33 | useEffect(() => { 34 | setParameters(objToArrayConverter(parametersProps)); 35 | }, [parametersProps]); 36 | 37 | useEffect(() => { 38 | if (paramsContainerRef.current) { 39 | paramsContainerRef.current.scrollTop = 40 | paramsContainerRef.current.scrollHeight; 41 | } 42 | }, [newParameter]); 43 | return ( 44 | <> 45 | {checkEmptyArray(parameters) && ( 46 |
47 | 48 | Parameters 49 | 50 |
54 | {parameters.length 55 | ? parameters.map((param: any, key: number) => ( 56 |
60 | { 64 | setParameters( 65 | parameters.map((parameter) => { 66 | if (parameter === param) { 67 | return [e.target.value, param[1]]; 68 | } 69 | return parameter; 70 | }) 71 | ); 72 | }} 73 | /> 74 | { 78 | setParameters( 79 | parameters.map((parameter) => { 80 | if (parameter === param) { 81 | return [param[0], { type: e.value }]; 82 | } 83 | return parameter; 84 | }) 85 | ); 86 | }} 87 | /> 88 |
89 | )) 90 | : null} 91 |
92 |
93 | )} 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/JobProperties/Steps.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import ReactJson from "react-json-view"; 3 | import { useSelector } from "react-redux"; 4 | import { getDarkMode } from "../../../redux/darkMode/darkModeSlice"; 5 | 6 | type StepsProps = { 7 | selectedEntity: any; 8 | stepsProps: any[]; 9 | }; 10 | 11 | export default function Steps({ stepsProps, selectedEntity }: StepsProps) { 12 | const [steps, setSteps] = useState([]); 13 | const [newStep, setNewStep] = useState<{ type?: string; step?: any } | null>( 14 | null 15 | ); 16 | const [menuPosition, setMenuPosition] = useState<{ 17 | x: number; 18 | y: number; 19 | } | null>(null); 20 | const darkMode = useSelector(getDarkMode); 21 | const stepsContainerRef = useRef(null); 22 | 23 | const setMenuItems = () => { 24 | const menuItems = [ 25 | { 26 | label: "Orb step", 27 | onClick: () => { 28 | setNewStep({ type: "orb", step: {} }); 29 | setMenuPosition(null); 30 | }, 31 | }, 32 | { 33 | label: "Custom step", 34 | onClick: () => { 35 | setNewStep({ 36 | type: "custom", 37 | step: { run: { name: "", command: "" } }, 38 | }); 39 | setMenuPosition(null); 40 | }, 41 | }, 42 | ]; 43 | return menuItems; 44 | }; 45 | 46 | useEffect(() => { 47 | setSteps(stepsProps); 48 | }, [stepsProps]); 49 | 50 | useEffect(() => { 51 | setNewStep(null); 52 | }, [selectedEntity]); 53 | 54 | useEffect(() => { 55 | if (stepsContainerRef.current) { 56 | stepsContainerRef.current.scrollTop = 57 | stepsContainerRef.current.scrollHeight; 58 | } 59 | }, [newStep]); 60 | 61 | return ( 62 |
63 |
67 | {steps.map((step) => { 68 | return ( 69 |
70 | {typeof step === "string" ? ( 71 |

72 | {step} 73 |

74 | ) : ( 75 |
76 | 87 |
88 | )} 89 |
90 | ); 91 | })} 92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/OrbProperties/OrbProperties.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import IconOnlyButton from "../../Widgets/Buttons/IconOnlyButton"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { getSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 5 | import { IoMdOpen } from "react-icons/io"; 6 | import { setOrbsReducer } from "../../../redux/data/dataSlice"; 7 | 8 | export default function OrbProperties() { 9 | const selectedEntity = useSelector(getSelectedEntity); 10 | const [orbs, setOrbs] = useState(selectedEntity.entity); 11 | const [newOrb, setNewOrb] = useState(null); 12 | const dispatch = useDispatch(); 13 | const orbsContainerRef = useRef(null); 14 | 15 | const getOrbName = (orbName: string) => { 16 | return orbName.split("/")[1].split("@")[0]; 17 | }; 18 | 19 | const getOrbSourceAndName = (orbName: string) => { 20 | return orbName.split("@")[0]; 21 | }; 22 | 23 | useEffect(() => { 24 | if (orbsContainerRef.current && newOrb !== null) { 25 | orbsContainerRef.current.scrollTop = 26 | orbsContainerRef.current.scrollHeight; 27 | } 28 | }, [newOrb]); 29 | 30 | useEffect(() => { 31 | dispatch(setOrbsReducer(orbs)); 32 | }, [orbs]); 33 | 34 | return ( 35 |
36 |

37 | Orbs 38 |

39 |
43 |
44 | {orbs.map((orb: string[], key: number) => { 45 | return ( 46 |
47 |
48 |

49 | {orb[0]} 50 |

51 |

52 | {orb[1]} 53 |

54 |
55 | 68 |
69 | ); 70 | })} 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/PropertiesPane.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { getSelectedEntity } from "../../redux/selectedEntity/selectedEntitySlice"; 3 | import JobProperties from "./JobProperties/JobProperties"; 4 | import OrbProperties from "./OrbProperties/OrbProperties"; 5 | import ExecutorProperties from "./ExecutorProperties/ExecutorProperties"; 6 | import WorkflowProperties from "./WorkflowProperties/WorkflowProperties"; 7 | import CommandProperties from "./CommandProperties/CommandProperties"; 8 | 9 | export default function PropertiesPane() { 10 | const selectedEntity = useSelector(getSelectedEntity); 11 | 12 | return ( 13 |
20 | {selectedEntity.type && selectedEntity.type === "job" && ( 21 | 22 | )} 23 | {selectedEntity.type && selectedEntity.type === "workflow" && ( 24 | 25 | )} 26 | {selectedEntity.type && selectedEntity.type === "orb" && ( 27 | 28 | )} 29 | {selectedEntity.type && selectedEntity.type === "executor" && ( 30 | 31 | )} 32 | {selectedEntity.type && selectedEntity.type === "command" && ( 33 | 34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/WorkflowProperties/Jobs.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { FaAngleDown, FaAngleUp } from "react-icons/fa6"; 3 | import SecondaryButton from "../../Widgets/Buttons/SecondaryButton"; 4 | import { MdVerified } from "react-icons/md"; 5 | import InputBox from "../../Widgets/InputBox/InputBox"; 6 | import { useSelector } from "react-redux"; 7 | import { getAllJobs } from "../../../redux/data/dataSlice"; 8 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 9 | import checkIfArray from "../../../utils/checkIfArray"; 10 | import checkEmptyObj from "../../../utils/checkEmptyObj"; 11 | import Divider from "../../Widgets/Divider/Divider"; 12 | import checkEmptyArray from "../../../utils/checkEmptyArray"; 13 | 14 | type JobsProps = { 15 | jobName: string; 16 | jobData: any; 17 | }; 18 | 19 | export default function Jobs({ jobName, jobData }: JobsProps) { 20 | const jobs = useSelector(getAllJobs); 21 | const [expand, setExpand] = useState(false); 22 | const contexts = 23 | jobData.context && 24 | (checkIfArray(jobData.context) ? jobData.context : [jobData.context]); 25 | const jobDepends = checkIfArray(jobData.requires) 26 | ? jobData.requires.map((job) => `${job}, `) 27 | : jobData.requires; 28 | const currentJobFilters = jobData.filters; 29 | const branchFilters = currentJobFilters?.branches; 30 | const tagFilters = currentJobFilters?.tags; 31 | const branchesAllowed: string[] = 32 | branchFilters?.only && 33 | (checkIfArray(branchFilters?.only) 34 | ? branchFilters?.only 35 | : [branchFilters?.only]); 36 | const branchesIgnored: string[] = 37 | branchFilters?.ignore && 38 | (checkIfArray(branchFilters?.ignore) 39 | ? branchFilters?.ignore 40 | : [branchFilters?.ignore]); 41 | const tagsAllowed: string[] = 42 | tagFilters?.only && 43 | (checkIfArray(tagFilters?.only) ? tagFilters?.only : [tagFilters?.only]); 44 | const tagsIgnored: string[] = 45 | tagFilters?.ignore && 46 | (checkIfArray(tagFilters?.ignore) 47 | ? tagFilters?.ignore 48 | : [tagFilters?.ignore]); 49 | 50 | const currentJob = jobs.filter((job: any) => job[0] === jobName)[0]; 51 | const currentJobParameters = currentJob 52 | ? objToArrayConverter(currentJob[1].parameters) 53 | : null; 54 | 55 | return ( 56 | <> 57 |
62 | {jobData.type === "approval" && ( 63 |
64 |

65 | Approval 66 |

67 | 68 |
69 | )} 70 | 71 |
72 |

73 | {jobName + (jobData.name ? ` (${jobData.name})` : ``)} 74 |

75 | {jobData.type !== "approval" && ( 76 | 80 | ) : ( 81 | 82 | ) 83 | } 84 | label={!expand ? "View" : "Hide"} 85 | onClick={() => { 86 | setExpand(!expand); 87 | }} 88 | className="p-0 h-[10px]" 89 | /> 90 | )} 91 |
92 |
93 | {expand && ( 94 | <> 95 | {jobData.requires && ( 96 |
97 |

98 | Depends on: 99 |

100 |

101 | {jobDepends} 102 |

103 |
104 | )} 105 | {contexts && ( 106 |
107 | 108 | Contexts 109 | 110 | {contexts.map((context: any, key: number) => ( 111 |
112 |

116 | {context} 117 |

118 |
119 | ))} 120 |
121 | )} 122 | 123 | {checkEmptyArray(currentJobParameters as any[]) && ( 124 |
125 | 126 | Parameters 127 | 128 | {currentJobParameters?.map((parameter: any, key: number) => { 129 | return ( 130 |
134 | 135 | 136 |
137 | ); 138 | })} 139 |
140 | )} 141 | 142 | {!checkEmptyObj(currentJobFilters) && ( 143 |
144 | 145 | Filters 146 | 147 | {branchesAllowed || branchesIgnored ? ( 148 |
149 |

150 | Branches : 151 |

152 | 153 |
154 | {!checkEmptyObj(branchesAllowed) && 155 | branchesAllowed.map((branch: any, key: number) => ( 156 |

160 | {branch} 161 |

162 | ))} 163 | {!checkEmptyObj(branchesIgnored) && 164 | branchesIgnored.map((branch: any, key: number) => ( 165 |

169 | {branch} 170 |

171 | ))} 172 |
173 |
174 | ) : null} 175 | 176 | {tagsAllowed || tagsAllowed ? ( 177 |
178 |

179 | Tags : 180 |

181 | 182 |
183 | {!checkEmptyObj(tagsAllowed) && 184 | tagsAllowed.map((tag: any, key: number) => ( 185 |

189 | {tag} 190 |

191 | ))} 192 | {!checkEmptyObj(tagsIgnored) && 193 | tagsIgnored.map((tag: any, key: number) => ( 194 |

198 | {tag} 199 |

200 | ))} 201 |
202 |
203 | ) : null} 204 |
205 | )} 206 | 207 | )} 208 |
209 | 210 | 211 | ); 212 | } 213 | -------------------------------------------------------------------------------- /src/components/PropertiesPane/WorkflowProperties/WorkflowProperties.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { getSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 3 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 4 | import Jobs from "./Jobs"; 5 | 6 | export default function WorkflowProperties() { 7 | const selectedEntity = useSelector(getSelectedEntity); 8 | 9 | return ( 10 |
11 |

12 | Workflow 13 |

14 |

15 | {selectedEntity.entity[0]} 16 |

17 |

18 | Jobs 19 |

20 |
21 | {selectedEntity.entity[1].jobs.map((job: any, key: number) => { 22 | const jobName = objToArrayConverter(job)[0][0]; 23 | const jobData = objToArrayConverter(job)[0][1]; 24 | return( 25 | 26 | ) 27 | })} 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ToolBar/ToolBar.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import SecondaryButton from "../Widgets/Buttons/SecondaryButton"; 3 | import { GoHome } from "react-icons/go"; 4 | import Menu from "../Widgets/Menu/Menu"; 5 | import { MdDarkMode, MdLightMode } from "react-icons/md"; 6 | import { IoCameraOutline, IoConstructOutline } from "react-icons/io5"; 7 | import { useNavigate } from "react-router-dom"; 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { getDarkMode, setDarkMode } from "../../redux/darkMode/darkModeSlice"; 10 | import IconOnlyButton from "../Widgets/Buttons/IconOnlyButton"; 11 | 12 | type ToolBarProps = { 13 | takingScreenshot: boolean; 14 | setTakingScreenshot: React.Dispatch>; 15 | }; 16 | 17 | export default function ToolBar({ 18 | takingScreenshot, 19 | setTakingScreenshot, 20 | }: ToolBarProps) { 21 | const [menuPosition, setMenuPosition] = useState<{ 22 | x: number; 23 | y: number; 24 | } | null>(null); 25 | const [menuItems, setMenuItems] = useState(null); 26 | const navigate = useNavigate(); 27 | const darkMode = useSelector(getDarkMode); 28 | const dispatch = useDispatch(); 29 | 30 | return ( 31 |
36 | {menuPosition && menuPosition?.x && menuPosition?.y && ( 37 | 43 | )} 44 |
45 | } 48 | onClick={() => { 49 | localStorage.removeItem("currentFile"); 50 | navigate("/"); 51 | }} 52 | className="" 53 | /> 54 |
55 |
56 | } 59 | onClick={() => { 60 | setTakingScreenshot(true); 61 | }} 62 | className="" 63 | /> 64 | } 67 | onClick={() => { 68 | navigate("/development"); 69 | }} 70 | className="" 71 | /> 72 |
73 |
74 | 79 | ) : ( 80 | 84 | ) 85 | } 86 | onClick={() => { 87 | localStorage.setItem("darkMode", JSON.stringify(!darkMode)); 88 | dispatch(setDarkMode(!darkMode)); 89 | document.documentElement.classList.contains("dark") 90 | ? document.documentElement.classList.remove("dark") 91 | : document.documentElement.classList.add("dark"); 92 | }} 93 | /> 94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/CommandNode.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, NodeToolbar, Position } from "@xyflow/react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { setSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 4 | import IconOnlyButton from "../../Widgets/Buttons/IconOnlyButton"; 5 | import { LuTableProperties } from "react-icons/lu"; 6 | import { 7 | getActiveEntity, 8 | setActiveEntity, 9 | } from "../../../redux/activeEntity/activeEntitySlice"; 10 | import checkIfCommandExistsInJob from "./checkIfCommandExistsInJob"; 11 | 12 | type CommandNodeProps = { 13 | selected: boolean | undefined; 14 | data: any; 15 | }; 16 | 17 | export default function CommandNode({ 18 | selected, 19 | data, 20 | }: CommandNodeProps | any) { 21 | const commandName: string = data.command[0]; 22 | const commandData: any = data.command[1]; 23 | const commandDescription: string = commandData.description; 24 | const dispatch = useDispatch(); 25 | const activeEntity = useSelector(getActiveEntity); 26 | 27 | return ( 28 | <> 29 | 33 |
34 | 37 | } 38 | onClick={() => { 39 | dispatch( 40 | setSelectedEntity({ type: "command", entity: data.command }) 41 | ); 42 | }} 43 | /> 44 |
45 |
46 | 47 |
{ 61 | dispatch(setActiveEntity({ type: "command", entity: data.command })); 62 | }} 63 | > 64 |

65 | Command 66 |

67 |
68 |
69 |

{commandName}

70 |

71 | {commandDescription} 72 |

73 |
74 |
75 |
76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/ExecutorNode.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, NodeToolbar, Position } from "@xyflow/react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { setSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 4 | import IconOnlyButton from "../../Widgets/Buttons/IconOnlyButton"; 5 | import { LuTableProperties } from "react-icons/lu"; 6 | import { 7 | getActiveEntity, 8 | setActiveEntity, 9 | } from "../../../redux/activeEntity/activeEntitySlice"; 10 | 11 | type ExecutorNodeProps = { 12 | selected: boolean | undefined; 13 | data: { 14 | label: string; 15 | executor: any; 16 | }; 17 | }; 18 | 19 | export default function ExecutorNode({ 20 | selected, 21 | data, 22 | }: ExecutorNodeProps | any) { 23 | const dispatch = useDispatch(); 24 | const executorName: string = data.executor[0]; 25 | const executorData: any = data.executor[1]; 26 | const activeEntity = useSelector(getActiveEntity); 27 | 28 | return ( 29 | <> 30 | 34 |
35 | 38 | } 39 | onClick={() => { 40 | dispatch( 41 | setSelectedEntity({ type: "executor", entity: data.executor }) 42 | ); 43 | }} 44 | /> 45 |
46 |
47 | 48 | 49 |
{ 65 | dispatch( 66 | setActiveEntity({ type: "executor", entity: data.executor }) 67 | ); 68 | }} 69 | > 70 |

71 | Executor 72 |

73 |

74 | {executorName} 75 |

76 |
77 |

78 | Image: 79 |

80 |

81 | {executorData?.docker[0]["image"]} 82 |

83 |
84 |
85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/JobNode.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, Position, NodeResizer, NodeToolbar } from "@xyflow/react"; 2 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import IconOnlyButton from "../../Widgets/Buttons/IconOnlyButton"; 5 | import { getAllJobs, getAllWorkflows } from "../../../redux/data/dataSlice"; 6 | import { setSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 7 | import { LuTableProperties } from "react-icons/lu"; 8 | import { 9 | getActiveEntity, 10 | setActiveEntity, 11 | } from "../../../redux/activeEntity/activeEntitySlice"; 12 | import { useState } from "react"; 13 | import checkIfJobExistsInWorkflow from "./checkIfJobExistsInWorkflow"; 14 | import SecondaryButton from "../../Widgets/Buttons/SecondaryButton"; 15 | import { FaAngleDown, FaAngleUp } from "react-icons/fa6"; 16 | import checkIfCommandExistsInJob from "./checkIfCommandExistsInJob"; 17 | 18 | type JobNodeProps = { 19 | selected: boolean | undefined; 20 | data: { 21 | label: string; 22 | job: any; 23 | }; 24 | }; 25 | 26 | export default function JobNode({ selected, data }: JobNodeProps | any) { 27 | const [activeJobs, setActiveJobs] = useState([]); 28 | const dispatch = useDispatch(); 29 | const activeEntity = useSelector(getActiveEntity); 30 | const workflows = useSelector(getAllWorkflows); 31 | const jobs = useSelector(getAllJobs); 32 | const jobName: string = data?.job?.[0]; 33 | const jobData: any = data?.job?.[1]; 34 | const executor = jobData?.executor 35 | ? jobData?.executor 36 | : jobData?.machine 37 | ? jobData?.machine?.image 38 | : jobData?.docker[0]["image"]; 39 | const parameters = jobData?.parameters 40 | ? objToArrayConverter(jobData?.parameters) 41 | : null; 42 | const envVariables = jobData?.environment 43 | ? objToArrayConverter(jobData?.environment) 44 | : null; 45 | const [expand, setExpand] = useState([]); 46 | 47 | return ( 48 | <> 49 | 53 |
54 | 57 | } 58 | onClick={() => { 59 | dispatch(setSelectedEntity({ type: "job", entity: data?.job })); 60 | }} 61 | /> 62 |
63 |
64 | 65 | 66 | 67 | 73 |
{ 91 | dispatch(setActiveEntity({ type: "job", entity: data?.job })); 92 | }} 93 | > 94 |

95 | Job 96 |

97 |

{jobName}

98 | 99 |
100 |

101 | {jobData?.docker ? "Docker:" : "Executor:"} 102 |

103 |

104 | {executor} 105 |

106 |
107 | {parameters && ( 108 |
115 | 116 | Parameters 117 | 118 | 122 | ) : ( 123 | 124 | ) 125 | } 126 | label={ 127 | !expand.includes(`${jobName}-parameters`) ? "View" : "Hide" 128 | } 129 | onClick={() => { 130 | !expand.includes(`${jobName}-parameters`) 131 | ? setExpand((prev) => [...prev, `${jobName}-parameters`]) 132 | : setExpand( 133 | expand.filter((item) => item !== `${jobName}-parameters`) 134 | ); 135 | }} 136 | className="absolute right-0 top-0 p-0 h-[10px]" 137 | /> 138 | {expand.includes(`${jobName}-parameters`) && 139 | parameters.map((param: any, key: number) => ( 140 |
144 |

145 | {param[0]} 146 |

147 |

{param[1]["type"]}

148 |
149 | ))} 150 |
151 | )} 152 | {envVariables && ( 153 |
160 | 161 | Environment vars 162 | 163 | 167 | ) : ( 168 | 169 | ) 170 | } 171 | label={!expand.includes(`${jobName}-envVars`) ? "View" : "Hide"} 172 | onClick={() => { 173 | !expand.includes(`${jobName}-envVars`) 174 | ? setExpand((prev) => [...prev, `${jobName}-envVars`]) 175 | : setExpand( 176 | expand.filter((item) => item !== `${jobName}-envVars`) 177 | ); 178 | }} 179 | className="absolute right-0 top-0 p-0 h-[10px]" 180 | /> 181 | {expand.includes(`${jobName}-envVars`) && 182 | envVariables.map((env: any, key: number) => ( 183 |
187 |

188 | {env[0]} 189 |

190 | {env[1].includes("< 192 | {"@" + env[1].split(".")[1].replace(">>", "")} 193 |

194 | ) : ( 195 |

{env[1]}

196 | )} 197 |
198 | ))} 199 |
200 | )} 201 |
202 | 203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/OrbNode.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, NodeToolbar, Position } from "@xyflow/react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { setSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 4 | import { getAllOrbs } from "../../../redux/data/dataSlice"; 5 | import IconOnlyButton from "../../Widgets/Buttons/IconOnlyButton"; 6 | import { LuTableProperties } from "react-icons/lu"; 7 | 8 | type OrbNodeProps = { 9 | selected: boolean | undefined; 10 | data: any; 11 | }; 12 | 13 | export default function OrbNode({ selected, data }: OrbNodeProps | any) { 14 | const orbs = useSelector(getAllOrbs); 15 | const dispatch = useDispatch(); 16 | 17 | return ( 18 | <> 19 | 23 |
24 | 27 | } 28 | onClick={() => { 29 | dispatch(setSelectedEntity({ type: "orb", entity: orbs })); 30 | }} 31 | /> 32 |
33 |
34 | 35 |
42 |

43 | Orbs 44 |

45 |
46 | {orbs?.map((orb, key) => { 47 | return ( 48 |
49 |

{orb[0]}

50 |

{orb[1]}

51 |
52 | ); 53 | })} 54 |
55 |
56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/WorkflowNode.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, NodeResizer, NodeToolbar, Position } from "@xyflow/react"; 2 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { setSelectedEntity } from "../../../redux/selectedEntity/selectedEntitySlice"; 5 | import { getAllWorkflows } from "../../../redux/data/dataSlice"; 6 | import IconOnlyButton from "../../Widgets/Buttons/IconOnlyButton"; 7 | import { LuTableProperties } from "react-icons/lu"; 8 | import checkIfArray from "../../../utils/checkIfArray"; 9 | import checkEmptyObj from "../../../utils/checkEmptyObj"; 10 | import { useState } from "react"; 11 | import { FaAngleUp, FaAngleDown } from "react-icons/fa6"; 12 | import SecondaryButton from "../../Widgets/Buttons/SecondaryButton"; 13 | import { MdVerified } from "react-icons/md"; 14 | import { 15 | getActiveEntity, 16 | setActiveEntity, 17 | } from "../../../redux/activeEntity/activeEntitySlice"; 18 | import checkIfWorkflowUsesJob from "./getAllWorkflowsForAJob"; 19 | 20 | type WorkflowNodeProps = { 21 | selected: boolean | undefined; 22 | data: { 23 | label: string; 24 | workflow: any; 25 | jobs: any; 26 | }; 27 | }; 28 | 29 | export default function WorkflowNode({ 30 | selected, 31 | data, 32 | }: WorkflowNodeProps | any) { 33 | const workflowName: string = data.workflow[0]; 34 | const workflowData: any = data.workflow[1]; 35 | const jobs = data.jobs; 36 | const dispatch = useDispatch(); 37 | const workflows = useSelector(getAllWorkflows); 38 | const activeEntity = useSelector(getActiveEntity); 39 | const [expand, setExpand] = useState([]); 40 | 41 | return ( 42 | <> 43 | 50 |
51 | 54 | } 55 | onClick={() => { 56 | dispatch( 57 | setSelectedEntity({ type: "workflow", entity: data.workflow }) 58 | ); 59 | }} 60 | /> 61 |
62 |
63 | 64 | 70 |
{ 88 | dispatch( 89 | setActiveEntity({ type: "workflow", entity: data.workflow }) 90 | ); 91 | }} 92 | > 93 |

94 | Workflow 95 |

96 |

97 | {workflowName} 98 |

99 |
100 | {workflowData?.jobs?.map((job: any, key: number) => { 101 | var jobArray, jobName, jobData; 102 | if (typeof job === "object") { 103 | jobArray = objToArrayConverter(job); 104 | jobName = jobArray[0][0]; 105 | jobData = jobArray[0][1]; 106 | } else { 107 | jobArray = [[job]]; 108 | jobName = job; 109 | jobData = [jobName, []]; 110 | } 111 | const currentJob = jobs.filter( 112 | (job: any) => job[0] === jobArray[0][0] 113 | )[0]; 114 | const currentJobParameters = currentJob 115 | ? currentJob[1].parameters 116 | : null; 117 | const currentJobContexts = 118 | jobData.context && 119 | (checkIfArray(jobData.context) 120 | ? jobData.context 121 | : [jobData.context]); 122 | const currentJobFilters = jobData.filters; 123 | const branchFilters = currentJobFilters?.branches; 124 | const tagFilters = currentJobFilters?.tags; 125 | const branchesAllowed: string[] = 126 | branchFilters?.only && 127 | (checkIfArray(branchFilters?.only) 128 | ? branchFilters?.only 129 | : [branchFilters?.only]); 130 | const branchesIgnored: string[] = 131 | branchFilters?.ignore && 132 | (checkIfArray(branchFilters?.ignore) 133 | ? branchFilters?.ignore 134 | : [branchFilters?.ignore]); 135 | const tagsAllowed: string[] = 136 | tagFilters?.only && 137 | (checkIfArray(tagFilters?.only) 138 | ? tagFilters?.only 139 | : [tagFilters?.only]); 140 | const tagsIgnored: string[] = 141 | tagFilters?.ignore && 142 | (checkIfArray(tagFilters?.ignore) 143 | ? tagFilters?.ignore 144 | : [tagFilters?.ignore]); 145 | return ( 146 |
150 |
151 |

152 | {jobData.name 153 | ? jobData.name + ` (${jobArray[0][0]})` 154 | : jobArray[0][0]} 155 |

156 | {jobData.type === "approval" && ( 157 | 158 | )} 159 |
160 | 161 | {currentJobContexts && ( 162 |
163 | 164 | Contexts 165 | 166 | {currentJobContexts.map((context: any, key: number) => ( 167 |

171 | {context} 172 |

173 | ))} 174 |
175 | )} 176 | {currentJobParameters && ( 177 |
184 | 185 | Parameters 186 | 187 | 191 | ) : ( 192 | 193 | ) 194 | } 195 | label={ 196 | !expand.includes(`${jobName}-parameters`) 197 | ? "View" 198 | : "Hide" 199 | } 200 | onClick={() => { 201 | !expand.includes(`${jobName}-parameters`) 202 | ? setExpand((prev) => [ 203 | ...prev, 204 | `${jobName}-parameters`, 205 | ]) 206 | : setExpand( 207 | expand.filter( 208 | (item) => item !== `${jobName}-parameters` 209 | ) 210 | ); 211 | }} 212 | className="absolute right-2 top-0 p-0 h-[10px]" 213 | /> 214 | {expand.includes(`${jobName}-parameters`) && 215 | objToArrayConverter(currentJobParameters).map( 216 | (params: any, key: number) => ( 217 |

221 | {params[0] + " : " + (jobData[params[0]] ?? "")} 222 |

223 | ) 224 | )} 225 |
226 | )} 227 | 228 | {!checkEmptyObj(currentJobFilters) && ( 229 |
236 | 237 | Filters 238 | 239 | 243 | ) : ( 244 | 245 | ) 246 | } 247 | label={ 248 | !expand.includes(`${jobName}-filters`) ? "View" : "Hide" 249 | } 250 | onClick={() => { 251 | !expand.includes(`${jobName}-filters`) 252 | ? setExpand((prev) => [...prev, `${jobName}-filters`]) 253 | : setExpand( 254 | expand.filter( 255 | (item) => item !== `${jobName}-filters` 256 | ) 257 | ); 258 | }} 259 | className="absolute right-2 top-0 p-0 h-[10px]" 260 | /> 261 | 262 | {(branchesAllowed || branchesIgnored) && 263 | expand.includes(`${jobName}-filters`) ? ( 264 |
265 |

266 | Branches : 267 |

268 | 269 |
270 | {!checkEmptyObj(branchesAllowed) && 271 | branchesAllowed.map((branch: any, key: number) => ( 272 |

276 | {branch} 277 |

278 | ))} 279 | {!checkEmptyObj(branchesIgnored) && 280 | branchesIgnored.map((branch: any, key: number) => ( 281 |

285 | {branch} 286 |

287 | ))} 288 |
289 |
290 | ) : null} 291 | 292 | {(tagsAllowed || tagsAllowed) && 293 | expand.includes(`${jobName}-filters`) ? ( 294 |
295 |

296 | Tags : 297 |

298 | 299 |
300 | {!checkEmptyObj(tagsAllowed) && 301 | tagsAllowed.map((tag: any, key: number) => ( 302 |

306 | {tag} 307 |

308 | ))} 309 | {!checkEmptyObj(tagsIgnored) && 310 | tagsIgnored.map((tag: any, key: number) => ( 311 |

315 | {tag} 316 |

317 | ))} 318 |
319 |
320 | ) : null} 321 |
322 | )} 323 |
324 | ); 325 | })} 326 |
327 |
328 | 329 | ); 330 | } 331 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/checkIfCommandExistsInJob.ts: -------------------------------------------------------------------------------- 1 | export default function checkIfCommandExistsInJob(commandName: string, job: any) { 2 | const jobSteps: any[] = job.steps; 3 | for (const jobStep of jobSteps){ 4 | if(jobStep===commandName) return true; 5 | } 6 | return false; 7 | } -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/checkIfExecutorExistsInJob.ts: -------------------------------------------------------------------------------- 1 | export default function checkIfExecutorExistsInJob( 2 | executorName: string, 3 | job: any 4 | ) { 5 | if (job.executor === executorName) { 6 | return true; 7 | } 8 | return false; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/checkIfJobExistsInWorkflow.ts: -------------------------------------------------------------------------------- 1 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 2 | 3 | export default function checkIfJobExistsInWorkflow( 4 | jobName: string, 5 | workflow: any 6 | ): boolean { 7 | for (const job of workflow.jobs) { 8 | if (typeof job === "object") { 9 | if (objToArrayConverter(job)[0][0] === jobName) { 10 | return true; 11 | } 12 | } else { 13 | if (job === jobName) return true; 14 | } 15 | } 16 | return false; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/VisualEditor/CustomNodes/getAllWorkflowsForAJob.ts: -------------------------------------------------------------------------------- 1 | import objToArrayConverter from "../../../utils/objToArrayConverter"; 2 | 3 | function getAllWorkflowsForAJob(workflows: any[], jobName: string) { 4 | const jobWorkflows: string[] = []; 5 | workflows.forEach(workflow=>{ 6 | const workflowName: string = workflow[0]; 7 | const workflowData: any = workflow[1]; 8 | const workflowJobs: any[] = workflowData?.jobs; 9 | workflowJobs?.forEach(workflowJob=>{ 10 | const jobArray = objToArrayConverter(workflowJob)[0]; 11 | if(jobArray[0]===jobName){ 12 | jobWorkflows.push(workflowName); 13 | } 14 | }); 15 | }); 16 | return jobWorkflows; 17 | } 18 | 19 | export default function checkIfWorkflowUsesJob(workflowName: string, workflows: any[], jobName: string) { 20 | const jobWorkflows = getAllWorkflowsForAJob(workflows, jobName); 21 | for (const workflow of jobWorkflows) { 22 | if (workflow === workflowName) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/VisualEditor/VisualEditor.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect, useRef } from "react"; 2 | import "@xyflow/react/dist/style.css"; 3 | import { 4 | ReactFlow, 5 | MiniMap, 6 | addEdge, 7 | type Node, 8 | type OnConnect, 9 | type NodeTypes, 10 | Background, 11 | BackgroundVariant, 12 | MarkerType, 13 | getNodesBounds, 14 | getViewportForBounds, 15 | useNodesState, 16 | useEdgesState, 17 | useReactFlow, 18 | } from "@xyflow/react"; 19 | import WorkflowNode from "./CustomNodes/WorkflowNode"; 20 | import JobNode from "./CustomNodes/JobNode"; 21 | import ExecutorNode from "./CustomNodes/ExecutorNode"; 22 | import { useDispatch, useSelector } from "react-redux"; 23 | import { 24 | getAllCommands, 25 | getAllExecutors, 26 | getAllJobs, 27 | getAllOrbs, 28 | getAllWorkflows, 29 | getData, 30 | } from "../../redux/data/dataSlice"; 31 | import objToArrayConverter from "../../utils/objToArrayConverter"; 32 | import OrbNode from "./CustomNodes/OrbNode"; 33 | import { 34 | getSelectedEntity, 35 | setSelectedEntity, 36 | } from "../../redux/selectedEntity/selectedEntitySlice"; 37 | import { getDarkMode } from "../../redux/darkMode/darkModeSlice"; 38 | import { 39 | getActiveEntity, 40 | setActiveEntity, 41 | } from "../../redux/activeEntity/activeEntitySlice"; 42 | import checkIfJobExistsInWorkflow from "./CustomNodes/checkIfJobExistsInWorkflow"; 43 | import { toPng } from "html-to-image"; 44 | import checkIfWorkflowUsesJob from "./CustomNodes/getAllWorkflowsForAJob"; 45 | import CommandNode from "./CustomNodes/CommandNode"; 46 | import checkIfCommandExistsInJob from "./CustomNodes/checkIfCommandExistsInJob"; 47 | import { 48 | getVisibleCommands, 49 | getVisibleExecutors, 50 | getVisibleJobs, 51 | setCommandsVisible, 52 | setExecutorsVisible, 53 | setJobsVisible, 54 | } from "../../redux/visibleEntities/visibleEntitiesSlice"; 55 | import { TbFocusAuto } from "react-icons/tb"; 56 | import IconOnlyButton from "../Widgets/Buttons/IconOnlyButton"; 57 | import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "react-icons/ai"; 58 | import { RiZoomInLine, RiZoomOutLine } from "react-icons/ri"; 59 | import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md"; 60 | import { MdOutlineFitScreen } from "react-icons/md"; 61 | 62 | const imageWidth = 3000; 63 | const imageHeight = 3000; 64 | 65 | const nodeTypes: NodeTypes = { 66 | workflow: WorkflowNode, 67 | job: JobNode, 68 | executor: ExecutorNode, 69 | orb: OrbNode, 70 | command: CommandNode, 71 | }; 72 | 73 | // const onNodeDrag: OnNodeDrag = (_, node) => { 74 | // console.log("drag event", node.data); 75 | // }; 76 | 77 | type VisualEditorProps = { 78 | takingScreenshot: boolean; 79 | setTakingScreenshot: React.Dispatch>; 80 | }; 81 | 82 | export default function VisualEditor({ 83 | takingScreenshot, 84 | setTakingScreenshot, 85 | }: VisualEditorProps) { 86 | const workflows = useSelector(getAllWorkflows); 87 | const executors = useSelector(getAllExecutors); 88 | const jobs = useSelector(getAllJobs); 89 | const orbs = useSelector(getAllOrbs); 90 | const commands = useSelector(getAllCommands); 91 | const darkMode = useSelector(getDarkMode); 92 | const visibleJobs = useSelector(getVisibleJobs); 93 | const visibleExecutors = useSelector(getVisibleExecutors); 94 | const visibleCommands = useSelector(getVisibleCommands); 95 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 96 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 97 | const [active, setActive] = useState<{ 98 | type: string | undefined; 99 | entity: Node | any; 100 | } | null>(null); 101 | const [options, setOptions] = useState< 102 | | { 103 | autoFocus: boolean; 104 | fullscreen: boolean; 105 | nodesDraggable: boolean; 106 | } 107 | | any 108 | >({ autoFocus: true, fullscreen: false, nodesDraggable: true }); 109 | const selectedEntity = useSelector(getSelectedEntity); 110 | const activeEntity = useSelector(getActiveEntity); 111 | const dispatch = useDispatch(); 112 | const reactFlow = useReactFlow(); 113 | const canvaRef = useRef(null); 114 | 115 | const data = useSelector(getData); 116 | 117 | const renderExecutors = () => { 118 | let i = -700; 119 | executors.map((executor) => { 120 | if (visibleExecutors?.includes(executor[0])) { 121 | const executorNode = { 122 | id: executor[0], 123 | data: { label: executor[0], executor }, 124 | position: { x: i, y: 1700 }, 125 | type: "executor", 126 | focusable: true, 127 | }; 128 | i += 300; 129 | setNodes((nds) => [...nds, executorNode]); 130 | } 131 | }); 132 | }; 133 | 134 | const renderJobs = () => { 135 | let i = -600; 136 | jobs.map((job) => { 137 | if (visibleJobs.includes(job[0])) { 138 | const jobNode = { 139 | id: job[0], 140 | data: { label: job[0], job }, 141 | position: { x: i, y: 1200 }, 142 | type: "job", 143 | focusable: true, 144 | }; 145 | i += 300; 146 | setNodes((nds) => [...nds, jobNode]); 147 | } 148 | }); 149 | }; 150 | 151 | const renderWorkflows = () => { 152 | let i = 0, 153 | count = 0; 154 | for (const workflow of workflows) { 155 | if (workflow[0] === "version") continue; 156 | const workflowNode = { 157 | id: workflow[0], 158 | data: { label: workflow[0], workflow, jobs: jobs }, 159 | position: { x: i, y: 400 }, 160 | type: "workflow", 161 | focusable: true, 162 | }; 163 | i += 600; 164 | count++; 165 | setNodes((nds) => [...nds, workflowNode]); 166 | } 167 | }; 168 | 169 | const renderOrbs = () => { 170 | const orbNode = { 171 | id: "orb", 172 | data: { orb: orbs }, 173 | position: { x: 0, y: 100 }, 174 | type: "orb", 175 | focusable: true, 176 | }; 177 | setNodes((nds) => [...nds, orbNode]); 178 | }; 179 | 180 | const renderCommands = () => { 181 | let i = 5; 182 | for (const command of commands) { 183 | if (visibleCommands.includes(command[0])) { 184 | const commandNode = { 185 | id: command[0], 186 | data: { label: command[0], command }, 187 | position: { x: -600, y: i * -1 }, 188 | type: "command", 189 | focusable: true, 190 | }; 191 | i += 200; 192 | setNodes((nds) => [...nds, commandNode]); 193 | } 194 | } 195 | }; 196 | 197 | const renderEdges = () => { 198 | if (executors.length && jobs.length) { 199 | jobs.map((job) => { 200 | if (activeEntity?.type === "workflow") { 201 | const workflowJobs: any[] = []; 202 | activeEntity.entity[1].jobs.forEach((wJob: any) => { 203 | if (job[0] === objToArrayConverter(wJob)[0][0]) { 204 | workflowJobs.push(job); 205 | } 206 | }); 207 | workflowJobs.map((job: any) => { 208 | var jobName, jobData; 209 | if (typeof job === "object") { 210 | jobName = job[0]; 211 | jobData = job[1]; 212 | } else { 213 | jobName = job; 214 | jobData = job; 215 | } 216 | setEdges((eds) => [ 217 | ...eds, 218 | { 219 | id: `${jobData.executor}-${jobName}`, 220 | source: jobData.executor, 221 | target: jobName, 222 | animated: true, 223 | style: { stroke: "#ffb41eff", strokeWidth: 1.5 }, 224 | markerEnd: { 225 | type: MarkerType.ArrowClosed, 226 | width: 20, 227 | height: 20, 228 | color: "#ffb41eff", 229 | }, 230 | }, 231 | ]); 232 | }); 233 | } else if (activeEntity?.type === "job") { 234 | const activeJobName = activeEntity?.entity[0]; 235 | const activeJobData = activeEntity?.entity[1]; 236 | setEdges((eds) => [ 237 | ...eds, 238 | { 239 | id: `${activeJobData.executor}-${activeJobName}`, 240 | source: activeJobData.executor, 241 | target: activeJobName, 242 | animated: true, 243 | style: { stroke: "#ffb41eff", strokeWidth: 1.5 }, 244 | markerEnd: { 245 | type: MarkerType.ArrowClosed, 246 | width: 20, 247 | height: 20, 248 | color: "#ffb41eff", 249 | }, 250 | }, 251 | ]); 252 | } else if (activeEntity?.type === "executor") { 253 | const executorName = activeEntity?.entity[0]; 254 | const jobsUsingExecutor: string[] = []; 255 | jobs.forEach((job: any) => { 256 | const jobName: string = job[0]; 257 | const jobData: any = job[1]; 258 | const jobExecutor: any[] = jobData.executor; 259 | if ( 260 | typeof jobExecutor === "string" && 261 | jobExecutor === executorName 262 | ) { 263 | jobsUsingExecutor.push(jobName); 264 | } 265 | }); 266 | jobsUsingExecutor.map((job: string) => { 267 | setEdges((eds) => [ 268 | ...eds, 269 | { 270 | id: `${executorName}-${job}`, 271 | source: executorName, 272 | target: job, 273 | animated: true, 274 | style: { stroke: "#ffb41eff", strokeWidth: 1.5 }, 275 | markerEnd: { 276 | type: MarkerType.ArrowClosed, 277 | width: 20, 278 | height: 20, 279 | color: "#ffb41eff", 280 | }, 281 | }, 282 | ]); 283 | }); 284 | } 285 | }); 286 | } 287 | 288 | if (jobs.length && workflows.length) { 289 | workflows.map((workflow) => { 290 | const workflowName = workflow[0]; 291 | const workflowData = workflow[1]; 292 | const workflowJobsArray = workflow[1].jobs; 293 | workflowJobsArray?.map((wJob: any[]) => { 294 | const wJobArray = 295 | typeof wJob === "string" ? [wJob] : objToArrayConverter(wJob)[0]; 296 | const wJobName = wJobArray?.[0]; 297 | if ( 298 | activeEntity?.type === "workflow" && 299 | activeEntity?.entity[0] === workflowName && 300 | checkIfJobExistsInWorkflow(wJobName, workflowData) 301 | ) { 302 | setEdges((eds) => [ 303 | ...eds, 304 | { 305 | id: `${wJobName}-${workflowName}`, 306 | source: wJobName, 307 | target: workflowName, 308 | animated: true, 309 | style: { stroke: "#0672cbff", strokeWidth: 1.5 }, 310 | markerEnd: { 311 | type: MarkerType.ArrowClosed, 312 | width: 20, 313 | height: 20, 314 | color: "#0672cbff", 315 | }, 316 | }, 317 | ]); 318 | } else if ( 319 | activeEntity?.type === "job" && 320 | activeEntity?.entity[0] === wJobName && 321 | checkIfWorkflowUsesJob(workflowName, workflows, wJobName) 322 | ) { 323 | setEdges((eds) => [ 324 | ...eds, 325 | { 326 | id: `${wJobName}-${workflowName}`, 327 | source: wJobName, 328 | target: workflowName, 329 | animated: true, 330 | style: { stroke: "#0672cbff", strokeWidth: 1.5 }, 331 | markerEnd: { 332 | type: MarkerType.ArrowClosed, 333 | width: 20, 334 | height: 20, 335 | color: "#0672cbff", 336 | }, 337 | }, 338 | ]); 339 | } 340 | }); 341 | }); 342 | } 343 | 344 | if (commands.length && jobs.length) { 345 | commands.map((command) => { 346 | if (activeEntity?.type === "job") { 347 | const commandsUsedInJob: any[] = []; 348 | commands.forEach((command: any) => { 349 | const commandName = command[0]; 350 | if ( 351 | checkIfCommandExistsInJob(commandName, activeEntity?.entity[1]) 352 | ) { 353 | commandsUsedInJob.push(commandName); 354 | } 355 | }); 356 | commandsUsedInJob.map((command: any) => { 357 | const commandName = command; 358 | const jobName = activeEntity?.entity[0]; 359 | setEdges((eds) => [ 360 | ...eds, 361 | { 362 | id: `${commandName}-${jobName}`, 363 | source: commandName, 364 | target: jobName, 365 | animated: true, 366 | targetHandle: "command", 367 | style: { stroke: "#ff401eff", strokeWidth: 1.5 }, 368 | markerEnd: { 369 | type: MarkerType.ArrowClosed, 370 | width: 20, 371 | height: 20, 372 | color: "#ff401eff", 373 | }, 374 | }, 375 | ]); 376 | }); 377 | } else if (activeEntity?.type === "command") { 378 | const commandName = activeEntity?.entity[0]; 379 | const jobsUsingCommand: any[] = []; 380 | jobs.forEach((job: any) => { 381 | const jobName: string = job[0]; 382 | const jobData: any = job[1]; 383 | const jobSteps: any[] = jobData.steps; 384 | jobSteps.forEach((jobStep: any) => { 385 | if (typeof jobStep === "string" && jobStep === commandName) { 386 | jobsUsingCommand.push(jobName); 387 | } 388 | }); 389 | }); 390 | jobsUsingCommand.map((job: any) => { 391 | const jobName = job; 392 | setEdges((eds) => [ 393 | ...eds, 394 | { 395 | id: `${commandName}-${jobName}`, 396 | source: commandName, 397 | target: jobName, 398 | animated: true, 399 | targetHandle: "command", 400 | style: { stroke: "#ff401eff", strokeWidth: 1.5 }, 401 | markerEnd: { 402 | type: MarkerType.ArrowClosed, 403 | width: 20, 404 | height: 20, 405 | color: "#ff401eff", 406 | }, 407 | }, 408 | ]); 409 | }); 410 | } 411 | }); 412 | } 413 | }; 414 | 415 | useEffect(() => { 416 | setNodes([]); 417 | setEdges([]); 418 | executors.length && renderExecutors(); 419 | jobs.length && renderJobs(); 420 | commands.length && renderCommands(); 421 | workflows.length && renderWorkflows(); 422 | orbs.length && renderOrbs(); 423 | setTimeout(() => renderEdges(), 1000); 424 | }, [data, executors, jobs, orbs, commands, activeEntity]); 425 | 426 | const onConnect: OnConnect = useCallback( 427 | (connection) => { 428 | setEdges((eds) => addEdge(connection, eds)); 429 | }, 430 | [setEdges] 431 | ); 432 | 433 | function downloadImage(dataUrl: any) { 434 | setTakingScreenshot(false); 435 | const a = document.createElement("a"); 436 | 437 | a.setAttribute("download", "reactflow.png"); 438 | a.setAttribute("href", dataUrl); 439 | a.click(); 440 | } 441 | 442 | useEffect(() => { 443 | if (activeEntity.type === "workflow") { 444 | const workflowJobs = activeEntity.entity[1].jobs.map((job) => { 445 | const jobArray = 446 | typeof job === "string" ? [job] : objToArrayConverter(job)[0]; 447 | return jobArray[0]; 448 | }); 449 | dispatch(setJobsVisible(workflowJobs)); 450 | } else if (activeEntity.type === "job") { 451 | const executor = activeEntity.entity[1].executor; 452 | dispatch(setExecutorsVisible(executor)); 453 | } 454 | setTimeout(() => { 455 | options?.autoFocus && reactFlow.fitView({ duration: 400 }); 456 | }, 500); 457 | }, [activeEntity]); 458 | 459 | useEffect(() => { 460 | if (takingScreenshot) { 461 | const nodesBounds = getNodesBounds(nodes); 462 | const viewport = getViewportForBounds( 463 | nodesBounds, 464 | imageWidth, 465 | imageHeight, 466 | 1, 467 | 10, 468 | 0 469 | ); 470 | const canva: HTMLElement = document.getElementById("canva")!; 471 | toPng(canva, { 472 | backgroundColor: "#fff", 473 | width: imageWidth, 474 | height: imageHeight, 475 | style: { 476 | width: imageWidth.toString(), 477 | height: imageHeight.toString(), 478 | transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, 479 | }, 480 | }).then(downloadImage); 481 | } 482 | }, [takingScreenshot]); 483 | 484 | return ( 485 |
491 | { 501 | dispatch( 502 | setSelectedEntity({ 503 | type: node.type, 504 | entity: node.data[node.type || 0], 505 | }) 506 | ); 507 | }} 508 | onNodeClick={(event, node) => { 509 | setActive({ 510 | type: node.type, 511 | entity: node.data[node.type || 0], 512 | }); 513 | }} 514 | onPaneClick={() => { 515 | selectedEntity.type && 516 | dispatch(setSelectedEntity({ type: null, entity: null })); 517 | activeEntity.type && 518 | dispatch(setActiveEntity({ type: null, entity: null })); 519 | visibleJobs.length && dispatch(setJobsVisible([])); 520 | visibleExecutors.length && dispatch(setExecutorsVisible([])); 521 | visibleExecutors.length && dispatch(setCommandsVisible([])); 522 | }} 523 | defaultMarkerColor="red" 524 | nodeTypes={nodeTypes} 525 | edges={edges} 526 | onNodesChange={onNodesChange} 527 | onEdgesChange={onEdgesChange} 528 | onConnect={onConnect} 529 | nodesDraggable={options.nodesDraggable} 530 | // onNodeDrag={onNodeDrag} 531 | fitView 532 | fitViewOptions={{ 533 | padding: 0.2, 534 | }} 535 | > 536 | 537 | {!takingScreenshot && } 538 | {!takingScreenshot && ( 539 |
540 | { 542 | reactFlow.zoomIn(); 543 | }} 544 | icon={} 545 | className={`p-2 rounded hover:bg-gray-200`} 546 | /> 547 | { 549 | reactFlow.zoomOut(); 550 | }} 551 | icon={} 552 | className={`p-2 rounded hover:bg-gray-200`} 553 | /> 554 | { 556 | reactFlow.fitView(); 557 | }} 558 | icon={} 559 | className={`p-2 rounded hover:bg-gray-200`} 560 | /> 561 | { 563 | setOptions({ 564 | ...options, 565 | nodesDraggable: !options?.nodesDraggable, 566 | }); 567 | }} 568 | icon={ 569 | options.nodesDraggable ? ( 570 | 571 | ) : ( 572 | 573 | ) 574 | } 575 | className={`p-2 rounded hover:bg-gray-200 ${ 576 | !options?.nodesDraggable ? "bg-blue-500" : "" 577 | }`} 578 | /> 579 | { 581 | setOptions({ ...options, autoFocus: !options?.autoFocus }); 582 | }} 583 | icon={} 584 | className={`p-2 rounded hover:bg-gray-200 ${ 585 | options?.autoFocus ? "bg-blue-500" : "" 586 | }`} 587 | /> 588 | { 590 | if (!document.fullscreenElement) { 591 | const canvaElement: any = canvaRef?.current; 592 | canvaElement.requestFullscreen(); 593 | } else { 594 | document.exitFullscreen(); 595 | } 596 | setOptions({ ...options, fullscreen: !options?.fullscreen }); 597 | }} 598 | icon={options.fullscreen?:} 599 | className={`p-2 rounded hover:bg-gray-200 ${ 600 | options?.fullscreen ? "bg-blue-500" : "" 601 | }`} 602 | /> 603 |
604 | )} 605 |
606 |
607 | ); 608 | } 609 | -------------------------------------------------------------------------------- /src/components/Widgets/Buttons/IconOnlyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type IconOnlyButtonProps = { 4 | disabled?: boolean; 5 | className?: string; 6 | onClick?: (e?: React.MouseEvent) => void; 7 | icon: React.ReactNode; 8 | }; 9 | 10 | export default function IconOnlyButton({ 11 | disabled = false, 12 | className, 13 | onClick, 14 | icon, 15 | }: IconOnlyButtonProps) { 16 | return ( 17 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Widgets/Buttons/PrimaryButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type PrimaryButtonProps = { 4 | label: string; 5 | disabled?: boolean; 6 | color?: string; 7 | onClick?: (e?: any) => void; 8 | icon?: React.ReactNode; 9 | className?: string; 10 | }; 11 | 12 | export default function PrimaryButton({ 13 | label, 14 | disabled = false, 15 | color = "bg-blue-500", 16 | onClick, 17 | icon, 18 | className 19 | }: PrimaryButtonProps) { 20 | return ( 21 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Widgets/Buttons/SecondaryButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type SecondaryButtonProps = { 4 | label: string; 5 | disabled?: boolean; 6 | color?: string; 7 | onClick?: (e?: any) => void; 8 | icon?: React.ReactNode; 9 | className?: string; 10 | }; 11 | 12 | export default function SecondaryButton({ 13 | label, 14 | disabled = false, 15 | color = "white", 16 | onClick, 17 | icon, 18 | className, 19 | }: SecondaryButtonProps) { 20 | return ( 21 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Widgets/ConfirmDialog/ConfirmDialog.tsx: -------------------------------------------------------------------------------- 1 | import IconOnlyButton from "../Buttons/IconOnlyButton"; 2 | import { FaPlus } from "react-icons/fa6"; 3 | import PrimaryButton from "../Buttons/PrimaryButton"; 4 | import SecondaryButton from "../Buttons/SecondaryButton"; 5 | 6 | type ConfirmDialogProps = { 7 | msg?: string; 8 | onConfirm?: () => void; 9 | onCancel?: () => void; 10 | }; 11 | 12 | export default function ConfirmDialog({ 13 | msg = "Are you sure you want to quit?", 14 | onCancel, 15 | onConfirm, 16 | }: ConfirmDialogProps) { 17 | return ( 18 |
19 |
20 | {}} 27 | /> 28 | } 29 | /> 30 |
31 |

32 | Confirm 33 |

34 |

{msg}

35 |
36 |
37 | 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Widgets/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | type DividerProps = { 2 | className?: string; 3 | }; 4 | export default function Divider({ className }: DividerProps) { 5 | return ( 6 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Widgets/InputBox/InputBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type InputBoxProps = { 4 | type: string; 5 | label?: string; 6 | className?: string; 7 | disabled?: boolean; 8 | placeholder?: string; 9 | value: string | undefined | null; 10 | onChange?: (e: React.ChangeEvent) => void; 11 | onKeyDown?: (e: React.KeyboardEvent | undefined) => void; 12 | style?: any; 13 | }; 14 | 15 | export default function InputBox({ 16 | type = "text", 17 | label, 18 | className, 19 | disabled = true, 20 | placeholder, 21 | value, 22 | onChange, 23 | onKeyDown, 24 | style, 25 | }: InputBoxProps) { 26 | return ( 27 |
28 | {label && ( 29 | 30 | )} 31 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Widgets/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Rings } from "react-loader-spinner"; 2 | 3 | type LoadingProps = { 4 | text?: string; 5 | }; 6 | 7 | export default function Loading({ text = "Loading..." }: LoadingProps) { 8 | return ( 9 |
10 | 19 |

{text}

20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Widgets/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | 3 | type MenuItem = { 4 | onClick?: () => void; 5 | label: string; 6 | icon?: React.ReactNode; 7 | }; 8 | 9 | type MenuProps = { 10 | x: number; 11 | y: number; 12 | menuItems: MenuItem[] | null; 13 | setMenuItems: (items: MenuItem[] | null) => void; 14 | }; 15 | 16 | export default function Menu({ x, y, menuItems, setMenuItems }: MenuProps) { 17 | const menuRef = useRef(null); 18 | 19 | return ( 20 |
25 | {menuItems?.map((item: MenuItem) => { 26 | return ( 27 | 36 | ); 37 | })} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Widgets/SelectBox/SelectBox.tsx: -------------------------------------------------------------------------------- 1 | import Select from "react-select"; 2 | import Creatable from "react-select/creatable"; 3 | 4 | type SelectBoxProps = { 5 | options: any; 6 | value?: any; 7 | defaultVal?: any; 8 | onChange?: any; 9 | disabled?: boolean; 10 | className?: string; 11 | customInput?: boolean; 12 | }; 13 | 14 | export default function SelectBox({ 15 | options, 16 | defaultVal, 17 | value, 18 | onChange, 19 | disabled = true, 20 | className, 21 | customInput = false, 22 | }: SelectBoxProps) { 23 | return ( 24 |
25 | {customInput ? ( 26 | `dark:text-white text-[13px] cursor-pointer`, 35 | singleValue: (state) => 36 | `dark:text-white text-[13px] cursor-pointer`, 37 | control: (state) => 38 | `dark:bg-gray-900/70 dark:text-white border-[1px] text-[13px] ${ 39 | state.isFocused 40 | ? "border-blue-500" 41 | : "border-gray-400 dark:border-gray-700" 42 | } cursor-pointer`, 43 | option: (state) => 44 | `${ 45 | state.isSelected ? "dark:bg-blue-500" : " dark:bg-gray-900/70" 46 | } scroll text-[13px] cursor-pointer`, 47 | menu: (state) => `dark:bg-gray-900/70 text-[13px] scroll h-[100px]`, 48 | menuList: (state) => 49 | `dark:text-white dark:bg-gray-900/70 text-[13px] scroll h-[100px] overflow-y-scroll`, 50 | }} 51 | /> 52 | ) : ( 53 | 33 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/data/configReference.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": { 3 | "required": false, 4 | "type": "string", 5 | "description": "The version field is intended to be used in order to issue warnings for deprecation or breaking changes.", 6 | "availableForVisualization": false 7 | }, 8 | "orbs": { 9 | "required": false, 10 | "type": "map", 11 | "description": "The orbs key is a map describing orbs and their versions. Keys are orb names, values are orb versions.", 12 | "availableForVisualization": true 13 | }, 14 | "commands": { 15 | "required": false, 16 | "type": "map", 17 | "description": "The commands key allows you to define a reusable set of steps as a single key.", 18 | "subKeys": { 19 | "steps": { 20 | "required": true, 21 | "type": "sequence", 22 | "description": "Sequence of steps." 23 | }, 24 | "parameters": { 25 | "required": false, 26 | "type": "map", 27 | "description": "Map of parameter keys to values." 28 | } 29 | }, 30 | "availableForVisualization": true 31 | }, 32 | "jobs": { 33 | "required": true, 34 | "type": "map", 35 | "description": "The jobs key is where you define the build, test, and deployment jobs that comprise your config.", 36 | "subKeys": { 37 | "docker": { 38 | "required": false, 39 | "type": "list", 40 | "description": "List of maps, each defining an image.", 41 | "subKeys": { 42 | "image": { 43 | "required": true, 44 | "type": "string", 45 | "description": "The Docker image to use." 46 | }, 47 | "name": { 48 | "required": false, 49 | "type": "string", 50 | "description": "The name to use for the image." 51 | }, 52 | "entrypoint": { 53 | "required": false, 54 | "type": ["string", "list"], 55 | "description": "The entrypoint for the image." 56 | }, 57 | "user": { 58 | "required": false, 59 | "type": "string", 60 | "description": "The user to use for the image." 61 | }, 62 | "auth": { 63 | "required": false, 64 | "type": "map", 65 | "description": "Authentication credentials for the image.", 66 | "subKeys": { 67 | "username": { 68 | "required": true, 69 | "type": "string", 70 | "description": "The username for authentication." 71 | }, 72 | "password": { 73 | "required": true, 74 | "type": "string", 75 | "description": "The password for authentication." 76 | } 77 | } 78 | }, 79 | "aws_auth": { 80 | "required": false, 81 | "type": "map", 82 | "description": "AWS authentication credentials for the image.", 83 | "subKeys": { 84 | "aws_access_key_id": { 85 | "required": true, 86 | "type": "string", 87 | "description": "The AWS access key ID." 88 | }, 89 | "aws_secret_access_key": { 90 | "required": true, 91 | "type": "string", 92 | "description": "The AWS secret access key." 93 | } 94 | } 95 | }, 96 | "environment": { 97 | "required": false, 98 | "type": "map", 99 | "description": "Environment variables for the image." 100 | } 101 | } 102 | }, 103 | "machine": { 104 | "required": false, 105 | "type": "map", 106 | "description": "Configuration for a virtual machine image.", 107 | "subKeys": { 108 | "image": { 109 | "required": true, 110 | "type": "string", 111 | "description": "The virtual machine image to use." 112 | }, 113 | "docker_layer_caching": { 114 | "required": false, 115 | "type": "boolean", 116 | "description": "Whether to enable Docker layer caching." 117 | } 118 | } 119 | }, 120 | "steps": { 121 | "required": true, 122 | "type": "sequence", 123 | "description": "Sequence of steps to run in the job." 124 | }, 125 | "parallelism": { 126 | "required": false, 127 | "type": "integer", 128 | "description": "The number of instances of the job to run in parallel." 129 | }, 130 | "resource_class": { 131 | "required": false, 132 | "type": "string", 133 | "description": "The resource class to use for the job." 134 | }, 135 | "working_directory": { 136 | "required": false, 137 | "type": "string", 138 | "description": "The working directory for the job." 139 | }, 140 | "shell": { 141 | "required": false, 142 | "type": "string", 143 | "description": "The shell to use for all commands in the job." 144 | }, 145 | "environment": { 146 | "required": false, 147 | "type": "map", 148 | "description": "Environment variables for the job." 149 | }, 150 | "enable_services": { 151 | "required": false, 152 | "type": "list", 153 | "description": "List of services to enable for the job." 154 | }, 155 | "context": { 156 | "required": false, 157 | "type": "list", 158 | "description": "List of contexts to share environment variables across jobs." 159 | }, 160 | "checkout": { 161 | "required": false, 162 | "type": "map", 163 | "description": "Configuration for checking out source code.", 164 | "subKeys": { 165 | "path": { 166 | "required": false, 167 | "type": "string", 168 | "description": "The path to check out the code." 169 | } 170 | } 171 | }, 172 | "persist_to_workspace": { 173 | "required": false, 174 | "type": "map", 175 | "description": "Configuration for persisting data for downstream jobs.", 176 | "subKeys": { 177 | "root": { 178 | "required": true, 179 | "type": "string", 180 | "description": "The root directory to persist." 181 | }, 182 | "paths": { 183 | "required": true, 184 | "type": "list", 185 | "description": "List of paths to persist." 186 | } 187 | } 188 | }, 189 | "attach_workspace": { 190 | "required": false, 191 | "type": "map", 192 | "description": "Configuration for attaching a persisted workspace.", 193 | "subKeys": { 194 | "at": { 195 | "required": true, 196 | "type": "string", 197 | "description": "The path to attach the workspace." 198 | } 199 | } 200 | }, 201 | "post": { 202 | "required": false, 203 | "type": "sequence", 204 | "description": "Sequence of steps to run after the job." 205 | } 206 | }, 207 | "availableForVisualization": true 208 | }, 209 | "workflows": { 210 | "required": true, 211 | "type": "map", 212 | "description": "With workflows, you can schedule jobs and hold jobs for manual approval.", 213 | "subKeys": { 214 | "jobs": { 215 | "required": false, 216 | "type": "map", 217 | "description": "Map of job names to filters.", 218 | "subKeys": { 219 | "filters": { 220 | "required": false, 221 | "type": "map", 222 | "description": "Filters for when the job should run.", 223 | "subKeys": { 224 | "branches": { 225 | "required": false, 226 | "type": "map", 227 | "description": "Branch filters.", 228 | "subKeys": { 229 | "only": { 230 | "required": false, 231 | "type": "list", 232 | "description": "List of branches to include." 233 | }, 234 | "ignore": { 235 | "required": false, 236 | "type": "list", 237 | "description": "List of branches to ignore." 238 | } 239 | } 240 | }, 241 | "tags": { 242 | "required": false, 243 | "type": "map", 244 | "description": "Tag filters.", 245 | "subKeys": { 246 | "only": { 247 | "required": false, 248 | "type": "list", 249 | "description": "List of tags to include." 250 | }, 251 | "ignore": { 252 | "required": false, 253 | "type": "list", 254 | "description": "List of tags to ignore." 255 | } 256 | } 257 | } 258 | } 259 | } 260 | } 261 | }, 262 | "requires": { 263 | "required": false, 264 | "type": "list", 265 | "description": "List of job names that must complete before this workflow can run." 266 | }, 267 | "context": { 268 | "required": false, 269 | "type": "list", 270 | "description": "List of contexts to share environment variables across jobs." 271 | }, 272 | "filters": { 273 | "required": false, 274 | "type": "map", 275 | "description": "Filters for when the workflow should run.", 276 | "subKeys": { 277 | "branches": { 278 | "required": false, 279 | "type": "map", 280 | "description": "Branch filters.", 281 | "subKeys": { 282 | "only": { 283 | "required": false, 284 | "type": "list", 285 | "description": "List of branches to include." 286 | }, 287 | "ignore": { 288 | "required": false, 289 | "type": "list", 290 | "description": "List of branches to ignore." 291 | } 292 | } 293 | }, 294 | "tags": { 295 | "required": false, 296 | "type": "map", 297 | "description": "Tag filters.", 298 | "subKeys": { 299 | "only": { 300 | "required": false, 301 | "type": "list", 302 | "description": "List of tags to include." 303 | }, 304 | "ignore": { 305 | "required": false, 306 | "type": "list", 307 | "description": "List of tags to ignore." 308 | } 309 | } 310 | } 311 | } 312 | } 313 | }, 314 | "availableForVisualization": true 315 | }, 316 | "executors": { 317 | "required": false, 318 | "type": "map", 319 | "description": "Executors define the environment in which the steps of a job will be run.", 320 | "subKeys": { 321 | "docker": { 322 | "required": false, 323 | "type": "list", 324 | "description": "List of maps, each defining an image.", 325 | "subKeys": { 326 | "image": { 327 | "required": true, 328 | "type": "string", 329 | "description": "The Docker image to use." 330 | }, 331 | "name": { 332 | "required": false, 333 | "type": "string", 334 | "description": "The name to use for the image." 335 | }, 336 | "entrypoint": { 337 | "required": false, 338 | "type": ["string", "list"], 339 | "description": "The entrypoint for the image." 340 | }, 341 | "user": { 342 | "required": false, 343 | "type": "string", 344 | "description": "The user to use for the image." 345 | }, 346 | "auth": { 347 | "required": false, 348 | "type": "map", 349 | "description": "Authentication credentials for the image.", 350 | "subKeys": { 351 | "username": { 352 | "required": true, 353 | "type": "string", 354 | "description": "The username for authentication." 355 | }, 356 | "password": { 357 | "required": true, 358 | "type": "string", 359 | "description": "The password for authentication." 360 | } 361 | } 362 | }, 363 | "aws_auth": { 364 | "required": false, 365 | "type": "map", 366 | "description": "AWS authentication credentials for the image.", 367 | "subKeys": { 368 | "aws_access_key_id": { 369 | "required": true, 370 | "type": "string", 371 | "description": "The AWS access key ID." 372 | }, 373 | "aws_secret_access_key": { 374 | "required": true, 375 | "type": "string", 376 | "description": "The AWS secret access key." 377 | } 378 | } 379 | }, 380 | "environment": { 381 | "required": false, 382 | "type": "map", 383 | "description": "Environment variables for the image." 384 | } 385 | } 386 | }, 387 | "machine": { 388 | "required": false, 389 | "type": "map", 390 | "description": "Configuration for a virtual machine image.", 391 | "subKeys": { 392 | "image": { 393 | "required": true, 394 | "type": "string", 395 | "description": "The virtual machine image to use." 396 | }, 397 | "docker_layer_caching": { 398 | "required": false, 399 | "type": "boolean", 400 | "description": "Whether to enable Docker layer caching." 401 | } 402 | } 403 | }, 404 | "resource_class": { 405 | "required": false, 406 | "type": "string", 407 | "description": "The resource class to use for the executor." 408 | }, 409 | "working_directory": { 410 | "required": false, 411 | "type": "string", 412 | "description": "The working directory for the executor." 413 | } 414 | }, 415 | "availableForVisualization": true 416 | }, 417 | "setup_remote_docker": { 418 | "required": false, 419 | "type": "map", 420 | "description": "The setup_remote_docker key allows you to opt-in to using the Docker Engine for remote Docker execution.", 421 | "subKeys": { 422 | "version": { 423 | "required": true, 424 | "type": "string", 425 | "description": "The version of Docker to use." 426 | }, 427 | "docker_layer_caching": { 428 | "required": false, 429 | "type": "boolean", 430 | "description": "Whether to enable Docker layer caching." 431 | } 432 | }, 433 | "availableForVisualization": false 434 | }, 435 | "machine": { 436 | "required": false, 437 | "type": "map", 438 | "description": "The machine key allows you to configure virtual machine image for your job.", 439 | "subKeys": { 440 | "image": { 441 | "required": true, 442 | "type": "string", 443 | "description": "The virtual machine image to use." 444 | }, 445 | "docker_layer_caching": { 446 | "required": false, 447 | "type": "boolean", 448 | "description": "Whether to enable Docker layer caching." 449 | } 450 | }, 451 | "availableForVisualization": false 452 | }, 453 | "docker": { 454 | "required": false, 455 | "type": "list", 456 | "description": "The docker key allows you to configure a Docker image for the job's execution environment.", 457 | "subKeys": { 458 | "image": { 459 | "required": true, 460 | "type": "string", 461 | "description": "The Docker image to use." 462 | }, 463 | "name": { 464 | "required": false, 465 | "type": "string", 466 | "description": "The name to use for the image." 467 | }, 468 | "entrypoint": { 469 | "required": false, 470 | "type": ["string", "list"], 471 | "description": "The entrypoint for the image." 472 | }, 473 | "user": { 474 | "required": false, 475 | "type": "string", 476 | "description": "The user to use for the image." 477 | }, 478 | "auth": { 479 | "required": false, 480 | "type": "map", 481 | "description": "Authentication credentials for the image.", 482 | "subKeys": { 483 | "username": { 484 | "required": true, 485 | "type": "string", 486 | "description": "The username for authentication." 487 | }, 488 | "password": { 489 | "required": true, 490 | "type": "string", 491 | "description": "The password for authentication." 492 | } 493 | } 494 | }, 495 | "aws_auth": { 496 | "required": false, 497 | "type": "map", 498 | "description": "AWS authentication credentials for the image.", 499 | "subKeys": { 500 | "aws_access_key_id": { 501 | "required": true, 502 | "type": "string", 503 | "description": "The AWS access key ID." 504 | }, 505 | "aws_secret_access_key": { 506 | "required": true, 507 | "type": "string", 508 | "description": "The AWS secret access key." 509 | } 510 | } 511 | }, 512 | "environment": { 513 | "required": false, 514 | "type": "map", 515 | "description": "Environment variables for the image." 516 | } 517 | }, 518 | "availableForVisualization": true 519 | }, 520 | "resource_class": { 521 | "required": false, 522 | "type": "string", 523 | "description": "The resource_class key allows you to configure CPU and RAM resources for the job.", 524 | "availableForVisualization": false 525 | }, 526 | "parallelism": { 527 | "required": false, 528 | "type": "integer", 529 | "description": "The parallelism key allows you to configure how many instances of a job should run in parallel.", 530 | "availableForVisualization": false 531 | }, 532 | "shell": { 533 | "required": false, 534 | "type": "string", 535 | "description": "The shell key allows you to configure the shell to use for all commands in a job.", 536 | "availableForVisualization": false 537 | }, 538 | "steps": { 539 | "required": true, 540 | "type": "sequence", 541 | "description": "The steps key defines a collection of single actions to run during a job.", 542 | "subKeys": { 543 | "run": { 544 | "required": false, 545 | "type": "map", 546 | "description": "The run key allows you to run arbitrary commands.", 547 | "subKeys": { 548 | "name": { 549 | "required": false, 550 | "type": "string", 551 | "description": "The name of the step." 552 | }, 553 | "command": { 554 | "required": true, 555 | "type": "string", 556 | "description": "The command to run." 557 | }, 558 | "shell": { 559 | "required": false, 560 | "type": "string", 561 | "description": "The shell to use for the command." 562 | }, 563 | "environment": { 564 | "required": false, 565 | "type": "map", 566 | "description": "Environment variables for the command." 567 | }, 568 | "background": { 569 | "required": false, 570 | "type": "boolean", 571 | "description": "Whether to run the command in the background." 572 | }, 573 | "working_directory": { 574 | "required": false, 575 | "type": "string", 576 | "description": "The working directory for the command." 577 | }, 578 | "no_output_timeout": { 579 | "required": false, 580 | "type": "string", 581 | "description": "The time after which the command is terminated if there is no output." 582 | } 583 | } 584 | }, 585 | "checkout": { 586 | "required": false, 587 | "type": "map", 588 | "description": "The checkout key allows you to check out source code.", 589 | "subKeys": { 590 | "path": { 591 | "required": false, 592 | "type": "string", 593 | "description": "The path to check out the code." 594 | } 595 | } 596 | }, 597 | "persist_to_workspace": { 598 | "required": false, 599 | "type": "map", 600 | "description": "The persist_to_workspace key allows you to persist data for use in downstream jobs.", 601 | "subKeys": { 602 | "root": { 603 | "required": true, 604 | "type": "string", 605 | "description": "The root directory to persist." 606 | }, 607 | "paths": { 608 | "required": true, 609 | "type": "list", 610 | "description": "List of paths to persist." 611 | } 612 | } 613 | }, 614 | "attach_workspace": { 615 | "required": false, 616 | "type": "map", 617 | "description": "The attach_workspace key allows you to attach a persisted workspace.", 618 | "subKeys": { 619 | "at": { 620 | "required": true, 621 | "type": "string", 622 | "description": "The path to attach the workspace." 623 | } 624 | } 625 | }, 626 | "store_artifacts": { 627 | "required": false, 628 | "type": "map", 629 | "description": "The store_artifacts key allows you to upload artifacts for later viewing in the CircleCI web application.", 630 | "subKeys": { 631 | "path": { 632 | "required": true, 633 | "type": "string", 634 | "description": "The path to the artifacts to store." 635 | }, 636 | "destination": { 637 | "required": false, 638 | "type": "string", 639 | "description": "The destination directory for the artifacts." 640 | } 641 | } 642 | }, 643 | "store_test_results": { 644 | "required": false, 645 | "type": "map", 646 | "description": "The store_test_results key allows you to upload test results for display in the Test Summary section.", 647 | "subKeys": { 648 | "path": { 649 | "required": true, 650 | "type": "string", 651 | "description": "The path to the test results to store." 652 | } 653 | } 654 | }, 655 | "add_ssh_keys": { 656 | "required": false, 657 | "type": "map", 658 | "description": "The add_ssh_keys key allows you to add SSH keys from a project's settings to a container.", 659 | "subKeys": { 660 | "fingerprints": { 661 | "required": true, 662 | "type": "list", 663 | "description": "List of fingerprints of the SSH keys to add." 664 | } 665 | } 666 | }, 667 | "when": { 668 | "required": false, 669 | "type": "map", 670 | "description": "The when key allows you to control when a step is run based on criteria.", 671 | "subKeys": { 672 | "condition": { 673 | "required": true, 674 | "type": "string", 675 | "description": "The condition to evaluate." 676 | }, 677 | "filters": { 678 | "required": false, 679 | "type": "map", 680 | "description": "Filters for when the step should run.", 681 | "subKeys": { 682 | "branches": { 683 | "required": false, 684 | "type": "map", 685 | "description": "Branch filters.", 686 | "subKeys": { 687 | "only": { 688 | "required": false, 689 | "type": "list", 690 | "description": "List of branches to include." 691 | }, 692 | "ignore": { 693 | "required": false, 694 | "type": "list", 695 | "description": "List of branches to ignore." 696 | } 697 | } 698 | }, 699 | "tags": { 700 | "required": false, 701 | "type": "map", 702 | "description": "Tag filters.", 703 | "subKeys": { 704 | "only": { 705 | "required": false, 706 | "type": "list", 707 | "description": "List of tags to include." 708 | }, 709 | "ignore": { 710 | "required": false, 711 | "type": "list", 712 | "description": "List of tags to ignore." 713 | } 714 | } 715 | } 716 | } 717 | } 718 | } 719 | }, 720 | "unless": { 721 | "required": false, 722 | "type": "map", 723 | "description": "The unless key allows you to control when a step is skipped based on criteria.", 724 | "subKeys": { 725 | "condition": { 726 | "required": true, 727 | "type": "string", 728 | "description": "The condition to evaluate." 729 | }, 730 | "filters": { 731 | "required": false, 732 | "type": "map", 733 | "description": "Filters for when the step should be skipped.", 734 | "subKeys": { 735 | "branches": { 736 | "required": false, 737 | "type": "map", 738 | "description": "Branch filters.", 739 | "subKeys": { 740 | "only": { 741 | "required": false, 742 | "type": "list", 743 | "description": "List of branches to include." 744 | }, 745 | "ignore": { 746 | "required": false, 747 | "type": "list", 748 | "description": "List of branches to ignore." 749 | } 750 | } 751 | }, 752 | "tags": { 753 | "required": false, 754 | "type": "map", 755 | "description": "Tag filters.", 756 | "subKeys": { 757 | "only": { 758 | "required": false, 759 | "type": "list", 760 | "description": "List of tags to include." 761 | }, 762 | "ignore": { 763 | "required": false, 764 | "type": "list", 765 | "description": "List of tags to ignore." 766 | } 767 | } 768 | } 769 | } 770 | } 771 | } 772 | } 773 | }, 774 | "availableForVisualization": true 775 | }, 776 | "filters": { 777 | "required": false, 778 | "type": "map", 779 | "description": "The filters key allows you to control when a job is run based on criteria.", 780 | "subKeys": { 781 | "branches": { 782 | "required": false, 783 | "type": "map", 784 | "description": "Branch filters.", 785 | "subKeys": { 786 | "only": { 787 | "required": false, 788 | "type": "list", 789 | "description": "List of branches to include." 790 | }, 791 | "ignore": { 792 | "required": false, 793 | "type": "list", 794 | "description": "List of branches to ignore." 795 | } 796 | } 797 | }, 798 | "tags": { 799 | "required": false, 800 | "type": "map", 801 | "description": "Tag filters.", 802 | "subKeys": { 803 | "only": { 804 | "required": false, 805 | "type": "list", 806 | "description": "List of tags to include." 807 | }, 808 | "ignore": { 809 | "required": false, 810 | "type": "list", 811 | "description": "List of tags to ignore." 812 | } 813 | } 814 | } 815 | }, 816 | "availableForVisualization": true 817 | }, 818 | "requires": { 819 | "required": false, 820 | "type": "list", 821 | "description": "The requires key allows you to define job dependencies.", 822 | "availableForVisualization": false 823 | }, 824 | "context": { 825 | "required": false, 826 | "type": "list", 827 | "description": "The context key allows you to share environment variables across jobs.", 828 | "availableForVisualization": true 829 | }, 830 | "checkout": { 831 | "required": false, 832 | "type": "map", 833 | "description": "The checkout key allows you to check out source code.", 834 | "subKeys": { 835 | "path": { 836 | "required": false, 837 | "type": "string", 838 | "description": "The path to check out the code." 839 | } 840 | }, 841 | "availableForVisualization": false 842 | }, 843 | "persist_to_workspace": { 844 | "required": false, 845 | "type": "map", 846 | "description": "The persist_to_workspace key allows you to persist data for use in downstream jobs.", 847 | "subKeys": { 848 | "root": { 849 | "required": true, 850 | "type": "string", 851 | "description": "The root directory to persist." 852 | }, 853 | "paths": { 854 | "required": true, 855 | "type": "list", 856 | "description": "List of paths to persist." 857 | } 858 | }, 859 | "availableForVisualization": false 860 | }, 861 | "attach_workspace": { 862 | "required": false, 863 | "type": "map", 864 | "description": "The attach_workspace key allows you to attach a persisted workspace.", 865 | "subKeys": { 866 | "at": { 867 | "required": true, 868 | "type": "string", 869 | "description": "The path to attach the workspace." 870 | } 871 | }, 872 | "availableForVisualization": false 873 | }, 874 | "store_artifacts": { 875 | "required": false, 876 | "type": "map", 877 | "description": "The store_artifacts key allows you to upload artifacts for later viewing in the CircleCI web application.", 878 | "subKeys": { 879 | "path": { 880 | "required": true, 881 | "type": "string", 882 | "description": "The path to the artifacts to store." 883 | }, 884 | "destination": { 885 | "required": false, 886 | "type": "string", 887 | "description": "The destination directory for the artifacts." 888 | } 889 | }, 890 | "availableForVisualization": false 891 | }, 892 | "store_test_results": { 893 | "required": false, 894 | "type": "map", 895 | "description": "The store_test_results key allows you to upload test results for display in the Test Summary section.", 896 | "subKeys": { 897 | "path": { 898 | "required": true, 899 | "type": "string", 900 | "description": "The path to the test results to store." 901 | } 902 | }, 903 | "availableForVisualization": false 904 | }, 905 | "add_ssh_keys": { 906 | "required": false, 907 | "type": "map", 908 | "description": "The add_ssh_keys key allows you to add SSH keys from a project's settings to a container.", 909 | "subKeys": { 910 | "fingerprints": { 911 | "required": true, 912 | "type": "list", 913 | "description": "List of fingerprints of the SSH keys to add." 914 | } 915 | }, 916 | "availableForVisualization": false 917 | }, 918 | "run": { 919 | "required": false, 920 | "type": "map", 921 | "description": "The run key allows you to run arbitrary commands.", 922 | "subKeys": { 923 | "name": { 924 | "required": false, 925 | "type": "string", 926 | "description": "The name of the step." 927 | }, 928 | "command": { 929 | "required": true, 930 | "type": "string", 931 | "description": "The command to run." 932 | }, 933 | "shell": { 934 | "required": false, 935 | "type": "string", 936 | "description": "The shell to use for the command." 937 | }, 938 | "environment": { 939 | "required": false, 940 | "type": "map", 941 | "description": "Environment variables for the command." 942 | }, 943 | "background": { 944 | "required": false, 945 | "type": "boolean", 946 | "description": "Whether to run the command in the background." 947 | }, 948 | "working_directory": { 949 | "required": false, 950 | "type": "string", 951 | "description": "The working directory for the command." 952 | }, 953 | "no_output_timeout": { 954 | "required": false, 955 | "type": "string", 956 | "description": "The time after which the command is terminated if there is no output." 957 | } 958 | }, 959 | "availableForVisualization": false 960 | }, 961 | "when": { 962 | "required": false, 963 | "type": "map", 964 | "description": "The when key allows you to control when a step is run based on criteria.", 965 | "subKeys": { 966 | "condition": { 967 | "required": true, 968 | "type": "string", 969 | "description": "The condition to evaluate." 970 | }, 971 | "filters": { 972 | "required": false, 973 | "type": "map", 974 | "description": "Filters for when the step should run.", 975 | "subKeys": { 976 | "branches": { 977 | "required": false, 978 | "type": "map", 979 | "description": "Branch filters.", 980 | "subKeys": { 981 | "only": { 982 | "required": false, 983 | "type": "list", 984 | "description": "List of branches to include." 985 | }, 986 | "ignore": { 987 | "required": false, 988 | "type": "list", 989 | "description": "List of branches to ignore." 990 | } 991 | } 992 | }, 993 | "tags": { 994 | "required": false, 995 | "type": "map", 996 | "description": "Tag filters.", 997 | "subKeys": { 998 | "only": { 999 | "required": false, 1000 | "type": "list", 1001 | "description": "List of tags to include." 1002 | }, 1003 | "ignore": { 1004 | "required": false, 1005 | "type": "list", 1006 | "description": "List of tags to ignore." 1007 | } 1008 | } 1009 | } 1010 | } 1011 | } 1012 | }, 1013 | "availableForVisualization": false 1014 | }, 1015 | "unless": { 1016 | "required": false, 1017 | "type": "map", 1018 | "description": "The unless key allows you to control when a step is skipped based on criteria.", 1019 | "subKeys": { 1020 | "condition": { 1021 | "required": true, 1022 | "type": "string", 1023 | "description": "The condition to evaluate." 1024 | }, 1025 | "filters": { 1026 | "required": false, 1027 | "type": "map", 1028 | "description": "Filters for when the step should be skipped.", 1029 | "subKeys": { 1030 | "branches": { 1031 | "required": false, 1032 | "type": "map", 1033 | "description": "Branch filters.", 1034 | "subKeys": { 1035 | "only": { 1036 | "required": false, 1037 | "type": "list", 1038 | "description": "List of branches to include." 1039 | }, 1040 | "ignore": { 1041 | "required": false, 1042 | "type": "list", 1043 | "description": "List of branches to ignore." 1044 | } 1045 | } 1046 | }, 1047 | "tags": { 1048 | "required": false, 1049 | "type": "map", 1050 | "description": "Tag filters.", 1051 | "subKeys": { 1052 | "only": { 1053 | "required": false, 1054 | "type": "list", 1055 | "description": "List of tags to include." 1056 | }, 1057 | "ignore": { 1058 | "required": false, 1059 | "type": "list", 1060 | "description": "List of tags to ignore." 1061 | } 1062 | } 1063 | } 1064 | } 1065 | } 1066 | }, 1067 | "availableForVisualization": false 1068 | }, 1069 | "no_output_timeout": { 1070 | "required": false, 1071 | "type": "string", 1072 | "description": "The no_output_timeout key allows you to configure the time after which a job is terminated if there is no output.", 1073 | "availableForVisualization": false 1074 | }, 1075 | "working_directory": { 1076 | "required": false, 1077 | "type": "string", 1078 | "description": "The working_directory key allows you to specify the working directory for a job.", 1079 | "availableForVisualization": false 1080 | }, 1081 | "environment": { 1082 | "required": false, 1083 | "type": "map", 1084 | "description": "The environment key allows you to set environment variables for a job.", 1085 | "availableForVisualization": true 1086 | }, 1087 | "enable_services": { 1088 | "required": false, 1089 | "type": "list", 1090 | "description": "The enable_services key allows you to enable services for a job.", 1091 | "availableForVisualization": false 1092 | } 1093 | } 1094 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .scroll { 6 | overflow-y: scroll; 7 | scrollbar-width: none; 8 | -ms-overflow-style: none; 9 | } 10 | 11 | .scroll::-webkit-scrollbar { 12 | width: 0; 13 | height: 0; 14 | } 15 | 16 | * { 17 | font-family: "Poppins", sans-serif; 18 | font-weight: 400; 19 | font-style: normal; 20 | scroll-behavior: smooth; 21 | } 22 | 23 | .debug { 24 | @apply border-2 border-red-500; 25 | } 26 | 27 | .expand-properties-pane { 28 | width: 45%; 29 | transition: 0.4s width ease-in-out; 30 | } 31 | 32 | .collapse-properties-pane { 33 | width: 0%; 34 | padding: 0; 35 | transition: 0.4s width ease-in-out; 36 | } 37 | 38 | .expand-workflow-job { 39 | height: 300px; 40 | transition: 0.4s height ease-in-out; 41 | } 42 | 43 | .collapse-workflow-job { 44 | height: 70px; 45 | transition: 0.4s height ease-in-out; 46 | } 47 | 48 | .expand-vertically { 49 | height: 100px; 50 | transition: 0.4s height ease-in-out; 51 | } 52 | 53 | .collapse-vertically { 54 | height: 40px; 55 | transition: 0.4s height ease-in-out; 56 | } 57 | 58 | .absolute-center { 59 | transform: translate(-50%, 0%); 60 | } 61 | 62 | @keyframes rotate360 { 63 | from { 64 | transform: rotate(0deg); 65 | } 66 | to { 67 | transform: rotate(360deg); 68 | } 69 | } 70 | 71 | .rotate { 72 | animation: rotate360 1s linear infinite; 73 | } 74 | 75 | .react-switch-label { 76 | transition: background-color 0.2s; 77 | } 78 | 79 | .react-switch-label .react-switch-button { 80 | transition: 0.2s; 81 | } 82 | 83 | .react-switch-checkbox:checked + .react-switch-label .react-switch-button { 84 | left: calc(100% - 2px); 85 | transform: translateX(-90%); 86 | } 87 | 88 | .react-switch-label:active .react-switch-button { 89 | width: 60px; 90 | } 91 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { Provider } from "react-redux"; 6 | import store from "./redux/store"; 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById("root") as HTMLElement 10 | ); 11 | root.render( 12 | // 13 | 14 | 15 | 16 | // 17 | ); 18 | -------------------------------------------------------------------------------- /src/pages/Development/Development.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router"; 2 | import configReference from "../../data/configReference.json"; 3 | import IconOnlyButton from "../../components/Widgets/Buttons/IconOnlyButton"; 4 | import { MdDarkMode, MdLightMode } from "react-icons/md"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { getDarkMode, setDarkMode } from "../../redux/darkMode/darkModeSlice"; 7 | import { IoArrowBack } from "react-icons/io5"; 8 | import objToArrayConverter from "../../utils/objToArrayConverter"; 9 | import { FaCheck, FaXmark, FaCircleCheck, FaCircleXmark } from "react-icons/fa6"; 10 | 11 | export default function Development() { 12 | const dispatch = useDispatch(); 13 | const navigate = useNavigate(); 14 | const darkMode = useSelector(getDarkMode); 15 | 16 | return ( 17 |
18 |
19 | 25 | } 26 | onClick={() => { 27 | navigate(-1); 28 | }} 29 | /> 30 | 34 | ) : ( 35 | 39 | ) 40 | } 41 | onClick={() => { 42 | localStorage.setItem("darkMode", JSON.stringify(!darkMode)); 43 | dispatch(setDarkMode(!darkMode)); 44 | document.documentElement.classList.contains("dark") 45 | ? document.documentElement.classList.remove("dark") 46 | : document.documentElement.classList.add("dark"); 47 | }} 48 | /> 49 |
50 |
51 |
52 |

53 | Keys 54 |

55 |

56 | Type 57 |

58 |

59 | Required 60 |

61 |

62 | Description 63 |

64 |

65 | Subkeys 66 |

67 |

68 | Available for Visualization 69 |

70 |
71 |
72 | {objToArrayConverter(configReference).map( 73 | (item: any, key: number) => { 74 | const keyName = item[0]; 75 | const { type, required, description, subKeys, availableForVisualization } = item[1]; 76 | return ( 77 |
82 |

83 | {keyName} 84 |

85 |

86 | {type} 87 |

88 |

89 | {required ? ( 90 | 91 | ) : ( 92 | 93 | )} 94 |

95 |

96 | {description} 97 |

98 |

99 | {subKeys 100 | ? objToArrayConverter(subKeys).map( 101 | (subItem: any, key: number) => { 102 | const subKeyName = subItem[0]; 103 | const { 104 | type: subItemType, 105 | required: subItemRequired, 106 | description: subItemDescription, 107 | } = item[1]; 108 | return ( 109 |

110 |

111 | {subKeyName} 112 |

113 |

114 | {"Type: " + subItemType} 115 |

116 |

117 | {"Required: "} 118 | {subItemRequired ? ( 119 | 120 | ) : ( 121 | 122 | )} 123 |

124 |

125 | {"Description: " + subItemDescription} 126 |

127 |
128 | ); 129 | } 130 | ) 131 | : "-"} 132 |

133 |

134 | {availableForVisualization ? ( 135 | 136 | ) : ( 137 | 138 | )} 139 |

140 |
141 | ); 142 | } 143 | )} 144 |
145 |
146 |
147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/pages/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { SiCircleci } from "react-icons/si"; 2 | import PrimaryButton from "../../components/Widgets/Buttons/PrimaryButton"; 3 | import { FaDocker, FaPlus } from "react-icons/fa6"; 4 | import { AiOutlineCloudUpload } from "react-icons/ai"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { setDataReducer } from "../../redux/data/dataSlice"; 7 | import yaml from "js-yaml"; 8 | import { useNavigate } from "react-router"; 9 | import { GoWorkflow } from "react-icons/go"; 10 | import { MdDarkMode, MdLightMode, MdWork } from "react-icons/md"; 11 | import { HiCommandLine } from "react-icons/hi2"; 12 | import { getDarkMode, setDarkMode } from "../../redux/darkMode/darkModeSlice"; 13 | import IconOnlyButton from "../../components/Widgets/Buttons/IconOnlyButton"; 14 | import SecondaryButton from "../../components/Widgets/Buttons/SecondaryButton"; 15 | import { IoConstructOutline } from "react-icons/io5"; 16 | import { FaGithub } from "react-icons/fa"; 17 | import { useEffect, useState } from "react"; 18 | import GitHub from "../../components/GitHub/GitHub"; 19 | import { getGithubData } from "../../redux/githubData/githubDataSlice"; 20 | 21 | export default function Home() { 22 | const dispatch = useDispatch(); 23 | const navigate = useNavigate(); 24 | const darkMode = useSelector(getDarkMode); 25 | const githubData = useSelector(getGithubData); 26 | const [viewGithubWindow, setViewGithubWindow] = useState(false); 27 | 28 | return ( 29 | <> 30 | {viewGithubWindow && } 31 |
32 |
33 | 40 | ) : ( 41 | 45 | ) 46 | } 47 | onClick={() => { 48 | localStorage.setItem("darkMode", JSON.stringify(!darkMode)); 49 | dispatch(setDarkMode(!darkMode)); 50 | document.documentElement.classList.contains("dark") 51 | ? document.documentElement.classList.remove("dark") 52 | : document.documentElement.classList.add("dark"); 53 | }} 54 | /> 55 | } 58 | onClick={() => { 59 | navigate("/development"); 60 | }} 61 | /> 62 |
63 |
68 |
69 |

70 | CircleCI Config Visualizer 71 |

72 |

73 | Bringing Clarity to Your CI/CD Pipelines 74 |

75 |

76 | Gain a comprehensive visual understanding of your CircleCI 77 | pipelines with an intuitive and interactive config visualizer 78 |

79 |
80 | } 82 | label={githubData?.token?`Load from GitHub`:`Connect GitHub`} 83 | className="px-4" 84 | color="bg-gray-700" 85 | onClick={()=>{setViewGithubWindow(true)}} 86 | /> 87 | { 93 | if (e?.target?.files?.length) { 94 | var file = e?.target?.files[0]; 95 | var reader = new FileReader(); 96 | reader.onload = function (event) { 97 | const data: any = event?.target?.result; 98 | const yamlData = yaml.load(data); 99 | localStorage.setItem( 100 | "currentFile", 101 | JSON.stringify(yamlData) 102 | ); 103 | dispatch(setDataReducer(yamlData)); 104 | navigate("/editor"); 105 | }; 106 | reader.readAsText(file); 107 | } 108 | }} 109 | /> 110 | 117 |
118 |
119 |
120 |
121 | 122 |
123 |
124 | 125 |
126 |
127 | 128 |
129 |
130 | 131 |
132 | 136 |
137 |
138 |
139 | 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test'; 8 | readonly PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module '*.avif' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.bmp' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.gif' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.jpg' { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module '*.jpeg' { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module '*.png' { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.webp' { 43 | const src: string; 44 | export default src; 45 | } 46 | 47 | declare module '*.svg' { 48 | import * as React from 'react'; 49 | 50 | export const ReactComponent: React.FunctionComponent & { title?: string }>; 53 | 54 | const src: string; 55 | export default src; 56 | } 57 | 58 | declare module '*.module.css' { 59 | const classes: { readonly [key: string]: string }; 60 | export default classes; 61 | } 62 | 63 | declare module '*.module.scss' { 64 | const classes: { readonly [key: string]: string }; 65 | export default classes; 66 | } 67 | 68 | declare module '*.module.sass' { 69 | const classes: { readonly [key: string]: string }; 70 | export default classes; 71 | } 72 | -------------------------------------------------------------------------------- /src/redux/activeEntity/activeEntitySlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | type: "", 5 | entity: {}, 6 | }; 7 | 8 | const activeEntitySlice = createSlice({ 9 | name: "activeEntity", 10 | initialState, 11 | reducers: { 12 | setActiveEntity: (state, action) => { 13 | state.type = action.payload.type; 14 | state.entity = action.payload.entity; 15 | }, 16 | }, 17 | }); 18 | 19 | export const { setActiveEntity } = activeEntitySlice.actions; 20 | export const getActiveEntity = (state: { 21 | activeEntity: { type: string; entity: any }; 22 | }) => state.activeEntity; 23 | export default activeEntitySlice.reducer; 24 | -------------------------------------------------------------------------------- /src/redux/darkMode/darkModeSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | darkMode: false 5 | }; 6 | 7 | const darkModeSlice = createSlice({ 8 | name: "darkMode", 9 | initialState, 10 | reducers: { 11 | setDarkMode: (state, action) => { 12 | state.darkMode = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setDarkMode } = darkModeSlice.actions; 18 | export const getDarkMode = (state: {darkMode: { darkMode: boolean }}) => 19 | state.darkMode.darkMode; 20 | export default darkModeSlice.reducer; 21 | -------------------------------------------------------------------------------- /src/redux/data/dataSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import objToArrayConverter from "../../utils/objToArrayConverter"; 3 | 4 | const initialState = { 5 | data: { 6 | executors: [], 7 | orbs: [], 8 | commands: [], 9 | jobs: [], 10 | workflows: [], 11 | }, 12 | }; 13 | 14 | const dataSlice = createSlice({ 15 | name: "data", 16 | initialState, 17 | reducers: { 18 | setDataReducer: (state, action) => { 19 | state.data = action.payload; 20 | }, 21 | setExecutorsReducer: (state, action) => { 22 | state.data.executors = action.payload; 23 | }, 24 | setOrbsReducer: (state, action) => { 25 | state.data.orbs = action.payload; 26 | }, 27 | setCommandsReducer: (state, action) => { 28 | state.data.commands = action.payload; 29 | }, 30 | setJobsReducer: (state, action) => { 31 | state.data.jobs = action.payload; 32 | }, 33 | setWorkflowsReducer: (state, action) => { 34 | state.data.workflows = action.payload; 35 | }, 36 | }, 37 | }); 38 | 39 | const getAllExecutors = (state: { data: { data: { executors: any[] } } }) => 40 | objToArrayConverter(state.data.data.executors); 41 | const getAllOrbs = (state: { data: { data: { orbs: any[] } } }) => 42 | objToArrayConverter(state.data.data.orbs); 43 | const getAllCommands = (state: { data: { data: { commands: any[] } } }) => 44 | objToArrayConverter(state.data.data.commands); 45 | const getAllJobs = (state: { data: { data: { jobs: any[] } } }) => 46 | objToArrayConverter(state.data.data.jobs); 47 | const getAllWorkflows = (state: { data: { data: { workflows: any[] } } }) => 48 | objToArrayConverter(state.data.data.workflows); 49 | 50 | export const { 51 | setDataReducer, 52 | setExecutorsReducer, 53 | setOrbsReducer, 54 | setCommandsReducer, 55 | setJobsReducer, 56 | setWorkflowsReducer, 57 | } = dataSlice.actions; 58 | export const getData = (state: { data: { data: any } }) => state.data.data; 59 | export { 60 | getAllExecutors, 61 | getAllOrbs, 62 | getAllCommands, 63 | getAllJobs, 64 | getAllWorkflows, 65 | }; 66 | export default dataSlice.reducer; 67 | -------------------------------------------------------------------------------- /src/redux/githubData/githubDataSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | githubData: {} 5 | }; 6 | 7 | const githubDataSlice = createSlice({ 8 | name: "githubData", 9 | initialState, 10 | reducers: { 11 | setGithubData: (state, action) => { 12 | state.githubData = JSON.parse(action.payload); 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setGithubData } = githubDataSlice.actions; 18 | export const getGithubData = (state: {githubData: { githubData: any }}) => 19 | state.githubData.githubData; 20 | export default githubDataSlice.reducer; 21 | -------------------------------------------------------------------------------- /src/redux/selectedEntity/selectedEntitySlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | type: "", 5 | entity: {}, 6 | }; 7 | 8 | const selectedEntitySlice = createSlice({ 9 | name: "selectedEntity", 10 | initialState, 11 | reducers: { 12 | setSelectedEntity: (state, action) => { 13 | state.type = action.payload.type; 14 | state.entity = action.payload.entity; 15 | }, 16 | }, 17 | }); 18 | 19 | export const { setSelectedEntity } = selectedEntitySlice.actions; 20 | export const getSelectedEntity = (state: { 21 | selectedEntity: { type: string; entity: any }; 22 | }) => state.selectedEntity; 23 | export default selectedEntitySlice.reducer; 24 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import dataReducer from "./data/dataSlice"; 3 | import selectedEntityReducer from "./selectedEntity/selectedEntitySlice"; 4 | import activeEntityReducer from "./activeEntity/activeEntitySlice"; 5 | import visibleEntitiesReducer from "./visibleEntities/visibleEntitiesSlice"; 6 | import darkModeReducer from "./darkMode/darkModeSlice"; 7 | import githubDataReducer from "./githubData/githubDataSlice"; 8 | 9 | const store = configureStore({ 10 | reducer: { 11 | data: dataReducer, 12 | selectedEntity: selectedEntityReducer, 13 | activeEntity: activeEntityReducer, 14 | darkMode: darkModeReducer, 15 | githubData: githubDataReducer, 16 | visibleEntities: visibleEntitiesReducer, 17 | }, 18 | }); 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /src/redux/visibleEntities/visibleEntitiesSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | type initialState = { 4 | entities: { 5 | workflows: string[]; 6 | jobs: string[]; 7 | executors: string[]; 8 | orbs: string[]; 9 | commands: string[]; 10 | }; 11 | }; 12 | 13 | const initialState: initialState = { 14 | entities: { 15 | workflows: [], 16 | jobs: [], 17 | executors: [], 18 | orbs: [], 19 | commands: [], 20 | }, 21 | }; 22 | 23 | const visibleEntitiesSlice = createSlice({ 24 | name: "visibleEntities", 25 | initialState, 26 | reducers: { 27 | setWorkflowsVisible: (state, action) => { 28 | state.entities.workflows = action.payload; 29 | }, 30 | setJobsVisible: (state, action) => { 31 | state.entities.jobs = action.payload; 32 | }, 33 | setExecutorsVisible: (state, action) => { 34 | state.entities.executors = action.payload; 35 | }, 36 | setOrbsVisible: (state, action) => { 37 | state.entities.orbs = action.payload; 38 | }, 39 | setCommandsVisible: (state, action) => { 40 | state.entities.commands = action.payload; 41 | }, 42 | }, 43 | }); 44 | 45 | export const { 46 | setWorkflowsVisible, 47 | setJobsVisible, 48 | setOrbsVisible, 49 | setCommandsVisible, 50 | setExecutorsVisible, 51 | } = visibleEntitiesSlice.actions; 52 | export const getVisibleJobs = (state: { visibleEntities: initialState }) => 53 | state.visibleEntities.entities.jobs; 54 | export const getVisibleWorkflows = (state: { visibleEntities: initialState }) => 55 | state.visibleEntities.entities.workflows; 56 | export const getVisibleOrbs = (state: { visibleEntities: initialState }) => 57 | state.visibleEntities.entities.orbs; 58 | export const getVisibleExecutors = (state: { visibleEntities: initialState }) => 59 | state.visibleEntities.entities.executors; 60 | export const getVisibleCommands = (state: { visibleEntities: initialState }) => 61 | state.visibleEntities.entities.commands; 62 | export default visibleEntitiesSlice.reducer; 63 | -------------------------------------------------------------------------------- /src/utils/checkEmptyArray.ts: -------------------------------------------------------------------------------- 1 | const checkEmptyArray = (arr: any[]) => { 2 | if(!arr) return false; 3 | return arr.length?true:false; 4 | } 5 | 6 | export default checkEmptyArray; -------------------------------------------------------------------------------- /src/utils/checkEmptyObj.ts: -------------------------------------------------------------------------------- 1 | const checkEmptyObj = (obj: any) => { 2 | if(!obj) return true; 3 | return Object.keys(obj).length === 0; 4 | } 5 | 6 | export default checkEmptyObj; -------------------------------------------------------------------------------- /src/utils/checkIfArray.ts: -------------------------------------------------------------------------------- 1 | const checkIfArray = (arr) => { 2 | return Array.isArray(arr); 3 | }; 4 | 5 | export default checkIfArray; 6 | -------------------------------------------------------------------------------- /src/utils/objToArrayConverter.ts: -------------------------------------------------------------------------------- 1 | const objToArrayConverter = (obj: any) => { 2 | if(!obj) return []; 3 | if(Array.isArray(obj)) return obj; 4 | return Object.keys(obj).map((key) => [key, obj[key]]); 5 | } 6 | 7 | export default objToArrayConverter; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | "./src/**/*.{js,jsx,ts,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "noImplicitAny": false, 19 | "allowImportingTsExtensions": true, 20 | // "typeRoots": [ "./types"] 21 | }, 22 | "include": ["src"], 23 | "exclude": ["types"] 24 | } 25 | -------------------------------------------------------------------------------- /types/react-resizable-panels/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-resizable-panels' --------------------------------------------------------------------------------