├── .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 | [](https://github.com/syngenta/circleci-config-visualizer/actions/workflows/deploy.yml)
6 | [](https://img.shields.io/github/stars/syngenta/circleci-config-visualizer)
7 |
8 | [](https://img.shields.io/github/license/syngenta/circleci-config-visualizer)
9 | [](https://img.shields.io/github/v/release/syngenta/circleci-config-visualizer)
10 | [](https://img.shields.io/github/issues/syngenta/circleci-config-visualizer)
11 | [](https://img.shields.io/github/issues-pr/syngenta/circleci-config-visualizer)
12 |
13 | [](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 | 
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 | 
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 | 
42 |
43 | 4. The file gets opened in a visual editor:
44 |
45 | 
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 | You need to enable JavaScript to run this app.
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 |
{setOpenTab(tab)}}>
39 | {tab}
40 |
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 |
22 | {icon}
23 |
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 |
26 | {icon}
27 | {label}
28 |
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 |
26 | {icon}
27 | {label}
28 |
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 |
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 | {label}
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 |
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 |
31 | {item.icon ? item.icon : null}
32 |
33 | {item.label}
34 |
35 |
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 | `dark:text-white text-[13px] cursor-pointer`,
62 | singleValue: (state) =>
63 | `${!disabled && "dark:text-white"} text-[13px] cursor-pointer`,
64 | control: (state) =>
65 | `dark:bg-gray-900/70 dark:text-white border-[1px] text-[13px] ${
66 | state.isFocused
67 | ? "border-blue-500"
68 | : "border-gray-400 dark:border-gray-700"
69 | } cursor-pointer`,
70 | option: (state) =>
71 | `${
72 | state.isSelected ? "dark:bg-blue-500" : " dark:bg-gray-900/70"
73 | } scroll text-[13px] cursor-pointer`,
74 | menu: (state) => `dark:bg-gray-900/70 text-[13px] scroll h-[100px]`,
75 | menuList: (state) =>
76 | `dark:text-white dark:bg-gray-900/70 text-[13px] scroll h-[100px] overflow-y-scroll`,
77 | }}
78 | />
79 | )}
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/Widgets/ToggleSwitch/ToggleSwitch.tsx:
--------------------------------------------------------------------------------
1 | type ToggleSwitch = {
2 | checked: boolean;
3 | handleToggle: () => void;
4 | size?: number;
5 | onIcon?: React.ReactNode;
6 | offIcon?: React.ReactNode;
7 | onColor?: string;
8 | offColor?: string;
9 | className?: string;
10 | };
11 | export default function ToggleSwitch({
12 | checked,
13 | handleToggle,
14 | size = 30,
15 | onIcon,
16 | offIcon,
17 | onColor = "bg-blue-500",
18 | offColor = "bg-white",
19 | className
20 | }: ToggleSwitch) {
21 | return (
22 |
26 |
33 |
39 | {onIcon}
40 |
44 | {offIcon}
45 |
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 |
114 |
115 | Upload
116 |
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'
--------------------------------------------------------------------------------