├── .eslintrc.json
├── .gitignore
├── README.md
├── client
├── App.tsx
├── assets
│ ├── dep-icon.png
│ ├── gif2.gif
│ ├── gif3.gif
│ ├── images
│ │ ├── KN-logo.png
│ │ ├── anthony.png
│ │ ├── clusterPic.png
│ │ ├── docs.png
│ │ ├── edit.png
│ │ ├── favicon.ico
│ │ ├── github.png
│ │ ├── jeremiah.png
│ │ ├── linkedin.png
│ │ ├── mainDashBoard.png
│ │ ├── michaelvan.png
│ │ ├── network.png
│ │ └── stephen.jpg
│ ├── logo.png
│ ├── name_blue.png
│ ├── node-icon.png
│ ├── ns-icon.png
│ ├── pod-icon.png
│ └── svc-icon.png
├── components
│ ├── CRUD
│ │ ├── CRUDMini.tsx
│ │ ├── CRUDModal.tsx
│ │ └── CRUDSelector.tsx
│ ├── Contexts.tsx
│ ├── Graphs
│ │ ├── BarGraph.tsx
│ │ ├── GaugeChart.tsx
│ │ └── LineGraph.tsx
│ ├── Logs.tsx
│ ├── Mapothy.tsx
│ └── helperFunctions.ts
├── containers
│ ├── InvisibleNavbar
│ │ ├── InvisibleNavbar.tsx
│ │ └── InvisibleNavbarModal.tsx
│ ├── LogsContainer.tsx
│ ├── MainContainer.tsx
│ ├── MainDashBoard.tsx
│ ├── Navbar.tsx
│ └── NetworkPerformance.tsx
├── index.html
├── index.tsx
└── sass
│ ├── App.scss
│ ├── _CRUDModal.scss
│ ├── _graph.scss
│ ├── _invisNav.scss
│ ├── _logs.scss
│ ├── _mainContainer.scss
│ ├── _map.scss
│ ├── _navBar.scss
│ ├── _variables.scss
│ └── _vis-network.scss
├── cypress.config.ts
├── cypress
├── components
│ ├── GaugeChart.cy.tsx
│ ├── InvisibleNavbar.cy.tsx
│ └── LineGraph.cy.tsx
├── e2e
│ └── frontEndTest.cy.ts
├── fixtures
│ └── example.json
└── support
│ ├── commands.ts
│ ├── component-index.html
│ ├── component.ts
│ └── e2e.ts
├── jest.config.ts
├── jest
└── super.test.ts
├── package-lock.json
├── package.json
├── scripts
└── loadtest.js
├── server
├── controllers
│ ├── clusterController.ts
│ ├── crudController.ts
│ ├── k6Controller.ts
│ ├── lokiController.ts
│ ├── mapController.ts
│ └── promController.ts
├── routers
│ ├── apiRouter.ts
│ ├── clusterRouter.ts
│ ├── crudRouter.ts
│ ├── k6Router.ts
│ ├── lokiRouter.ts
│ ├── mapRouter.ts
│ └── promRouter.ts
└── server.ts
├── tsconfig.eslint.json
├── tsconfig.json
├── types
├── png.d.ts
├── react-graph-vis.d.ts
└── types.ts
├── webpack.config.ts
└── yamls
├── Daemonset.yaml
└── values.yml
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "airbnb",
8 | "standard-with-typescript",
9 | "plugin:react/recommended"
10 | ],
11 | "parserOptions": {
12 | "sourceType": "module",
13 | "project": "tsconfig.eslint.json",
14 | "tsconfigRootDir": "./"
15 | },
16 | "plugins": [
17 | "react"
18 | ],
19 | "rules": {
20 | "import/extensions": [
21 | "error",
22 | "ignorePackages",
23 | {
24 | "": "never",
25 | "js": "never",
26 | "jsx": "never",
27 | "ts": "never",
28 | "tsx": "never"
29 | }
30 | ],
31 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }]
32 | },
33 | "root": true,
34 | "settings": {
35 | "import/resolver": {
36 | "node": {
37 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # General
2 | .DS_Store
3 |
4 | # dependencies
5 | node_modules/
6 |
7 | # logs
8 | # npm-debug.log*
9 | # yarn-debug.log*
10 | # yarn-error.log*
11 |
12 | # output from webpack
13 | dist
14 |
15 | #dotenv
16 | .env
17 |
18 | #GKE folder
19 | gke-setup
20 | #EKS folder
21 | eks-setup
22 |
23 | #yaml files
24 | prometheus.yaml
25 | kubeconfig.yaml
26 | lokiYaml
27 |
28 | #jeremiahs garbage
29 | #CRUDSelector.tsx
30 |
31 | coverage
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 
6 |
7 | [](https://www.javascript.com/)
8 | [](https://www.typescriptlang.org/)
9 | [](https://nodejs.org/)
10 | [](https://expressjs.com/)
11 | [](https://reactjs.org/)
12 | [](https://sass-lang.com/)
13 | [](https://developer.mozilla.org/en-US/docs/Web/CSS)
14 | [](https://developer.mozilla.org/en-US/docs/Web/HTML)
15 | [](https://webpack.js.org/)
16 | [](https://jestjs.io/)
17 | [](https://www.cypress.io/)
18 | [](https://www.docker.com/)
19 | [](https://kubernetes.io/)
20 | [](https://prometheus.io/)
21 | [](https://grafana.com/)
22 | [](https://www.chartjs.org/)
23 | [](https://helm.sh/)
24 | [](https://cloud.google.com/kubernetes-engine)
25 | [](https://azure.microsoft.com/en-us/services/kubernetes-service/)
26 | [](https://aws.amazon.com/eks/)
27 |
36 |
37 |
38 | ---
39 |
40 |
41 | Quick Links
42 |
43 |
44 |
45 | Website
46 | Medium
47 |
48 | LinkedIn
49 |
50 |
51 |
52 |
53 |
54 | ## KuberNautical
55 | Introducing KuberNautical, an open-source Kuberenetes developer tool designed to empower you with unparalleled insights and control over your Kubernetes clusters. Seamlessly merging the worlds of metrics analysis and streamlined cluster management, KuberNautical redefines the way you interact with your kubernetes infrastructure.
56 |
57 | [Getting Started](#set-up)
58 | ## Features
59 | ### 2D Cluster view
60 | Upon application launch, users can view a robust 2D configuration of their desired cluster.
61 |
62 | 
63 |
64 | ### Metrics Visualization
65 | Users are able to view important metrics and logs pertinent to cluster health.
66 |
67 | 
68 |
69 | ### Cluster Logs
70 | Users are able to view logs regarding events occuring within cluster. These logs can be filtered by namespace and pod.
71 |
72 | 
73 |
74 | ### Cluster Manipulation
75 | Users have the ability to make live changes to thier cluster in a variety of ways.
76 |
77 | 
78 | Users can create a new namespace within the current cluster context through the "Edit Cluster" Modal.
79 |
80 | 
81 | Users can create a new deployment within a given namespace using a public docker image.
82 |
83 | 
84 | Users can scale deployments as needed to meet demand.
85 |
86 | 
87 | Users can expose deployments within any chosen method, at the given ports.
88 |
89 | 
90 | Users can remove a namespace and all resources inside of it.
91 |
92 | ### Load Testing
93 | Users are able to apply a load test to a deployed application of their choosing.
94 | 
95 | Load Test Result
96 | 
97 | ## Set Up
98 | 1. Fork this repository and clone it onto your local machine:
99 | ```
100 | git clone https://github.com/oslabs-beta/Kubernautical
101 | ```
102 | 2. Install all package dependencies
103 | ```
104 | npm install
105 | ```
106 | ### Before you start checklist
107 | - [Cluster](#cluster-setup) from provider of choice
108 | - [Kubectl](https://kubernetes.io/docs/tasks/tools/) installed
109 | - [Kubectl Cheat Sheet](https://kubernetes.io/docs/reference/kubectl/cheatsheet/)
110 | - [Helm](https://helm.sh/docs/intro/install/) installed
111 | - SDK for your cluster(see specific instructions for your OS)
112 | - [GCP](https://cloud.google.com/sdk/docs/install) CLI
113 | - [Azure](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt) CLI
114 | - [AWS](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) CLI
115 | - [Prometheus](#prometheus) deployed
116 | - [Grafana Loki](#grafana-loki) deployed
117 | - [Grafana k6](https://k6.io/docs/get-started/installation/) installed
118 |
119 | ## Cluster Setup
120 |
121 | **Google Cluster**
122 | 1. Create a standard cluster with [Google](https://cloud.google.com/kubernetes-engine) (autopilot clusters will not work).
123 | 2. Click Connect in GCP and copy the command into your terminal or fill out the command below with pertinent information:
124 | ```
125 | gcloud container clusters get-credentials \
126 | --zone --project
127 | ```
128 | **Microsoft Cluster**
129 | 1. Create a cluster with [Microsoft](https://azure.microsoft.com/en-us/products/kubernetes-service).
130 | 2. Click Connect in Azure and copy the command into your terminal or fill out the command below with pertinent information:
131 | ```
132 | az aks get-credentials --resource-group \
133 | --name --admin
134 | ```
135 | **Amazon Cluster**
136 | 1. Create a cluster with [AWS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.htm).
137 | 2. Fill out the command below with pertinent information:
138 | ```
139 | aws eks update-kubeconfig --region --name
140 | ```
141 | **Minikube Cluster**
142 | 1. [Install Docker Desktop](https://www.docker.com/products/docker-desktop/).
143 | **We Reccommend you have 4g+ of free ram as Docker Desktop can overload and crash your machine.**
144 | 2. Install [Minikube](https://minikube.sigs.k8s.io/docs/start).
145 | 3. Create a cluster
146 | ```
147 | minikube start
148 | ```
149 | **Minikube Teardown**
150 | 1. Stop your cluster
151 | ```
152 | minikube stop
153 | ```
154 | 2. Delete your cluster (can also use '--all' flag to delete all)
155 | ```
156 | minikube delete
157 | ```
158 | ## Once your cluster is created
159 |
160 |
161 | ***Confirm that your cluster is in your kube config***
162 | ```
163 | kubectl config view
164 | ```
165 | ***Confirm context is set to desired cluster***
166 | ```
167 | kubectl config use-context
168 | ```
169 | ## Prometheus
170 | 1. Create a namespace for prometheus
171 | ```
172 | kubectl create ns prometheus
173 | ```
174 | 2. Install the helm charts
175 | ```
176 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
177 | ```
178 | 3. Apply the helm charts and install prometheus in the namespace
179 | ```
180 | helm upgrade -i prometheus prometheus-community/prometheus --namespace prometheus
181 | ```
182 | 4. For our application you need to expose the prometheus-server as a load balancer
183 | ```
184 | kubectl expose deployment prometheus-server --port=80 --target-port=9090 --type=LoadBalancer \
185 | --name prometheus-server-lb --n prometheus
186 | ```
187 |
188 | **Deployment, target port, type and namespace are all mandatory as above, while port, and name can be customized if desired but name MUST start with `prometheus-server`**
189 |
190 |
191 | ## Grafana-loki
192 | 1. Use helm to add Grafana
193 | ```
194 | helm repo add grafana https://grafana.github.io/helm-charts
195 | ```
196 | 2. Update helm if necessary
197 | ```
198 | helm repo update
199 | ```
200 | 3. Check if helm charts installed
201 | ```
202 | helm search repo loki
203 | ```
204 | 4. Create a namespace for Loki
205 | ```
206 | kubectl create ns loki
207 | ```
208 | 5. Install Loki on the `loki` namespace
209 | ```
210 | helm upgrade --install --namespace loki logging grafana/loki -f yamls/values.yml \
211 | --set loki auth_enabled=false
212 | ```
213 | 6. Deploy Grafana with Loki data source to enable log aggregation
214 | ```
215 | helm upgrade --install --namespace=loki loki-grafana grafana/grafana
216 | ```
217 | 7. Deploy Promtail to enable log scraping from whole cluster
218 | ```
219 | helm upgrade --install promtail grafana/promtail -n loki
220 | ```
221 | 8. Create a daemonset to ensure system-level logging
222 | ```
223 | kubectl apply -f yamls/Daemonset.yaml
224 | ```
225 | 9. Expose `loki-gateway` as a load balancer
226 | ```
227 | kubectl expose deployment loki-gateway --port=80 --target-port=8080 --type=LoadBalancer \
228 | --name loki-gateway-lb -n loki
229 | ```
230 |
231 | ***Naming conventions for namespaces and services created are customizable, but name for `loki-gateway` service must start with `loki-gateway`.***
232 |
233 | # Contributing
234 | Contributions play a vital role in the open-source community. Any contributions are greatly appreciated!
235 |
236 | - Fork the project.
237 | - Create and work off of your feature branch.
238 | - Create a pull request with detailed description of your changes from your feature branch to dev branch.
239 | - Inform us upon PR submission. Once the changes are reviewed and approved, we will merge your code into the main repository.
240 |
241 | # Meet the Team
242 | |
|
|
|
|
243 | | ------------- | ------------- |------------- | ------------- |
244 | | Jeremiah Hogue
[
](https://github.com/NotHogue) [
](https://www.linkedin.com/in/jeremiah-hogue/)| Anthony Vuong
[
](https://github.com/AnthonyKTVuong) [
](https://www.linkedin.com/in/anthonyktvuong/) | Stephen Acosta
[
](https://github.com/STAC98) [
](https://www.linkedin.com/in/staclb) | Michael Van
[
](https://github.com/michaelvan996) [
](https://www.linkedin.com/in/michael-van-901533222/) |
245 |
--------------------------------------------------------------------------------
/client/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ReactElement, useState } from 'react'
2 | import { type globalServiceObj, type ClusterData } from '../types/types'
3 | import './sass/App.scss'
4 | import MainContainer from './containers/MainContainer'
5 | import Navbar from './containers/Navbar'
6 | import { GlobalContext } from './components/Contexts'
7 |
8 | const serviceArr: globalServiceObj[] = []
9 | const defaultClusterData: ClusterData = {}
10 | function App (): ReactElement {
11 | const [globalServices, setGlobalServices] = useState(serviceArr)
12 | const [globalTimer, setGlobalTimer] = useState(0)
13 | const [globalServiceTest, setGlobalServiceTest] = useState('')
14 | const [globalClusterContext, setGlobalClusterContext] = useState('')
15 | const [showEditModal, setShowEditModal] = useState(false)
16 | const [globalCrudChange, setGlobalCrudChange] = useState(false)
17 | const [ongoingCrudChange, setOngoingCrudChange] = useState(false)
18 | const [globalClusterData, setGlobalClusterData] = useState(defaultClusterData)
19 | return (
20 | <>
21 |
22 |
42 |
43 |
44 |
45 |
46 |
47 | >
48 | )
49 | }
50 |
51 | export default App
52 |
--------------------------------------------------------------------------------
/client/assets/dep-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/dep-icon.png
--------------------------------------------------------------------------------
/client/assets/gif2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/gif2.gif
--------------------------------------------------------------------------------
/client/assets/gif3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/gif3.gif
--------------------------------------------------------------------------------
/client/assets/images/KN-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/KN-logo.png
--------------------------------------------------------------------------------
/client/assets/images/anthony.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/anthony.png
--------------------------------------------------------------------------------
/client/assets/images/clusterPic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/clusterPic.png
--------------------------------------------------------------------------------
/client/assets/images/docs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/docs.png
--------------------------------------------------------------------------------
/client/assets/images/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/edit.png
--------------------------------------------------------------------------------
/client/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/favicon.ico
--------------------------------------------------------------------------------
/client/assets/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/github.png
--------------------------------------------------------------------------------
/client/assets/images/jeremiah.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/jeremiah.png
--------------------------------------------------------------------------------
/client/assets/images/linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/linkedin.png
--------------------------------------------------------------------------------
/client/assets/images/mainDashBoard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/mainDashBoard.png
--------------------------------------------------------------------------------
/client/assets/images/michaelvan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/michaelvan.png
--------------------------------------------------------------------------------
/client/assets/images/network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/network.png
--------------------------------------------------------------------------------
/client/assets/images/stephen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/stephen.jpg
--------------------------------------------------------------------------------
/client/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/logo.png
--------------------------------------------------------------------------------
/client/assets/name_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/name_blue.png
--------------------------------------------------------------------------------
/client/assets/node-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/node-icon.png
--------------------------------------------------------------------------------
/client/assets/ns-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/ns-icon.png
--------------------------------------------------------------------------------
/client/assets/pod-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/pod-icon.png
--------------------------------------------------------------------------------
/client/assets/svc-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/svc-icon.png
--------------------------------------------------------------------------------
/client/components/CRUD/CRUDMini.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-shadow */
2 | import React, { useState, useContext, type ReactElement } from 'react'
3 | import { v4 as uuidv4 } from 'uuid'
4 | import { type MiniProps } from '../../../types/types'
5 | import { GlobalContext } from '../Contexts'
6 |
7 | function CRUDMini (
8 | {
9 | style,
10 | setShowModal,
11 | modalType,
12 | crudSelection,
13 | ns,
14 | service,
15 | deployment
16 | }: MiniProps
17 | ): ReactElement {
18 | const {
19 | globalClusterData,
20 | setShowEditModal, globalClusterContext,
21 | setGlobalCrudChange, globalCrudChange,
22 | setOngoingCrudChange
23 | } = useContext(GlobalContext)
24 | const [form, setForm] = useState('')
25 | const [form2, setForm2] = useState('')
26 | const [exposeType, setExposeType] = useState('')
27 | const [targetPort, setTargetPort] = useState(0)
28 | const [port, setPort] = useState(0)
29 | // dynamically set inner text for modal
30 | let name
31 | if (crudSelection === 'service') name = service
32 | if (crudSelection === 'deployment') name = deployment
33 | if (crudSelection === 'namespace') name = ns
34 | let innerText
35 | if (modalType === 'create') innerText = `Enter ${crudSelection} here`
36 | if (modalType === 'delete') innerText = name === undefined ? 'Please make a selection' : `Are you sure you want to remove ${name}?`
37 | // completely different text for some selections
38 | if (crudSelection === 'deployment' && modalType === 'edit') innerText = 'Scale Deployment'
39 | // dynamically find the obj selected for changes
40 | const obj = (globalClusterData !== undefined) ? globalClusterData[`${crudSelection}s`].find(({ name }: any) => name === (crudSelection === 'service' ? service : deployment)) : null
41 | // assign previous variable for curent scale of deployment
42 | const oldReplicas = obj?.availableReplicas ?? 0
43 | const [scale, setScale] = useState(oldReplicas)
44 |
45 | const crudFunction = async (): Promise => {
46 | try {
47 | let query = 'api/crud/'
48 | let type = ''
49 | if (modalType === 'create' && form === '') { alert('Please fill out all fields'); return }
50 | // build query string for back-end operations based on user input
51 | switch (crudSelection) {
52 | case 'namespace':
53 | query += `ns?namespace=${modalType === 'create' ? form : ns}&crud=${modalType}&context=${globalClusterContext ?? ''}`
54 | break
55 | case 'deployment':
56 | if (modalType === 'edit') {
57 | if (scale !== oldReplicas) type = 'scale'
58 | if (form2 !== '' && exposeType !== '') type = 'expose'
59 | } else { type = modalType }
60 | query += `dep?namespace=${ns}&crud=${type}&${type === 'expose' ? `name=${form2}` : `image=${form2}`}
61 | &replicas=${scale as string}&deployment=${deployment !== '' ? deployment : ''}&old=${oldReplicas as string}
62 | &context=${globalClusterContext ?? ''}&port=${port}&targetPort=${targetPort}&type=${exposeType}`
63 | break
64 | case 'service':
65 | query += `svc?namespace=${ns}&crud=${modalType}&service=${service}&context=${globalClusterContext ?? ''}
66 | &port=${port}&targetPort=${targetPort}`
67 | break
68 | default:
69 | break
70 | }
71 | const response = await fetch(query)
72 | if (setGlobalCrudChange !== undefined) {
73 | globalCrudChange === true ? setGlobalCrudChange(false) : setGlobalCrudChange(true)
74 | }
75 | if (setOngoingCrudChange !== undefined) setOngoingCrudChange(false)
76 | if (!response.ok) throw new Error()
77 | } catch (error) {
78 | console.log(error)
79 | }
80 | }
81 |
82 | return (
83 | <>
84 |
87 |
91 |
94 | {innerText}
95 |
96 |
97 | {modalType === 'create' && (
98 | <>
99 |
{ setForm(e.target.value) }}
104 | required
105 | />
106 |
107 | {crudSelection !== 'namespace' && (
108 | { setForm2(e.target.value) }}
114 | required
115 | />
116 | )}
117 |
118 | >
119 | )}
120 | {(modalType === 'edit' && crudSelection === 'deployment') && (
121 | <>
122 |
{ setScale(Number(e.target.value)) }}
128 | />
129 |
132 | Expose as Service
133 |
134 |
135 |
{ setForm2(e.target.value) }}
141 | />
142 |
176 |
179 |
182 | Port:
183 | {' '}
184 |
185 |
186 |
{ setPort(Number(e.target.value)) }}
191 | />
192 |
193 |
196 |
199 | Target Port:
200 | {' '}
201 |
202 |
203 |
{ setTargetPort(Number(e.target.value)) }}
208 | />
209 |
210 | >
211 | )}
212 |
224 |
232 |
233 | >
234 | )
235 | }
236 | export default CRUDMini
237 |
--------------------------------------------------------------------------------
/client/components/CRUD/CRUDModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, type ReactElement } from 'react'
2 | import CrudSelector from './CRUDSelector'
3 | import CRUDMini from './CRUDMini'
4 |
5 | function CRUDModal (): ReactElement {
6 | const [ns, setNs] = useState('')
7 | const [service, setService] = useState('')
8 | const [deployment, setDeployment] = useState('')
9 | const [showModal, setShowModal] = useState(false)
10 | const [crudSelection, setCrudSelection] = useState('namespace')
11 | const [modalPos, setModalPos] = useState(0)
12 | const [modalType, setModalType] = useState('')
13 |
14 | return (
15 |
16 | {showModal &&
17 | (
18 |
27 | )}
28 |
Cluster Editor
29 |
30 |
42 | {ns !== '' &&
43 | (
44 |
57 | )}
58 | {(crudSelection !== 'namespace' && ns !== '') &&
59 | (
60 |
73 | )}
74 |
75 | )
76 | }
77 | export default CRUDModal
78 |
--------------------------------------------------------------------------------
/client/components/CRUD/CRUDSelector.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, type SyntheticEvent, type ReactElement } from 'react'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import { GlobalContext } from '../Contexts'
4 | import { type CLusterObj, type SelectorProps } from '../../../types/types'
5 | import { capitalize } from '../helperFunctions'
6 | import edit from '../../assets/images/edit.png'
7 |
8 | function CrudSelector (
9 | {
10 | type,
11 | state,
12 | stateSetter,
13 | showModal,
14 | setShowModal,
15 | modalPos,
16 | setModalPos,
17 | modalType,
18 | setModalType,
19 | crudSelection,
20 | setCrudSelection,
21 | ns
22 | }: SelectorProps
23 | ): ReactElement {
24 | const { globalClusterData } = useContext(GlobalContext)
25 |
26 | const openModal = (e: SyntheticEvent): void => {
27 | setModalPos(e.currentTarget.getBoundingClientRect().bottom + 5)
28 | showModal ? setShowModal(false) : setShowModal(true)
29 | }
30 | if (type === 'scope') {
31 | return (
32 | <>
33 |
36 | Select Scope
37 |
38 |
39 |
42 |
69 |
70 | >
71 | )
72 | }
73 | return (
74 | <>
75 |
76 | Edit
77 | {' '}
78 | {capitalize(type)}
79 | s
80 |
81 |
82 |
107 |
115 |
123 |
131 |
132 | >
133 | )
134 | }
135 |
136 | export default CrudSelector
137 |
--------------------------------------------------------------------------------
/client/components/Contexts.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, type Dispatch, type SetStateAction } from 'react'
2 | import { type globalServiceObj, type ClusterData } from '../../types/types'
3 |
4 | export interface GlobalContextInterace {
5 | globalServices?: globalServiceObj[]
6 | setGlobalServices?: Dispatch>
7 |
8 | globalTimer?: number
9 | setGlobalTimer?: Dispatch>
10 |
11 | globalServiceTest?: string
12 | setGlobalServiceTest?: Dispatch>
13 |
14 | showEditModal?: boolean
15 | setShowEditModal?: Dispatch>
16 |
17 | globalClusterData?: ClusterData
18 | setGlobalClusterData?: Dispatch>
19 |
20 | globalCrudChange?: boolean
21 | setGlobalCrudChange?: Dispatch>
22 |
23 | ongoingCrudChange?: boolean
24 | setOngoingCrudChange?: Dispatch>
25 |
26 | globalClusterContext?: string
27 | setGlobalClusterContext?: Dispatch>
28 | }
29 |
30 | export const GlobalContext = createContext({})
31 |
--------------------------------------------------------------------------------
/client/components/Graphs/BarGraph.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import React, { type ReactElement } from 'react'
3 | import { Bar } from 'react-chartjs-2'
4 | import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'
5 |
6 | ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
7 |
8 | function BarGraph (): ReactElement {
9 | const labels = [1, 2, 3, 4, 5, 6, 7] // x Axis values
10 | const data = {
11 | labels,
12 | datasets: [{
13 | label: 'My First Dataset',
14 | data: [65, 59, 80, 81, 56, 55, 40], // y Axis Values
15 | backgroundColor: [
16 | 'rgba(255, 99, 132, 0.2)', // color inside bar
17 | 'rgba(255, 159, 64, 0.2)',
18 | 'rgba(255, 205, 86, 0.2)',
19 | 'rgba(75, 192, 192, 0.2)',
20 | 'rgba(54, 162, 235, 0.2)',
21 | 'rgba(153, 102, 255, 0.2)',
22 | 'rgba(201, 203, 207, 0.2)'
23 | ],
24 | borderColor: [
25 | 'rgb(255, 99, 132)', // color border
26 | 'rgb(255, 159, 64)',
27 | 'rgb(255, 205, 86)',
28 | 'rgb(75, 192, 192)',
29 | 'rgb(54, 162, 235)',
30 | 'rgb(153, 102, 255)',
31 | 'rgb(201, 203, 207)'
32 | ],
33 | borderWidth: 1
34 | }]
35 | }
36 |
37 | return (
38 |
39 | )
40 | }
41 |
42 | export default BarGraph
43 |
--------------------------------------------------------------------------------
/client/components/Graphs/GaugeChart.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import React, { useState, useEffect, useContext, type ReactElement } from 'react'
3 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
4 | import { Doughnut } from 'react-chartjs-2'
5 | import { humanReadable } from '../helperFunctions'
6 | import type { GaugeGraphProps, globalServiceObj } from '../../../types/types'
7 | import { GlobalContext } from '../Contexts'
8 |
9 | ChartJS.register(ArcElement, Tooltip, Legend)
10 |
11 | const numberArr: number[] = []
12 | const stringArr: string[] = []
13 | function GaugeChart (
14 | {
15 | type,
16 | borderColor,
17 | backgroundColor,
18 | title,
19 | graphTextColor
20 | }: GaugeGraphProps
21 | ): ReactElement {
22 | const { globalServices } = useContext(GlobalContext)
23 | const [guageData, setGuageData] = useState(numberArr)
24 | const [guageName, setGuageName] = useState(stringArr)
25 | let svcArr: globalServiceObj[] | undefined
26 |
27 | const getData = async (): Promise => {
28 | try {
29 | const localSvc = localStorage.getItem('serviceArr')
30 | if (localSvc !== null) svcArr = await JSON.parse(localSvc)
31 | else svcArr = globalServices
32 | const ep = svcArr?.find(({ name }) => name.slice(0, 17) === 'prometheus-server')?.ip ?? ''
33 | const URL = `/api/prom/${type === 'mem' ? 'mem' : 'cpu'}?type=${type}&hour=24¬Time=true&ep=${ep}`
34 | const response = await fetch(URL)
35 | const data = await response.json()
36 | if (data[0] === undefined) { setGuageData([0]); return }
37 | const dataArr: number[] = []
38 | const dataNames: string[] = []
39 | data.forEach((el: any) => {
40 | const key = Object.keys(el)[0]
41 | dataArr.push(el[key])
42 | dataNames.push(humanReadable(key))
43 | })
44 | setGuageData(dataArr)
45 | setGuageName(dataNames)
46 | } catch (error) {
47 | console.log('Error fetching data:', error)
48 | }
49 | }
50 | useEffect(() => {
51 | void getData()
52 | }, [])
53 |
54 | const data = {
55 | labels: guageName,
56 | datasets: [{
57 | label: 'Percentage',
58 | data: guageData,
59 | backgroundColor,
60 | borderColor,
61 | circumference: 180,
62 | rotation: 270,
63 | cutout: '60%'
64 | }]
65 | }
66 |
67 | const options = {
68 | // aspectRatioL:2,
69 | responsive: true,
70 | maintainAspectRatio: false,
71 | plugins: {
72 | legend: {
73 | labels: {
74 | color: graphTextColor
75 | },
76 | display: true
77 | },
78 | title: {
79 | color: graphTextColor,
80 | display: true,
81 | text: title
82 | }
83 | }
84 | }
85 |
86 | return (
87 |
88 |
89 |
90 | )
91 | }
92 |
93 | export default GaugeChart
94 |
--------------------------------------------------------------------------------
/client/components/Graphs/LineGraph.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import React, { useState, useEffect, useContext, type ReactElement } from 'react'
3 | import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, type ChartOptions, Filler } from 'chart.js'
4 | import { Line } from 'react-chartjs-2'
5 | import { v4 as uuidv4 } from 'uuid'
6 | import type { CLusterObj, LineGraphProps, globalServiceObj } from '../../../types/types'
7 | import { GlobalContext } from '../Contexts'
8 |
9 | ChartJS.register(
10 | CategoryScale,
11 | LinearScale,
12 | PointElement,
13 | LineElement,
14 | Title,
15 | Tooltip,
16 | Legend,
17 | Filler
18 | )
19 | const defaultArr: number[] = []
20 | const stringArr: string[] = []
21 |
22 | function LineGraph (
23 | {
24 | type,
25 | title,
26 | yAxisTitle,
27 | color,
28 | graphTextColor
29 | }: LineGraphProps
30 | ): ReactElement {
31 | const { globalClusterData, globalServices } = useContext(GlobalContext)
32 | const [data, setData] = useState(defaultArr)
33 | const [label, setLabel] = useState(stringArr)
34 | const [hourSelection, setHourSelection] = useState('24')
35 | const [scope, setScope] = useState('')
36 | const [scopeType, setScopeType] = useState('')
37 |
38 | const getData = async (): Promise => {
39 | const time: number[] = []
40 | const specificData: number[] = []
41 | const gigaBytes: number[] = []
42 | const kiloBytes: number[] = []
43 | let svcArr: globalServiceObj[] | undefined
44 |
45 | try {
46 | const localSvc = localStorage.getItem('serviceArr')
47 | if (localSvc !== null) svcArr = await JSON.parse(localSvc)
48 | else svcArr = globalServices
49 | const ep = svcArr?.find(({ name }) => name.slice(0, 17) === 'prometheus-server')?.ip ?? ''
50 | const response = await fetch(`/api/prom/metrics?ep=${ep}&type=${type}&hour=${hourSelection}${scope !== '' ? `&scope=${scopeType}&name=${scope}` : ''}`, {
51 | method: 'GET',
52 | headers: {
53 | 'Content-Type': 'application/json'
54 | }
55 | })
56 | const newData = await response.json()
57 | if (newData[0] === undefined) {
58 | setData([])
59 | return
60 | }
61 | newData[0].values.forEach((el: [number, string]) => {
62 | time.push(el[0])
63 | specificData.push(Number(el[1]))
64 | })
65 | // convert bytes into gigabytes if asking for mem
66 | if (type === 'mem') {
67 | specificData.forEach((el: any) => {
68 | const newEl = el / 1073741824
69 | gigaBytes.push(newEl)
70 | setData(gigaBytes)
71 | })
72 | } else if (type === 'trans' || type === 'rec') { // convert bytes into kilobytes if asking for trans or rec
73 | specificData.forEach((el: any) => {
74 | const newEl = el / 1000
75 | kiloBytes.push(newEl)
76 | setData(kiloBytes)
77 | })
78 | } else {
79 | setData(specificData)
80 | }
81 | // set Unix time to Hours and Minutes
82 | const timeLabels: string[] = []
83 | time.forEach((el: any) => {
84 | const date: Date = new Date(1000 * el) // Convert seconds to milliseconds for Date Function
85 | const formattedTime = `${date.getHours()}:${date.getMinutes()}`
86 | timeLabels.push(formattedTime)
87 | setLabel(timeLabels)
88 | })
89 | } catch (error) {
90 | console.log('Error fetching data:', error)
91 | }
92 | }
93 |
94 | // ? add needed watchers to useEffect
95 | useEffect(() => {
96 | void getData()
97 | }, [hourSelection, scope])
98 |
99 | const dataSet = { // Data for tables
100 | labels: label, // set data for X Axis
101 | datasets: [{
102 | label: title,
103 | data, // set data for Y Axis
104 | fill: true,
105 | // backgroundColor: color, //looks better without fill
106 | borderColor: color,
107 | pointBorderColor: color,
108 | tension: 0.5,
109 | pointBackgroundColor: 'white',
110 | pointBorderWidth: 1,
111 | pointHoverRadius: 4,
112 | pointRadius: 1,
113 | borderWidth: 1.5
114 | }]
115 | }
116 | const options: ChartOptions<'line'> = {
117 | animations: {
118 | tension: {
119 | duration: 1000,
120 | easing: 'linear',
121 | from: 0.1,
122 | to: 0,
123 | loop: true
124 | }
125 | },
126 | plugins: {
127 | legend: {
128 | labels: {
129 | color: graphTextColor
130 | },
131 | display: true
132 |
133 | }
134 | },
135 | responsive: true,
136 | scales: {
137 | y: {
138 | ticks: {
139 | color: graphTextColor
140 | },
141 | grid: {
142 | display: true,
143 | color: 'rgba(128, 128, 128, 0.1)'
144 | },
145 | display: true,
146 | title: {
147 | display: true,
148 | text: yAxisTitle,
149 | color: graphTextColor
150 | }
151 | },
152 | x: {
153 | ticks: {
154 | color: graphTextColor
155 | },
156 | grid: {
157 | display: false,
158 | color: 'rgba(128, 128, 128, 0.1)'
159 | },
160 | display: true,
161 | title: {
162 | display: true,
163 | text: `Time(${hourSelection}hrs)`,
164 | color: 'rgba(255, 255, 255, 0.702)'
165 | }
166 | }
167 | }
168 | }
169 | return (
170 |
171 |
172 |
178 |
187 |
188 |
189 |
190 | )
191 | }
192 |
193 | export default LineGraph
194 |
--------------------------------------------------------------------------------
/client/components/Logs.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-shadow */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | /* eslint-disable jsx-a11y/no-static-element-interactions */
4 | import React, { useState, useEffect, useContext, type ReactElement } from 'react'
5 | import { v4 as uuidv4 } from 'uuid'
6 | import type { LogProps, LogEntry, globalServiceObj } from '../../types/types'
7 | import { GlobalContext } from './Contexts'
8 |
9 | function Logs ({ namespace, logType, pod }: LogProps): ReactElement {
10 | const [data, setData] = useState([])
11 | const [expandedLog, setExpandedLog] = useState(null)
12 | const { globalServices } = useContext(GlobalContext)
13 | let svcArr: globalServiceObj[] | undefined
14 |
15 | const getLogs = async (): Promise => {
16 | try {
17 | const localSvc = localStorage.getItem('serviceArr')
18 | if (localSvc !== null) svcArr = await JSON.parse(localSvc)
19 | else svcArr = globalServices
20 | const ep = svcArr?.find(({ name }) => name.slice(0, 12) === 'loki-gateway')?.ip
21 | const url = `/api/loki/logs?namespace=${namespace}&log=${logType}&pod=${pod}&ep=${ep ?? ''}`
22 | const response = await fetch(url)
23 | const newData = (await response.json()).data.result
24 |
25 | setData(newData)
26 | } catch (error) {
27 | console.log('Error fetching logs:', error)
28 | }
29 | }
30 | const handleLogClick = (log: LogEntry): void => {
31 | expandedLog === log ? setExpandedLog(null) : setExpandedLog(log)
32 | }
33 | useEffect(() => {
34 | void getLogs()
35 | }, [namespace, logType, pod])
36 | return (
37 |
38 | {data?.map((object: any, log) => {
39 | const { stream, values } = object
40 | const { namespace, container, job } = stream
41 | return (
42 |
43 | {values.map((value: any) => {
44 | const i: number = value[1].indexOf('ts=')
45 | const t = value[1].substring(i + 3, i + 24)
46 | return (
47 |
48 |
{ handleLogClick(value[0]) }}>
49 |
50 | Container:
51 | {' '}
52 | {container}
53 |
54 |
55 | Log:
56 | {' '}
57 | {value[1]}
58 |
59 |
60 | {expandedLog === value[0] && (
61 |
62 |
63 | Time:
64 | {' '}
65 | {t}
66 |
67 |
68 | Namespace:
69 | {' '}
70 | {namespace}
71 |
72 |
73 | Container:
74 | {' '}
75 | {container}
76 |
77 |
78 | Job:
79 | {' '}
80 | {job}
81 |
82 |
83 | )}
84 |
85 | )
86 | })}
87 |
88 | )
89 | })}
90 |
91 | )
92 | }
93 |
94 | export default Logs
95 |
--------------------------------------------------------------------------------
/client/components/Mapothy.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | /* eslint-disable no-shadow */
3 | import React, { useEffect, useState, useContext, type ReactElement } from 'react'
4 | import Graph from 'react-graph-vis'
5 | import { v4 as uuidv4 } from 'uuid'
6 | import { GlobalContext, type GlobalContextInterace } from './Contexts'
7 | import { type ClusterNode, type ClusterEdge, type clusterGraphData, type Props, type CLusterObj, type ContextObj, type globalServiceObj } from '../../types/types'
8 | import { makeModal, windowHelper } from './helperFunctions'
9 | import nsImg from '../assets/ns-icon.png'
10 | import podImg from '../assets/pod-icon.png'
11 | import svcImg from '../assets/svc-icon.png'
12 | import depImg from '../assets/dep-icon.png'
13 | import logoImg from '../assets/images/KN-logo.png'
14 |
15 | // ?-----------------------------------------Physics Testing--------------------------------------->
16 | const options = {
17 | layout: {
18 | // randomSeed: '0.07224874827053274:1691128352960',
19 | // randomSeed: '0.13999053405779072:1691128555260'
20 | // randomSeed: '0.26923438127640864:1691128645444'
21 | randomSeed: '0.00836184154624342:1691197042873' // current god seed
22 | // randomSeed: '0.19873095642451144:1691197919546' //current demi god seed
23 | },
24 | edges: {
25 | color: '#00FFFF'
26 | },
27 | interaction: {
28 | hover: true
29 | },
30 | autoResize: true,
31 | physics: {
32 | barnesHut: {
33 | gravitationalConstant: -1000,
34 | centralGravity: 0,
35 | springLength: 150,
36 | springConstant: 0.003,
37 | damping: 0.09,
38 | avoidOverlap: 0.2
39 | }
40 | }
41 | }
42 | // ?-------------------------------------Map Component--------------------------------------------->
43 | function Mapothy ({ header }: Props): ReactElement {
44 | const {
45 | setGlobalServices, globalServices,
46 | setGlobalClusterData, globalClusterData,
47 | setGlobalCrudChange, globalCrudChange,
48 | globalClusterContext, setGlobalClusterContext
49 | } = useContext(GlobalContext)
50 | const [graph, setGraph] = useState({
51 | nodes: [],
52 | edges: []
53 | })
54 | const [ns, setNs] = useState('Cluster')
55 | const [clusterContext, setClusterContext] = useState(globalClusterContext)
56 | const getData = async (): Promise => {
57 | try {
58 | // arrays used to store nodes and edges for use in react-graph-vis
59 | const nodesArr: ClusterNode[] = []
60 | const edgesArr: ClusterEdge[] = []
61 | const serviceArrTemp: globalServiceObj[] = []
62 | // temporary arrray used for filtering cluster view by namespace
63 | let filteredNsArr: CLusterObj[] = []
64 | // check to see if namespace array needs to be updated
65 | let newContext = false
66 | let data
67 | // conditionals to prevent an unnecessary fetching
68 | if (globalClusterData?.namespaces === undefined ||
69 | globalCrudChange === true ||
70 | globalClusterContext !== clusterContext) {
71 | // fetch made using the current context, checks used for base case and persistent context
72 | // TODO idk if this works anymore
73 | const result = await fetch(`api/map/elements?context=${clusterContext ?? ''}`)
74 | data = await result.json()
75 | // swithces flipped and state assigned
76 | // TODO all conditionals have been changed
77 | if (setGlobalCrudChange !== undefined) setGlobalCrudChange(false)
78 | if (setGlobalClusterData !== undefined) setGlobalClusterData(data)
79 | if (setGlobalClusterContext !== undefined && clusterContext !== undefined) {
80 | setGlobalClusterContext(clusterContext)
81 | }
82 | newContext = true
83 | } else { data = globalClusterData }
84 | const { pods, namespaces, deployments, services, currentContext } = data
85 | // base case for context
86 | if (globalClusterContext === '' && setGlobalClusterContext !== undefined) {
87 | setGlobalClusterContext(currentContext)
88 | }
89 | if (clusterContext === '') setClusterContext(currentContext)
90 | // center node assigned to current context, all namespaces will be connected to this node
91 | nodesArr.push({ id: '0', title: `${currentContext as string}`, size: 100, image: logoImg, shape: 'image' })
92 | // checks to filter cluste view if a namespace has been selected
93 | if (ns === 'Cluster') filteredNsArr = namespaces
94 | else { filteredNsArr = [namespaces.find(({ name }: any) => name === ns)] }
95 | // ?----------------------------------Namespace Search------------------------------------->
96 | // namespace array iterated over, with all other resources being attached accordingly
97 | filteredNsArr.forEach((nS: CLusterObj) => {
98 | // if (nS.name === 'loki') return
99 | const { name, uid } = nS
100 | const nsObj = {
101 | id: uid,
102 | name,
103 | title: makeModal(nS, 'Namespace'), // modals created using helper function
104 | size: 70,
105 | image: nsImg,
106 | shape: 'image'
107 | }
108 | // each node object is pushed to nodes array
109 | nodesArr.push(nsObj)
110 | // edges drawn from namespace to center (context) node
111 | edgesArr.push({ from: '0', to: uid, length: 400 })
112 | // ?------------------------------Pod Search------------------------------------------>
113 | pods.forEach((pod: CLusterObj) => {
114 | const { namespace, uid } = pod
115 | if (namespace === nsObj.name) {
116 | const pObj = {
117 | id: uid,
118 | title: makeModal(pod, 'Pod'),
119 | size: 45,
120 | image: podImg,
121 | shape: 'image'
122 | }
123 | // same as above but edges are drawn from current node to corresponding namespace node
124 | nodesArr.push(pObj)
125 | edgesArr.push({ from: nsObj.id, to: uid, length: 200 })
126 | }
127 | })
128 | // ?------------------------------Services Search------------------------------------------>
129 | services.forEach((service: CLusterObj) => {
130 | const { name, namespace, uid, ingressIP, ports } = service
131 | if (namespace === nsObj.name) {
132 | const sObj = {
133 | id: uid,
134 | title: makeModal(service, 'Service'),
135 | size: 45,
136 | image: svcImg,
137 | shape: 'image'
138 | }
139 | // pertinent data regarding exposed ingress ports is extracted
140 | if (ingressIP !== undefined) serviceArrTemp.push({ name, ip: `${ingressIP as string}:${(ports != null) ? ports[0].port : ''}` })
141 | nodesArr.push(sObj)
142 | edgesArr.push({ from: nsObj.id, to: uid, length: 300 })
143 | }
144 | })
145 | // ?------------------------------Deployments Search--------------------------------------->
146 | deployments.forEach((deployment: CLusterObj) => {
147 | const { namespace, uid } = deployment
148 | if (namespace === nsObj.name) {
149 | const dObj = {
150 | id: uid,
151 | title: makeModal(deployment, 'Deployment'),
152 | image: depImg,
153 | size: 45,
154 | shape: 'image'
155 | }
156 | nodesArr.push(dObj)
157 | edgesArr.push({ from: nsObj.id, to: uid, length: 150 })
158 | }
159 | })
160 | // ?------------------------------Ingress Search------------------------------------------>
161 | //! currently no ingresses
162 | // ingresses.forEach((ingress: ClusterObj) => {
163 | // const { name, namespace, uid } = ingress;
164 | // if (namespace === nsObj.name) {
165 | // const iObj = {
166 | // id: uid,
167 | // title: makeModal(ingress, 'Deployment'),
168 | // image: depImg,
169 | // size: 45,
170 | // shape: 'image',
171 | // }
172 | // nodesArr.push(iObj);
173 | // edgesArr.push({ from: nsObj.id, to: uid, length: 150 });
174 | // }
175 | // })
176 | })
177 | if (globalServices?.length === 0 || newContext) {
178 | if (setGlobalServices !== undefined) setGlobalServices(serviceArrTemp)
179 | localStorage.setItem('serviceArr', JSON.stringify(serviceArrTemp))
180 | }
181 | // graph created using node and edges array created above
182 | setGraph({
183 | nodes: nodesArr,
184 | edges: edgesArr
185 | })
186 | } catch (error) {
187 | console.log(error)
188 | }
189 | }
190 | // use effect controlling data fetch and map creation set to watch pertinent data changes
191 | useEffect(() => {
192 | void getData()
193 | }, [ns, clusterContext, globalCrudChange])
194 | // TODO fix error so we dont have to ignore it // error is benign
195 | useEffect(() => {
196 | windowHelper()
197 | }, [])
198 | return (
199 | <>
200 |
201 | {header}
202 |
203 |
204 |
213 |
221 |
222 |
223 | {
227 | console.log(network.getSeed())
228 | setTimeout(
229 | () => {
230 | network.fit({
231 | animation: {
232 | duration: 1000,
233 | easingFunction: 'easeOutQuad'
234 | }
235 | })
236 | },
237 | 3000
238 | )
239 | }}
240 | />
241 |
242 | >
243 | )
244 | }
245 | export default Mapothy
246 |
--------------------------------------------------------------------------------
/client/components/helperFunctions.ts:
--------------------------------------------------------------------------------
1 | import { type CLusterObj, type nestedObj } from '../../types/types'
2 |
3 | // ~----------------------------------------Modal-------------------------------------------------->
4 | export const makeModal = (obj: CLusterObj, type: string): string => {
5 | // create necessary dom elements
6 | const subLists: HTMLElement[] = []
7 | const div = document.createElement('div'); div.className = 'modal'
8 | const ul = document.createElement('ul')
9 | const header = document.createElement('div')
10 | header.className = 'modalHeader'
11 | // header assigned given type (Namespace, Service, etc.)
12 | header.innerText = `${type} Details`
13 | // all items in given object to be iteraeted over
14 | Object.keys(obj).forEach((key: any) => {
15 | // filtering for private data (can be turned off in private settings)
16 | if (key === 'uid' || key === 'ingressIP') return
17 | // arbitrarily nested objects dealt with using another helper function
18 | if (typeof obj[key] === 'object') subLists.push(recursiveList(obj[key], key))
19 | else {
20 | const li = document.createElement('li')
21 | li.innerText = `${humanReadable(key)}: ${obj[key] as string}`
22 | ul.appendChild(li)
23 | }
24 | })
25 | // elements appended in order
26 | div.appendChild(header)
27 | div.appendChild(ul)
28 | subLists.forEach((el) => {
29 | div.appendChild(el)
30 | })
31 | // dom elements manipulated to match typing expectations for react-graph-vis tooltip
32 | return div as unknown as string
33 | }
34 | // ?recursively iterate nested objects to create sub lists
35 | const recursiveList = (obj: nestedObj, key?: any): HTMLElement => {
36 | const subList = document.createElement('ul')
37 | const smallHeader = document.createElement('div')
38 | smallHeader.className = 'modalHeader'
39 | if (key !== undefined) smallHeader.innerText = `${humanReadable(key)}`
40 | // check for specifically nested obj (ports obj) to ensure consistent prints (edge case)
41 | if (Array.isArray(obj) && obj.length === 1 && typeof obj[0] !== 'object') {
42 | const singleLi = document.createElement('li')
43 | singleLi.innerText = `${humanReadable(key)}: ${obj[0] as string}`
44 | subList.appendChild(singleLi)
45 | return subList
46 | }
47 | // header to be appended first, no header for the above case
48 | subList.appendChild(smallHeader)
49 | Object.keys(obj).forEach((k: any) => { // fix typing here
50 | const li = document.createElement('li')
51 | // check for further nested objects
52 | if (typeof obj[k] === 'object') subList.appendChild(recursiveList(obj[k]))
53 | else {
54 | // check to ensure keys are not printed for array elements
55 | Array.isArray(obj) ? li.innerText = `${obj[k] as string}` : li.innerText = `${humanReadable(k)}: ${obj[k] as string}`
56 | subList.appendChild(li)
57 | }
58 | })
59 | return subList
60 | }
61 | // ?helper function to make keys human readable (camel case)
62 | export const humanReadable = (name: string): string => {
63 | const words = name.match(/[A-Za-z][a-z]*/g)
64 | return words?.map(capitalize).join(' ') ?? ''
65 | }
66 | export const capitalize = (word: string): string => word.charAt(0).toUpperCase() + word.substring(1)
67 |
68 | // ~---------------------------------------Window Helper------------------------------------------->
69 | // helper function to prevent benign error message created when viewing larger clusters
70 | export const windowHelper = (): void => {
71 | window.addEventListener('error', (e) => {
72 | console.log(e.message)
73 | if (e.message === 'ResizeObserver loop completed with undelivered notifications.') {
74 | const resizeObserverErrDiv = document.getElementById(
75 | 'webpack-dev-server-client-overlay-div'
76 | )
77 | const resizeObserverErr = document.getElementById(
78 | 'webpack-dev-server-client-overlay'
79 | )
80 | if (resizeObserverErr != null) {
81 | resizeObserverErr.setAttribute('style', 'display: none')
82 | }
83 | if (resizeObserverErrDiv != null) {
84 | resizeObserverErrDiv.setAttribute('style', 'display: none')
85 | }
86 | }
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/client/containers/InvisibleNavbar/InvisibleNavbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, type SyntheticEvent, type ReactElement } from 'react'
2 | import InvisibleNavbarModal from './InvisibleNavbarModal'
3 |
4 | function InvisibleNavbar (): ReactElement {
5 | const [showModal, setShowModal] = useState(false)
6 | const [modalPos, setModalPos] = useState(0)
7 |
8 | const openModal = (e: SyntheticEvent): void => {
9 | setModalPos(e.currentTarget.getBoundingClientRect().bottom + 5)
10 | showModal ? setShowModal(false) : setShowModal(true)
11 | }
12 | return (
13 | <>
14 |
15 | {showModal
16 | ? (
17 |
21 | )
22 | : }
23 | >
24 |
25 | )
26 | }
27 |
28 | export default InvisibleNavbar
29 |
--------------------------------------------------------------------------------
/client/containers/InvisibleNavbar/InvisibleNavbarModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, type ReactElement } from 'react'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import type { InvisibleNavbarModalProps } from '../../../types/types'
4 | import { GlobalContext } from '../../components/Contexts'
5 |
6 | function InvisibleNavbarModal (
7 | {
8 | style,
9 | setShowModal
10 | }: InvisibleNavbarModalProps
11 | ): ReactElement {
12 | const {
13 | globalServices,
14 | globalTimer, setGlobalTimer,
15 | setGlobalServiceTest
16 | } = useContext(GlobalContext)
17 | const [vU, setVu] = useState(0)
18 | const [duration, setDuration] = useState(0)
19 | const [service, setService] = useState('')
20 |
21 | const loadtest = async (): Promise => {
22 | if (vU < 0 || duration < 0) { alert('Please choose positive numbers'); return }
23 | if (vU === 0 || duration === 0) { alert('Please fill out both fields'); return }
24 | if (globalTimer !== 0) { alert('Load test is currently running, please wait'); return }
25 | try {
26 | const response = await fetch(`/api/k6/test?vus=${vU}&duration=${duration}&ip=${service}`)
27 | const data = response.json()
28 | console.log(data)
29 | setShowModal(false)
30 | const filtered = globalServices?.find((gService) => gService?.ip === service)
31 | if (setGlobalServiceTest !== undefined) setGlobalServiceTest(filtered?.name ?? '')
32 | if (setGlobalTimer !== undefined) setGlobalTimer((Date.now() + (duration * 1000)))
33 | } catch (error) {
34 | console.log('error in running load test:', error)
35 | }
36 | }
37 | return (
38 | <>
39 |
42 |
46 |
47 |
59 |
60 |
{ setVu(Number(e.target.value)) }}
65 | />
66 |
{ setDuration(Number(e.target.value)) }}
71 | />
72 |
80 |
88 |
89 | >
90 | )
91 | }
92 |
93 | export default InvisibleNavbarModal
94 |
--------------------------------------------------------------------------------
/client/containers/LogsContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext, type ReactElement } from 'react'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import { GlobalContext } from '../components/Contexts'
4 | import type { CLusterObj, Props } from '../../types/types'
5 | import Logs from '../components/Logs'
6 |
7 | const types = ['error', 'info']
8 |
9 | function LogsContainer ({ header }: Props): ReactElement {
10 | const [ns, setNs] = useState('default')
11 | const [nsArr, setNsArr] = useState([])
12 | const [pod, setPod] = useState('')
13 | const [podArr, setPodArr] = useState([])
14 | const [logType, setLogType] = useState('')
15 | const { globalClusterContext, globalClusterData } = useContext(GlobalContext)
16 | const getNs = async (): Promise => {
17 | try {
18 | const tempPodArr = globalClusterData?.pods
19 | ?.filter(({ namespace }: CLusterObj) => namespace === ns)
20 | .map(({ name }: CLusterObj) => name)
21 | const url = `/api/cluster/namespaces?context=${globalClusterContext ?? ''}&all=true`
22 | const response = await fetch(url)
23 | const data = (await response.json())
24 | const arr: string[] = data.map(({ name }: CLusterObj) => name)
25 | setPodArr(tempPodArr)
26 | setNsArr(arr)
27 | setPod('')
28 | } catch (error) {
29 | console.log('Error fetching logs namespaces:', error)
30 | }
31 | }
32 | useEffect(() => {
33 | void getNs()
34 | }, [ns])
35 | return (
36 | <>
37 | {header}
38 |
39 |
47 | {(ns !== '') && (
48 |
56 | )}
57 |
65 |
66 |
67 |
68 |
69 | >
70 | )
71 | }
72 |
73 | export default LogsContainer
74 |
--------------------------------------------------------------------------------
/client/containers/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ReactElement, useContext } from 'react'
2 | import { Route, Routes } from 'react-router-dom'
3 | import MainDashBoard from './MainDashBoard'
4 | import Mapothy from '../components/Mapothy'
5 | import { GlobalContext } from '../components/Contexts'
6 | import NetworkPerformance from './NetworkPerformance'
7 | import CRUDModal from '../components/CRUD/CRUDModal'
8 | import LogsContainer from './LogsContainer'
9 |
10 | function MainContainer (): ReactElement {
11 | const {
12 | showEditModal
13 | } = useContext(GlobalContext)
14 |
15 | return (
16 | <>
17 | {(showEditModal === true) && }
18 |
19 |
20 | } />
21 | } />
22 | } />
23 | } />
24 |
25 |
26 |
27 | >
28 | )
29 | }
30 |
31 | export default MainContainer
32 |
--------------------------------------------------------------------------------
/client/containers/MainDashBoard.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ReactElement } from 'react'
2 | import LineGraph from '../components/Graphs/LineGraph'
3 | import GaugeChart from '../components/Graphs/GaugeChart'
4 | import type { Props } from '../../types/types'
5 | import InvisibleNavbar from './InvisibleNavbar/InvisibleNavbar'
6 |
7 | function MainDashBoard ({ header }: Props): ReactElement {
8 | const textColor = 'rgba(255, 255, 255, 0.702)'
9 | return (
10 | <>
11 | {header}
12 |
13 |
14 |
21 |
28 |
29 |
30 |
37 |
44 |
45 | >
46 |
47 | )
48 | }
49 |
50 | export default MainDashBoard
51 |
--------------------------------------------------------------------------------
/client/containers/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ReactElement, useContext, useEffect, useState } from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import clusterpic from '../assets/images/clusterPic.png'
4 | import mainDashBoard from '../assets/images/mainDashBoard.png'
5 | import netWork from '../assets/images/network.png'
6 | import edit from '../assets/images/edit.png'
7 | import docs from '../assets/images/docs.png'
8 | import { GlobalContext } from '../components/Contexts'
9 | import gif2 from '../assets/gif3.gif'
10 |
11 | export default function Navbar (): ReactElement {
12 | const {
13 | globalTimer, setGlobalTimer,
14 | globalServiceTest, setGlobalServiceTest,
15 | showEditModal, setShowEditModal,
16 | ongoingCrudChange
17 | } = useContext(GlobalContext)
18 | const [activeTimer, setActiveTimer] = useState(0) //! smooth brain solution
19 | const navigate = useNavigate()
20 | function MainDashBoard (): void { navigate('/dashboard') }
21 | function GoHome (): void { navigate('/') }
22 | function Network (): void { navigate('/network') }
23 | function Logs (): void { navigate('/logs') }
24 |
25 | useEffect(() => {
26 | if ((globalTimer !== undefined) && (globalTimer - Date.now()) < 0) {
27 | if (setGlobalTimer !== undefined) setGlobalTimer(0)
28 | if (setGlobalServiceTest !== undefined) setGlobalServiceTest('')
29 | } else if ((globalTimer !== undefined) && (globalTimer - Date.now()) > 0) {
30 | setTimeout(() => {
31 | setActiveTimer(Math.floor((globalTimer - Date.now()) / 1000))
32 | }, 1000)
33 | }
34 | }, [globalTimer, activeTimer, ongoingCrudChange])
35 |
36 | return (
37 |
38 |
KUBERNAUTICAL
39 |
40 |
44 |
45 |
49 |
50 |
54 |
55 |
59 |
60 |
73 |
74 |
75 | {globalTimer !== 0 && globalTimer !== undefined
76 | ? `Load test time remaining:
77 | ${Math.floor((globalTimer - Date.now()) / 1000)}s, running on
78 | ${globalServiceTest as string}`
79 | : null}
80 |
81 |
82 | {ongoingCrudChange === true ?

: null}
83 |
84 |
85 |
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/client/containers/NetworkPerformance.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ReactElement } from 'react'
2 | import LineGraph from '../components/Graphs/LineGraph'
3 | import type { Props } from '../../types/types'
4 | import InvisibleNavbar from './InvisibleNavbar/InvisibleNavbar'
5 |
6 | function NetworkPerformance ({ header }: Props): ReactElement {
7 | return (
8 | <>
9 | {header}
10 |
11 |
12 |
13 |
20 |
27 |
28 |
29 | >
30 |
31 | )
32 | }
33 | export default NetworkPerformance
34 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Kubernautical
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 | import { BrowserRouter } from 'react-router-dom';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
7 | const root = createRoot(document.getElementById('root')!);
8 | root.render(
9 |
10 |
11 |
12 | );
--------------------------------------------------------------------------------
/client/sass/App.scss:
--------------------------------------------------------------------------------
1 | //imports
2 | @import '_variables';
3 | @import '_navBar';
4 | @import '_mainContainer';
5 | @import '_graph';
6 | @import '_vis-network';
7 | @import '_map';
8 | @import '_invisNav';
9 | @import '_CRUDModal';
10 | @import '_logs';
11 |
12 |
13 | body {
14 | background-color: $body-background;
15 | color: white;
16 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
17 | }
18 |
19 | .App {
20 | display: flex;
21 | height: 95vh;
22 | }
23 |
24 | .hr {
25 | border: 1px solid white;
26 | width: 90%;
27 | }
28 |
29 | .footer {
30 | display: flex;
31 | flex-direction: column;
32 | align-items: flex-end;
33 | }
34 |
35 |
36 | // large screen
37 | @media screen and (max-width: 1599px) {
38 | .navBar {
39 | display: flex;
40 | flex-direction: column;
41 | align-items: center;
42 | /* height: 90vh;
43 | width: 12vw;
44 | padding: 5px;
45 | margin: 10px;
46 | color: #EEEEEE;
47 | border: 2px solid cyan;
48 | border-radius: 5px; */
49 | font-size: 1.5rem;
50 | }
51 |
52 | .nav-title {
53 | padding-bottom: 10%;
54 | }
55 |
56 | .navButton {
57 | width: 11vw;
58 | margin: 5px;
59 | border-radius: 10px;
60 | padding: 10px 20px;
61 | font-size: 1.2rem;
62 | }
63 |
64 | .miniContainer {
65 | display: flex;
66 | flex-direction: column;
67 | justify-content: space-around;
68 | align-items: center;
69 | }
70 | }
71 |
72 | @media screen and (max-width: 1350px) {
73 | .nav-title {
74 | font-size: 1 rem;
75 | }
76 |
77 | .navButton {
78 | width: 11vw;
79 | font-size: 1.1rem;
80 | }
81 | }
82 |
83 | @media screen and (max-width: 1100px) {
84 | .App {
85 | display: flex;
86 | flex-direction: column;
87 | }
88 |
89 | .navBar {
90 | width: 98.5%;
91 | padding: 10px;
92 | display: flex;
93 | flex-direction: row;
94 | justify-content: flex-start;
95 | }
96 |
97 | .nav-title {
98 | padding-top: 10%;
99 | font-size: 0.8rem;
100 | }
101 |
102 | .navButton {
103 | width: 11vw;
104 | font-size: 1.1rem;
105 | }
106 | }
--------------------------------------------------------------------------------
/client/sass/_CRUDModal.scss:
--------------------------------------------------------------------------------
1 | .editModal {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: start;
5 | align-items: start;
6 | margin: 100% -100px 5px 40px;
7 | padding: 0px 3px 3px 3px;
8 | height: 500px;
9 | width: 125%;
10 | // border: 2px solid magenta;
11 | }
12 |
13 | .centerModal {
14 | display: flex;
15 | flex-direction: column;
16 | // border: 2px solid cyan;
17 | // justify-content: center;
18 | margin: 10px 5px 5px 10px;
19 | }
20 |
21 | .crudDelete {
22 | height: 24px;
23 | width: 24px;
24 | border-radius: 8px;
25 | margin: 5px;
26 | border: none;
27 | background-color: $red-button;
28 | }
29 |
30 | .crudSelector {
31 | display: flex;
32 | justify-content: space-between;
33 | margin-bottom: 7px;
34 | height: 30px;
35 | width: 95%;
36 | }
37 |
38 | .crudHeader {
39 | color: lighten($font-color, 30%);
40 | display: flex;
41 | align-items: end;
42 | // border: 2px solid cyan;
43 | padding: 0px;
44 | margin: 5px 5px 5px 5px;
45 | width: 80%;
46 | height: 8%;
47 | animation: loadin 1s ease-in;
48 | }
49 |
50 | .hrCrud {
51 | border: 1px solid lighten($font-color, 30%);
52 | width: 90%;
53 | }
54 |
55 | .smallEdit {
56 | height: 15px;
57 | width: 15px;
58 | }
59 |
60 | .crudMini {
61 | display: flex;
62 | flex-direction: column;
63 | justify-content: end;
64 | align-items: start;
65 | width: 200px;
66 | min-height: 200px;
67 | margin: 8px;
68 | padding: 5px;
69 | border-radius: 5px;
70 | background-color: lighten($container-background, 10%);
71 | box-shadow: $container-shadow;
72 | z-index: 10;
73 | }
74 |
75 | .portsDiv {
76 | color: lighten($font-color, 30%);
77 | display: flex;
78 | align-items: center;
79 | width: 100%;
80 | }
81 |
82 | .portsEdit {
83 | width: 20%;
84 | justify-self: end;
85 | }
--------------------------------------------------------------------------------
/client/sass/_graph.scss:
--------------------------------------------------------------------------------
1 | .lineGraph {
2 | width: 95%;
3 | height: 95%;
4 | margin: 8px;
5 | padding: 5px 5px 0 5px;
6 | border-radius: 5px;
7 | background-color: $container-background;
8 | box-shadow: $container-shadow;
9 | }
10 |
11 | .guageGraph {
12 | width: 95%;
13 | height: 95%;
14 | margin: 8px;
15 | padding: 5px;
16 | border-radius: 5px;
17 | background-color: $container-background;
18 | box-shadow: $container-shadow;
19 | }
--------------------------------------------------------------------------------
/client/sass/_invisNav.scss:
--------------------------------------------------------------------------------
1 | .invisNavButton {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | width: 100px;
6 | height: 25px;
7 | margin: -8px 0px 5px 0px;
8 | color: $font-color;
9 | border-radius: 5px;
10 | background-color: $container-background;
11 | box-shadow: $container-shadow;
12 | border: 0 solid;
13 | outline: 1px solid;
14 | outline-color: rgba(255, 255, 255, .5);
15 | outline-offset: 0px;
16 | text-shadow: none;
17 | transition: all 1250ms cubic-bezier(0.19, 1, 0.22, 1);
18 | z-index: 9;
19 | }
20 |
21 | .invisNavButton:hover {
22 | border: 1px solid;
23 | box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.1), 0 0 20px rgba(255, 255, 255, .2);
24 | outline-color: rgba(255, 255, 255, 0);
25 | outline-offset: 15px;
26 | }
27 |
28 | .invisModal {
29 | display: flex;
30 | flex-direction: column;
31 | justify-content: center;
32 | align-items: center;
33 | width: 200px;
34 | height: 200px;
35 | margin: 8px;
36 | padding: 5px 5px 0 5px;
37 | border-radius: 5px;
38 | background-color: lighten($container-background, 10%);
39 | box-shadow: $container-shadow;
40 | z-index: 10;
41 | }
42 |
43 | .page-mask {
44 | z-index: 8;
45 | background: rgba(0, 0, 0, 0.5);
46 | position: fixed;
47 | top: 0;
48 | right: 0;
49 | bottom: 0;
50 | left: 0;
51 | }
52 |
53 | .closeInvisModal {
54 | position: absolute;
55 | height: 24px;
56 | width: 24px;
57 | top: 5px;
58 | right: 5px;
59 | border-radius: 8px;
60 | border: none;
61 | background-color: $red-button;
62 | }
63 |
64 | .closeInvisModal:hover,
65 | .InvisSubmit:hover,
66 | .crudDelete:hover {
67 | cursor: pointer;
68 | transform: translate(1px, 1px);
69 | color: white;
70 | }
71 |
72 | .InvisInput {
73 | border-radius: 6px;
74 | border: none;
75 | width: 90%;
76 | padding: 8px;
77 | margin-bottom: 4px;
78 | background-color: rgb(66, 73, 90);
79 | color: rgba(246, 247, 249, 0.8);
80 | }
81 |
82 | .InvisSubmit {
83 | width: 95%;
84 | margin-top: 5px;
85 | border-radius: 6px;
86 | padding: 9px;
87 | border: none;
88 | background-color: $red-button;
89 | }
90 |
91 | .InvisService {
92 | position: absolute;
93 | height: 24px;
94 | width: 50%;
95 | top: 5px;
96 | left: 5px;
97 | border-radius: 8px;
98 | border: none;
99 | background-color: $red-button;
100 | }
--------------------------------------------------------------------------------
/client/sass/_logs.scss:
--------------------------------------------------------------------------------
1 | .log-container {
2 | margin: 10px;
3 | }
4 |
5 | .log-entry {
6 | margin: 10px;
7 | padding: 3px;
8 | // border: 1px solid #d1d0d0;
9 | background-color: #252525;
10 | color: $font-color;
11 | }
12 |
13 | .value-entry {
14 | margin-bottom: 10px;
15 | }
16 |
17 | .clickable {
18 | cursor: pointer;
19 | background-color: lighten($container-background, 10%);
20 | padding: 8px;
21 | border-radius: 4px;
22 |
23 | }
24 |
25 | .clickable:hover {
26 | background-color: lighten($container-background, 5%);
27 | }
28 |
29 | .log-details {
30 | margin-top: 5px;
31 | padding: 5px;
32 | background-color: lighten($container-background, 20%);
33 | border: 1px solid #555555;
34 | }
35 |
36 | .dropdown-container {
37 | display: flex;
38 | }
--------------------------------------------------------------------------------
/client/sass/_mainContainer.scss:
--------------------------------------------------------------------------------
1 | .mainContainer {
2 | // border: 2px solid cyan;
3 | border-radius: 5px;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | flex-grow: 1;
8 | padding: 5px;
9 | margin: 10px;
10 | height: 90vh;
11 |
12 | }
13 |
14 | .mainHeader {
15 | // border: 2px solid pink;
16 | color: lighten($font-color, 30%);
17 | padding: 5px;
18 | margin: 5px;
19 | width: 80%;
20 | height: 15%;
21 | border-radius: 5px;
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | font-size: 50px;
26 | animation: loadin 1s ease-in;
27 | }
28 |
29 | .miniContainerMap {
30 | // border: 2px solid cyan;
31 | border-radius: 5px;
32 | padding: 5px;
33 | width: 80%;
34 | height: 80%;
35 | display: flex;
36 | margin: 8px;
37 | border-radius: 5px;
38 | background-color: $container-background;
39 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
40 | // flex-direction: column;
41 | animation: loadin 1s ease-in;
42 | }
43 |
44 | .miniContainerGraph {
45 | // border: 2px solid cyan;
46 | border-radius: 5px;
47 | padding: 5px;
48 | width: 80%;
49 | height: 40%;
50 | display: flex;
51 | // flex-direction: column;
52 | animation: loadin 1s lienar;
53 | }
54 |
55 | .miniContainerLogs {
56 | border-radius: 5px;
57 | padding: 5px;
58 | width: 80%;
59 | height: 95%;
60 | display: flex;
61 | margin: 8px;
62 | border-radius: 5px;
63 | background-color: $container-background;
64 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
65 | overflow: auto;
66 | // flex-direction: column;
67 | // animation: loadin 1s ease-in;
68 | animation: loadin 1s lienar;
69 | }
70 |
71 |
72 | .containerButton {
73 | // width: 95%;
74 | // height: 45%;
75 | color: $font-color;
76 | margin: 5px 5px 0 5px;
77 | border-radius: 5px;
78 | border: none;
79 | background-color: $body-background;
80 | box-shadow: $container-shadow;
81 | }
82 |
83 | @keyframes loadin {
84 | from {
85 | opacity: 0;
86 | transform: translateX(-10px)
87 | }
88 |
89 | to {
90 | opacity: 1;
91 | transform: translateX(0)
92 | }
93 | }
--------------------------------------------------------------------------------
/client/sass/_map.scss:
--------------------------------------------------------------------------------
1 | ul {
2 | list-style-type: none;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | .modalHeader {
8 | font-size: large;
9 | text-decoration: underline;
10 | font-weight: bold;
11 | }
12 |
13 | li {
14 | padding: 5px;
15 | }
16 |
17 | div.vis-tooltip {
18 | position: absolute;
19 | visibility: hidden;
20 | padding: 5px;
21 | white-space: normal;
22 | font-family: verdana;
23 | font-size: 14px;
24 | color: lighten($font-color, 20%);
25 | background-color: $container-background;
26 | box-shadow: $container-shadow;
27 | margin: 5px 5px 0 5px;
28 | border-radius: 5px;
29 | border: 1px solid rgb(24, 25, 35);
30 | -moz-border-radius: 3px;
31 | -webkit-border-radius: 3px;
32 | border-radius: 3px;
33 | pointer-events: none;
34 | z-index: 5;
35 | }
36 |
37 | .mapButton {
38 | z-index: 3;
39 | background-color: lighten($container-background, 15%);
40 | }
41 |
42 | .mapButton:hover,
43 | .mapButton2:hover {
44 | cursor: pointer;
45 | transform: translate(1px, 1px);
46 | color: lighten($font-color, 20%);
47 | }
48 |
49 | .buttonWrap {
50 | // border: 2px solid cyan;
51 | width: 80%;
52 | padding: 0;
53 | margin: 0;
54 | }
55 |
56 | .buttonLeft {
57 | margin-left: -4px;
58 | }
--------------------------------------------------------------------------------
/client/sass/_navBar.scss:
--------------------------------------------------------------------------------
1 | .navBar {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: flex-start;
5 | height: 95vh;
6 | width: 12vw;
7 | padding: 5px;
8 | margin: 10px 5px 5px 10px;
9 | background: linear-gradient(-45deg, rgb(253, 93, 146), rgb(197, 47, 30), rgb(253, 93, 146), rgb(197, 47, 30));
10 | background-size: 400% 400%;
11 | color: #EEEEEE;
12 | border-radius: 5px;
13 | animation: gradient 5s ease infinite;
14 | }
15 |
16 | @keyframes gradient {
17 | 0% {
18 | background-position: 0% 50%;
19 | }
20 |
21 | 50% {
22 | background-position: 100% 50%;
23 | }
24 |
25 | 100% {
26 | background-position: 0% 50%;
27 | }
28 | }
29 |
30 | // title
31 | .navBarTitle {
32 | align-self: center;
33 | font-size: 1.5rem;
34 | margin: 20px 0 20px 0;
35 | }
36 |
37 | // Button
38 |
39 | .navButton {
40 | width: 100%;
41 | height: 8%;
42 | margin: 5px;
43 | border: none;
44 | font-size: 1rem;
45 | background: transparent;
46 | display: flex;
47 | align-items: center;
48 | color: white
49 | }
50 |
51 | .navButton:hover {
52 | cursor: pointer;
53 | transform: translate(1px, 1px);
54 | color: white;
55 | }
56 |
57 | .btn-icon {
58 | margin-right: 1.2rem;
59 | height: 30px;
60 | width: 30px;
61 | margin-left: 5px;
62 | }
63 |
64 | .btn-text {
65 | text-align: left;
66 | }
67 |
68 | .loadingStatus {
69 | position: relative;
70 | top: 40%;
71 | justify-self: end;
72 | align-self: center;
73 | font-size: small;
74 | }
75 |
76 | .loadingGif {
77 | height: 50px;
78 | width: 50px;
79 | }
--------------------------------------------------------------------------------
/client/sass/_variables.scss:
--------------------------------------------------------------------------------
1 | $font-color: rgba(255, 255, 255, 0.702);
2 | $container-background: rgb(38, 38, 38);
3 | $container-shadow: rgba(28, 28, 28, 0.24) 0px 3px 8px;
4 | $red-button: rgb(225, 70, 89);
5 | $body-background: rgb(25, 24, 24);
--------------------------------------------------------------------------------
/client/sass/_vis-network.scss:
--------------------------------------------------------------------------------
1 | .vis .overlay {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 | z-index: 10;
8 | }
9 |
10 | .vis-active {
11 | box-shadow: 0 0 10px #86d5f8;
12 | }
13 |
14 | .vis [class*="span"] {
15 | min-height: 0;
16 | width: auto;
17 | }
18 |
19 | div.vis-configuration {
20 | position: relative;
21 | display: block;
22 | float: left;
23 | font-size: 12px;
24 | }
25 |
26 | div.vis-configuration-wrapper {
27 | display: block;
28 | width: 700px;
29 | }
30 |
31 | div.vis-configuration-wrapper::after {
32 | clear: both;
33 | content: "";
34 | display: block;
35 | }
36 |
37 | div.vis-configuration.vis-config-option-container {
38 | display: block;
39 | width: 495px;
40 | background-color: #fff;
41 | border: 2px solid #f7f8fa;
42 | border-radius: 4px;
43 | margin-top: 20px;
44 | left: 10px;
45 | padding-left: 5px;
46 | }
47 |
48 | div.vis-configuration.vis-config-button {
49 | display: block;
50 | width: 495px;
51 | height: 25px;
52 | vertical-align: middle;
53 | line-height: 25px;
54 | background-color: #f7f8fa;
55 | border: 2px solid #ceced0;
56 | border-radius: 4px;
57 | margin-top: 20px;
58 | left: 10px;
59 | padding-left: 5px;
60 | cursor: pointer;
61 | margin-bottom: 30px;
62 | }
63 |
64 | div.vis-configuration.vis-config-button.hover {
65 | background-color: #4588e6;
66 | border: 2px solid #214373;
67 | color: #fff;
68 | }
69 |
70 | div.vis-configuration.vis-config-item {
71 | display: block;
72 | float: left;
73 | width: 495px;
74 | height: 25px;
75 | vertical-align: middle;
76 | line-height: 25px;
77 | }
78 |
79 | div.vis-configuration.vis-config-item.vis-config-s2 {
80 | left: 10px;
81 | background-color: #f7f8fa;
82 | padding-left: 5px;
83 | border-radius: 3px;
84 | }
85 |
86 | div.vis-configuration.vis-config-item.vis-config-s3 {
87 | left: 20px;
88 | background-color: #e4e9f0;
89 | padding-left: 5px;
90 | border-radius: 3px;
91 | }
92 |
93 | div.vis-configuration.vis-config-item.vis-config-s4 {
94 | left: 30px;
95 | background-color: #cfd8e6;
96 | padding-left: 5px;
97 | border-radius: 3px;
98 | }
99 |
100 | div.vis-configuration.vis-config-header {
101 | font-size: 18px;
102 | font-weight: 700;
103 | }
104 |
105 | div.vis-configuration.vis-config-label {
106 | width: 120px;
107 | height: 25px;
108 | line-height: 25px;
109 | }
110 |
111 | div.vis-configuration.vis-config-label.vis-config-s3 {
112 | width: 110px;
113 | }
114 |
115 | div.vis-configuration.vis-config-label.vis-config-s4 {
116 | width: 100px;
117 | }
118 |
119 | div.vis-configuration.vis-config-colorBlock {
120 | top: 1px;
121 | width: 30px;
122 | height: 19px;
123 | border: 1px solid #444;
124 | border-radius: 2px;
125 | padding: 0;
126 | margin: 0;
127 | cursor: pointer;
128 | }
129 |
130 | input.vis-configuration.vis-config-checkbox {
131 | left: -5px;
132 | }
133 |
134 | input.vis-configuration.vis-config-rangeinput {
135 | position: relative;
136 | top: -5px;
137 | width: 60px;
138 | padding: 1px;
139 | margin: 0;
140 | pointer-events: none;
141 | }
142 |
143 | input.vis-configuration.vis-config-range {
144 | -webkit-appearance: none;
145 | border: 0 solid #fff;
146 | background-color: rgba(0, 0, 0, 0);
147 | width: 300px;
148 | height: 20px;
149 | }
150 |
151 | input.vis-configuration.vis-config-range::-webkit-slider-runnable-track {
152 | width: 300px;
153 | height: 5px;
154 | background: #dedede;
155 | background: -moz-linear-gradient(top, #dedede 0, #c8c8c8 99%);
156 | background: -webkit-gradient(linear,
157 | left top,
158 | left bottom,
159 | color-stop(0, #dedede),
160 | color-stop(99%, #c8c8c8));
161 | background: -webkit-linear-gradient(top, #dedede 0, #c8c8c8 99%);
162 | background: -o-linear-gradient(top, #dedede 0, #c8c8c8 99%);
163 | background: -ms-linear-gradient(top, #dedede 0, #c8c8c8 99%);
164 | background: linear-gradient(to bottom, #dedede 0, #c8c8c8 99%);
165 | border: 1px solid #999;
166 | box-shadow: #aaa 0 0 3px 0;
167 | border-radius: 3px;
168 | }
169 |
170 | input.vis-configuration.vis-config-range::-webkit-slider-thumb {
171 | -webkit-appearance: none;
172 | border: 1px solid #14334b;
173 | height: 17px;
174 | width: 17px;
175 | border-radius: 50%;
176 | background: #3876c2;
177 | background: -moz-linear-gradient(top, #3876c2 0, #385380 100%);
178 | background: -webkit-gradient(linear,
179 | left top,
180 | left bottom,
181 | color-stop(0, #3876c2),
182 | color-stop(100%, #385380));
183 | background: -webkit-linear-gradient(top, #3876c2 0, #385380 100%);
184 | background: -o-linear-gradient(top, #3876c2 0, #385380 100%);
185 | background: -ms-linear-gradient(top, #3876c2 0, #385380 100%);
186 | background: linear-gradient(to bottom, #3876c2 0, #385380 100%);
187 | box-shadow: #111927 0 0 1px 0;
188 | margin-top: -7px;
189 | }
190 |
191 | input.vis-configuration.vis-config-range:focus {
192 | outline: 0;
193 | }
194 |
195 | input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track {
196 | background: #9d9d9d;
197 | background: -moz-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%);
198 | background: -webkit-gradient(linear,
199 | left top,
200 | left bottom,
201 | color-stop(0, #9d9d9d),
202 | color-stop(99%, #c8c8c8));
203 | background: -webkit-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%);
204 | background: -o-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%);
205 | background: -ms-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%);
206 | background: linear-gradient(to bottom, #9d9d9d 0, #c8c8c8 99%);
207 | }
208 |
209 | input.vis-configuration.vis-config-range::-moz-range-track {
210 | width: 300px;
211 | height: 10px;
212 | background: #dedede;
213 | background: -moz-linear-gradient(top, #dedede 0, #c8c8c8 99%);
214 | background: -webkit-gradient(linear,
215 | left top,
216 | left bottom,
217 | color-stop(0, #dedede),
218 | color-stop(99%, #c8c8c8));
219 | background: -webkit-linear-gradient(top, #dedede 0, #c8c8c8 99%);
220 | background: -o-linear-gradient(top, #dedede 0, #c8c8c8 99%);
221 | background: -ms-linear-gradient(top, #dedede 0, #c8c8c8 99%);
222 | background: linear-gradient(to bottom, #dedede 0, #c8c8c8 99%);
223 | border: 1px solid #999;
224 | box-shadow: #aaa 0 0 3px 0;
225 | border-radius: 3px;
226 | }
227 |
228 | input.vis-configuration.vis-config-range::-moz-range-thumb {
229 | border: none;
230 | height: 16px;
231 | width: 16px;
232 | border-radius: 50%;
233 | background: #385380;
234 | }
235 |
236 | input.vis-configuration.vis-config-range:-moz-focusring {
237 | outline: 1px solid #fff;
238 | outline-offset: -1px;
239 | }
240 |
241 | // input.vis-configuration.vis-config-range::-ms-track {
242 | // width: 300px;
243 | // height: 5px;
244 | // background: 0 0;
245 | // border-color: transparent;
246 | // border-width: 6px 0;
247 | // color: transparent;
248 | // }
249 |
250 | input.vis-configuration.vis-config-range::-ms-fill-lower {
251 | background: #777;
252 | border-radius: 10px;
253 | }
254 |
255 | input.vis-configuration.vis-config-range::-ms-fill-upper {
256 | background: #ddd;
257 | border-radius: 10px;
258 | }
259 |
260 | input.vis-configuration.vis-config-range::-ms-thumb {
261 | border: none;
262 | height: 16px;
263 | width: 16px;
264 | border-radius: 50%;
265 | background: #385380;
266 | }
267 |
268 | input.vis-configuration.vis-config-range:focus::-ms-fill-lower {
269 | background: #888;
270 | }
271 |
272 | input.vis-configuration.vis-config-range:focus::-ms-fill-upper {
273 | background: #ccc;
274 | }
275 |
276 | .vis-configuration-popup {
277 | position: absolute;
278 | background: rgba(57, 76, 89, 0.85);
279 | border: 2px solid #f2faff;
280 | line-height: 30px;
281 | height: 30px;
282 | width: 150px;
283 | text-align: center;
284 | color: #fff;
285 | font-size: 14px;
286 | border-radius: 4px;
287 | -webkit-transition: opacity 0.3s ease-in-out;
288 | -moz-transition: opacity 0.3s ease-in-out;
289 | transition: opacity 0.3s ease-in-out;
290 | }
291 |
292 | .vis-configuration-popup:after,
293 | .vis-configuration-popup:before {
294 | left: 100%;
295 | top: 50%;
296 | border: solid transparent;
297 | content: " ";
298 | height: 0;
299 | width: 0;
300 | position: absolute;
301 | pointer-events: none;
302 | }
303 |
304 | .vis-configuration-popup:after {
305 | border-color: rgba(136, 183, 213, 0);
306 | border-left-color: rgba(57, 76, 89, 0.85);
307 | border-width: 8px;
308 | margin-top: -8px;
309 | }
310 |
311 | .vis-configuration-popup:before {
312 | border-color: rgba(194, 225, 245, 0);
313 | border-left-color: #f2faff;
314 | border-width: 12px;
315 | margin-top: -12px;
316 | }
317 |
318 | div.vis-color-picker {
319 | position: absolute;
320 | top: 0;
321 | left: 30px;
322 | margin-top: -140px;
323 | margin-left: 30px;
324 | width: 310px;
325 | height: 444px;
326 | z-index: 1;
327 | padding: 10px;
328 | border-radius: 15px;
329 | background-color: #fff;
330 | display: none;
331 | box-shadow: rgba(0, 0, 0, 0.5) 0 0 10px 0;
332 | }
333 |
334 | div.vis-color-picker div.vis-arrow {
335 | position: absolute;
336 | top: 147px;
337 | left: 5px;
338 | }
339 |
340 | div.vis-color-picker div.vis-arrow::after,
341 | div.vis-color-picker div.vis-arrow::before {
342 | right: 100%;
343 | top: 50%;
344 | border: solid transparent;
345 | content: " ";
346 | height: 0;
347 | width: 0;
348 | position: absolute;
349 | pointer-events: none;
350 | }
351 |
352 | div.vis-color-picker div.vis-arrow:after {
353 | border-color: rgba(255, 255, 255, 0);
354 | border-right-color: #fff;
355 | border-width: 30px;
356 | margin-top: -30px;
357 | }
358 |
359 | div.vis-color-picker div.vis-color {
360 | position: absolute;
361 | width: 289px;
362 | height: 289px;
363 | cursor: pointer;
364 | }
365 |
366 | div.vis-color-picker div.vis-brightness {
367 | position: absolute;
368 | top: 313px;
369 | }
370 |
371 | div.vis-color-picker div.vis-opacity {
372 | position: absolute;
373 | top: 350px;
374 | }
375 |
376 | div.vis-color-picker div.vis-selector {
377 | position: absolute;
378 | top: 137px;
379 | left: 137px;
380 | width: 15px;
381 | height: 15px;
382 | border-radius: 15px;
383 | border: 1px solid #fff;
384 | background: #4c4c4c;
385 | background: -moz-linear-gradient(top,
386 | #4c4c4c 0,
387 | #595959 12%,
388 | #666 25%,
389 | #474747 39%,
390 | #2c2c2c 50%,
391 | #000 51%,
392 | #111 60%,
393 | #2b2b2b 76%,
394 | #1c1c1c 91%,
395 | #131313 100%);
396 | background: -webkit-gradient(linear,
397 | left top,
398 | left bottom,
399 | color-stop(0, #4c4c4c),
400 | color-stop(12%, #595959),
401 | color-stop(25%, #666),
402 | color-stop(39%, #474747),
403 | color-stop(50%, #2c2c2c),
404 | color-stop(51%, #000),
405 | color-stop(60%, #111),
406 | color-stop(76%, #2b2b2b),
407 | color-stop(91%, #1c1c1c),
408 | color-stop(100%, #131313));
409 | background: -webkit-linear-gradient(top,
410 | #4c4c4c 0,
411 | #595959 12%,
412 | #666 25%,
413 | #474747 39%,
414 | #2c2c2c 50%,
415 | #000 51%,
416 | #111 60%,
417 | #2b2b2b 76%,
418 | #1c1c1c 91%,
419 | #131313 100%);
420 | background: -o-linear-gradient(top,
421 | #4c4c4c 0,
422 | #595959 12%,
423 | #666 25%,
424 | #474747 39%,
425 | #2c2c2c 50%,
426 | #000 51%,
427 | #111 60%,
428 | #2b2b2b 76%,
429 | #1c1c1c 91%,
430 | #131313 100%);
431 | background: -ms-linear-gradient(top,
432 | #4c4c4c 0,
433 | #595959 12%,
434 | #666 25%,
435 | #474747 39%,
436 | #2c2c2c 50%,
437 | #000 51%,
438 | #111 60%,
439 | #2b2b2b 76%,
440 | #1c1c1c 91%,
441 | #131313 100%);
442 | background: linear-gradient(to bottom,
443 | #4c4c4c 0,
444 | #595959 12%,
445 | #666 25%,
446 | #474747 39%,
447 | #2c2c2c 50%,
448 | #000 51%,
449 | #111 60%,
450 | #2b2b2b 76%,
451 | #1c1c1c 91%,
452 | #131313 100%);
453 | }
454 |
455 | div.vis-color-picker div.vis-new-color {
456 | position: absolute;
457 | width: 140px;
458 | height: 20px;
459 | border: 1px solid rgba(0, 0, 0, 0.1);
460 | border-radius: 5px;
461 | top: 380px;
462 | left: 159px;
463 | text-align: right;
464 | padding-right: 2px;
465 | font-size: 10px;
466 | color: rgba(0, 0, 0, 0.4);
467 | vertical-align: middle;
468 | line-height: 20px;
469 | }
470 |
471 | div.vis-color-picker div.vis-initial-color {
472 | position: absolute;
473 | width: 140px;
474 | height: 20px;
475 | border: 1px solid rgba(0, 0, 0, 0.1);
476 | border-radius: 5px;
477 | top: 380px;
478 | left: 10px;
479 | text-align: left;
480 | padding-left: 2px;
481 | font-size: 10px;
482 | color: rgba(0, 0, 0, 0.4);
483 | vertical-align: middle;
484 | line-height: 20px;
485 | }
486 |
487 | div.vis-color-picker div.vis-label {
488 | position: absolute;
489 | width: 300px;
490 | left: 10px;
491 | }
492 |
493 | div.vis-color-picker div.vis-label.vis-brightness {
494 | top: 300px;
495 | }
496 |
497 | div.vis-color-picker div.vis-label.vis-opacity {
498 | top: 338px;
499 | }
500 |
501 | div.vis-color-picker div.vis-button {
502 | position: absolute;
503 | width: 68px;
504 | height: 25px;
505 | border-radius: 10px;
506 | vertical-align: middle;
507 | text-align: center;
508 | line-height: 25px;
509 | top: 410px;
510 | border: 2px solid #d9d9d9;
511 | background-color: #f7f7f7;
512 | cursor: pointer;
513 | }
514 |
515 | div.vis-color-picker div.vis-button.vis-cancel {
516 | left: 5px;
517 | }
518 |
519 | div.vis-color-picker div.vis-button.vis-load {
520 | left: 82px;
521 | }
522 |
523 | div.vis-color-picker div.vis-button.vis-apply {
524 | left: 159px;
525 | }
526 |
527 | div.vis-color-picker div.vis-button.vis-save {
528 | left: 236px;
529 | }
530 |
531 | div.vis-color-picker input.vis-range {
532 | width: 290px;
533 | height: 20px;
534 | }
535 |
536 | div.vis-network div.vis-manipulation {
537 | box-sizing: content-box;
538 | border-width: 0;
539 | border-bottom: 1px;
540 | border-style: solid;
541 | border-color: #d6d9d8;
542 | background: #fff;
543 | background: -moz-linear-gradient(top,
544 | #fff 0,
545 | #fcfcfc 48%,
546 | #fafafa 50%,
547 | #fcfcfc 100%);
548 | background: -webkit-gradient(linear,
549 | left top,
550 | left bottom,
551 | color-stop(0, #fff),
552 | color-stop(48%, #fcfcfc),
553 | color-stop(50%, #fafafa),
554 | color-stop(100%, #fcfcfc));
555 | background: -webkit-linear-gradient(top,
556 | #fff 0,
557 | #fcfcfc 48%,
558 | #fafafa 50%,
559 | #fcfcfc 100%);
560 | background: -o-linear-gradient(top,
561 | #fff 0,
562 | #fcfcfc 48%,
563 | #fafafa 50%,
564 | #fcfcfc 100%);
565 | background: -ms-linear-gradient(top,
566 | #fff 0,
567 | #fcfcfc 48%,
568 | #fafafa 50%,
569 | #fcfcfc 100%);
570 | background: linear-gradient(to bottom,
571 | #fff 0,
572 | #fcfcfc 48%,
573 | #fafafa 50%,
574 | #fcfcfc 100%);
575 | padding-top: 4px;
576 | position: absolute;
577 | left: 0;
578 | top: 0;
579 | width: 100%;
580 | height: 28px;
581 | }
582 |
583 | div.vis-network div.vis-edit-mode {
584 | position: absolute;
585 | left: 0;
586 | top: 5px;
587 | height: 30px;
588 | }
589 |
590 | div.vis-network div.vis-close:hover {
591 | opacity: 0.6;
592 | }
593 |
594 | div.vis-network div.vis-edit-mode div.vis-button,
595 | div.vis-network div.vis-manipulation div.vis-button {
596 | float: left;
597 | font-family: verdana;
598 | font-size: 12px;
599 | -moz-border-radius: 15px;
600 | border-radius: 15px;
601 | display: inline-block;
602 | background-position: 0 0;
603 | background-repeat: no-repeat;
604 | height: 24px;
605 | margin-left: 10px;
606 | cursor: pointer;
607 | padding: 0 8px 0 8px;
608 | -webkit-touch-callout: none;
609 | -webkit-user-select: none;
610 | -khtml-user-select: none;
611 | -moz-user-select: none;
612 | -ms-user-select: none;
613 | user-select: none;
614 | }
615 |
616 | div.vis-network div.vis-manipulation div.vis-button:hover {
617 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.2);
618 | }
619 |
620 | div.vis-network div.vis-manipulation div.vis-button:active {
621 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.5);
622 | }
623 |
624 | /* div.vis-network div.vis-manipulation div.vis-button.vis-back {
625 | background-image: url(img/network/backIcon.png);
626 | } */
627 | div.vis-network div.vis-manipulation div.vis-button.vis-none:hover {
628 | box-shadow: 1px 1px 8px transparent;
629 | cursor: default;
630 | }
631 |
632 | div.vis-network div.vis-manipulation div.vis-button.vis-none:active {
633 | box-shadow: 1px 1px 8px transparent;
634 | }
635 |
636 | div.vis-network div.vis-manipulation div.vis-button.vis-none {
637 | padding: 0;
638 | }
639 |
640 | div.vis-network div.vis-manipulation div.notification {
641 | margin: 2px;
642 | font-weight: 700;
643 | }
644 |
645 | div.vis-network div.vis-edit-mode div.vis-button.vis-edit.vis-edit-mode {
646 | background-color: #fcfcfc;
647 | border: 1px solid #ccc;
648 | }
649 |
650 | div.vis-network div.vis-edit-mode div.vis-label,
651 | div.vis-network div.vis-manipulation div.vis-label {
652 | margin: 0 0 0 23px;
653 | line-height: 25px;
654 | }
655 |
656 | div.vis-network div.vis-manipulation div.vis-separator-line {
657 | float: left;
658 | display: inline-block;
659 | width: 1px;
660 | height: 21px;
661 | background-color: #bdbdbd;
662 | margin: 0 7px 0 15px;
663 | }
664 |
665 | div.vis-network div.vis-navigation div.vis-button {
666 | width: 34px;
667 | height: 34px;
668 | -moz-border-radius: 17px;
669 | border-radius: 17px;
670 | position: absolute;
671 | display: inline-block;
672 | background-position: 2px 2px;
673 | background-repeat: no-repeat;
674 | cursor: pointer;
675 | -webkit-touch-callout: none;
676 | -webkit-user-select: none;
677 | -khtml-user-select: none;
678 | -moz-user-select: none;
679 | -ms-user-select: none;
680 | user-select: none;
681 | }
682 |
683 | div.vis-network div.vis-navigation div.vis-button:hover {
684 | box-shadow: 0 0 3px 3px rgba(56, 207, 21, 0.3);
685 | }
686 |
687 | div.vis-network div.vis-navigation div.vis-button:active {
688 | box-shadow: 0 0 1px 3px rgba(56, 207, 21, 0.95);
689 | }
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | e2e: {
5 | setupNodeEvents(on, config) {
6 | // implement node event listeners here
7 | },
8 | },
9 |
10 | component: {
11 | devServer: {
12 | framework: "react",
13 | bundler: "webpack",
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/cypress/components/GaugeChart.cy.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react'
2 | // import GaugeChart from '../../client/components/Graphs/GaugeChart'
3 |
4 | // describe('', () => {
5 | // it('renders', () => {
6 | // // see: https://on.cypress.io/mounting-react
7 | // cy.mount()
8 | // })
9 | // })
10 |
--------------------------------------------------------------------------------
/cypress/components/InvisibleNavbar.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import InvisibleNavbar from '../../client/containers/InvisibleNavbar/InvisibleNavbar'
3 |
4 | describe('', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount()
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/components/LineGraph.cy.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react'
2 | // import LineGraph from '../../client/components/Graphs/LineGraph'
3 |
4 | // describe('', () => {
5 | // it('renders', () => {
6 | // // see: https://on.cypress.io/mounting-react
7 | // cy.mount()
8 | // })
9 | // })
10 |
--------------------------------------------------------------------------------
/cypress/e2e/frontEndTest.cy.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import Chance from 'chance'
3 | const chance = new Chance()
4 | const fakeNumber = chance.integer({min:0,max:20}).toString()
5 | const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
6 |
7 |
8 | describe('ClusterView Test', () => {
9 | beforeEach(()=>{
10 | Cypress.on('uncaught:exception', (err) => {
11 | if (resizeObserverLoopErrRe.test(err.message)) {
12 | return false
13 | }
14 | })
15 | cy.visit('http://localhost:8000/')
16 | })
17 | it('Selecting Namespaces should work')
18 | })
19 | describe('Main DashBoard Monitor test', () => {
20 | beforeEach(()=>{
21 | cy.visit('http://localhost:8000/dashboard')
22 | })
23 | it('contain the title Cluster Health Monitor', () => {
24 | cy.contains('Cluster Health Monitor').should("be.visible")
25 | })
26 | it('Load Testing Button works', ()=>{
27 | cy.get('.invisNavButton').click()
28 | cy.get('input[placeholder="Number of VUs"]').type(fakeNumber)
29 | cy.get('input[placeholder="Test Duration"]').type('s')
30 | cy.contains('DDOS me').click()
31 | cy.on('window:alert', (alertText) => {
32 | expect(alertText).to.equal('Please fill out both fields');
33 | });
34 | })
35 | })
36 |
37 | describe('Network Performance Monitor test', () => {
38 | beforeEach(()=>{
39 | cy.visit('http://localhost:8000/network')
40 | })
41 | it('should contain the title Network Performance Monitor',()=>{
42 | cy.contains('Network Performance Monitor').should("be.visible")
43 | })
44 | })
45 |
46 | describe('NavBar test', () => {
47 | beforeEach(()=>{
48 | Cypress.on('uncaught:exception', (err) => {
49 | if (resizeObserverLoopErrRe.test(err.message)) {
50 | return false
51 | }
52 | })
53 | cy.visit('http://localhost:8000/')
54 | })
55 | it('Navigation to Main DashBoard works',()=>{
56 | cy.get('#MainDashButton').click({force:true})
57 | cy.url().should('include','dashboard')
58 | })
59 | it('Navigation to Network Performance works',()=>{
60 | cy.get('#NetworkButton').click({force:true})
61 | cy.url().should('include','network')
62 | })
63 | it('Edit Cluster should work',()=>{
64 | cy.get('#EditClusterButton').click({force:true})
65 | cy.contains('Select Namespace')
66 | })
67 | })
68 |
69 | describe('LineGraph Test',()=>{
70 | const cpuRoute='/api/prom/metrics?type=cpu&hour=24'
71 | const NetworkTransmittedRoute='/api/prom/metrics?type=trans&hour=24'
72 | const memoryRoute='/api/prom/metrics?type=mem&hour=24'
73 | const NetworkRecievedRoute='/api/prom/metrics?type=rec&hour=24'
74 | beforeEach(()=>{
75 | cy.visit('http://localhost:8000/dashboard')
76 | })
77 | const testApiData = (apiRoute: string, expectedData:any) => {
78 | cy.intercept('GET', apiRoute, {
79 | statusCode: 200,
80 | body: expectedData,
81 | }).as('LineGraphData');
82 | cy.wait('@LineGraphData').then((data:any) => {
83 | expect(data.response.statusCode).to.eq(200);
84 | })
85 | }
86 | const time = Date.now()
87 | it('Loads Cpu Line Graph Data', () => {
88 | const expectedCpuData: any = [{
89 | metric:{},
90 | values:[[time,'1'],[time,'2'],[time,'3'],
91 | [time,'4'],[time,'5'],[time,'6']]
92 | }];
93 | testApiData(cpuRoute, expectedCpuData);
94 | })
95 | it('Loads memory Line Graph Data', () => {
96 | const expectedMemoryData: any = [{
97 | metric:{},
98 | values:[[time,'19224410448'],[time,'1902455540448'],[time,'1902440448'],
99 | [time,'190243410448'],[time,'1902424440448'],[time,'1904440448']]
100 | }];
101 | testApiData(memoryRoute, expectedMemoryData);
102 | })
103 | it('Loads memory Line Graph Data', () => {
104 | cy.visit('http://localhost:8000/network')
105 | const expectedNetworkTransmittedRoute: any = [{
106 | metric:{},
107 | values:[[time,'19248'],[time,'19020448'],[time,'19048'],
108 | [time,'190248'],[time,'40448'],[time,'90448']]
109 | }];
110 | testApiData(NetworkTransmittedRoute, expectedNetworkTransmittedRoute);
111 | })
112 | it('Loads memory Line Graph Data', () => {
113 | cy.visit('http://localhost:8000/network')
114 | const expectedNetworkRecievedRoute: any = [{
115 | metric:{},
116 | values:[[time,'19448'],[time,'1902448'],[time,'20448'],
117 | [time,'12448'],[time,'19048'],[time,'144448']]
118 | }];
119 | testApiData(NetworkRecievedRoute, expectedNetworkRecievedRoute);
120 | })
121 | it('Hour Drop Down Menu exists', ()=>{
122 | cy.get('#hourDropDown').select('1')
123 | cy.contains('1 hour').should('have.value', '1')
124 | cy.get('#hourDropDown').select('6')
125 | cy.contains('6 hour').should('have.value', '6')
126 | cy.get('#hourDropDown').select('12')
127 | cy.contains('12 hour').should('have.value', '12')
128 | cy.get('#hourDropDown').select('24')
129 | cy.contains('24 hour').should('have.value', '24')
130 | })
131 | it('Changing Name Spaces Menu exists')
132 | })
133 |
134 | describe('Edit Cluster View Modal works',()=>{
135 | it('Adding Namespace should work')
136 | it('Deleting Namespace should work')
137 | })
138 |
139 | describe('Guage Graph Test', () => {
140 | const memoryApiRoute = '/api/prom/mem?type=mem&hour=24¬Time=true';
141 | const cpuApiRoute = '/api/prom/cpu?type=cpu&hour=24¬Time=true';
142 | type GuageApiData = Record; // define type as an object where string is key and number is key value
143 |
144 | beforeEach(() => {
145 | cy.visit(`http://localhost:8000/dashboard`);
146 | });
147 |
148 | const testApiData = (apiRoute: string, expectedData: GuageApiData[]) => {
149 | cy.intercept('GET', apiRoute, {
150 | statusCode: 200,
151 | body: expectedData,
152 | }).as('fetchMock');
153 |
154 | cy.wait('@fetchMock').then((data:any) => {
155 | expect(data.response.statusCode).to.eq(200);
156 | const responseBody = data.response.body;
157 | expect(responseBody.length).to.eq(3);
158 | expect(responseBody[0]).to.deep.equal(expectedData[0]);
159 | expect(responseBody[1]).to.deep.equal(expectedData[1]);
160 | expect(responseBody[2]).to.deep.equal(expectedData[2]);
161 | });
162 | };
163 |
164 | it('Loads Memory Gauge Data', () => {
165 | const expectedMemoryData: GuageApiData[] = [
166 | { Memoryused: 10 },
167 | { Memoryrequested: 40 },
168 | { Memoryallocatable: 50 },
169 | ];
170 |
171 | testApiData(memoryApiRoute, expectedMemoryData);
172 | });
173 |
174 | it('Loads CPU Gauge Data', () => {
175 | const expectedCpuData: GuageApiData[] = [
176 | { Cpuused: 10 },
177 | { Cpurequested: 40 },
178 | { Cpuallocatable: 50 },
179 | ];
180 | testApiData(cpuApiRoute, expectedCpuData);
181 | });
182 | });
183 |
184 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************
3 | // This example commands.ts shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add('login', (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27 | //
28 | // declare global {
29 | // namespace Cypress {
30 | // interface Chainable {
31 | // login(email: string, password: string): Chainable
32 | // drag(subject: string, options?: Partial): Chainable
33 | // dismiss(subject: string, options?: Partial): Chainable
34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
35 | // }
36 | // }
37 | // }
--------------------------------------------------------------------------------
/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/component.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | import { mount } from 'cypress/react18'
23 |
24 | // Augment the Cypress namespace to include type definitions for
25 | // your custom command.
26 | // Alternatively, can be defined in cypress/support/component.d.ts
27 | // with a at the top of your spec.
28 | declare global {
29 | namespace Cypress {
30 | interface Chainable {
31 | mount: typeof mount
32 | }
33 | }
34 | }
35 |
36 | Cypress.Commands.add('mount', mount)
37 |
38 | // Example use:
39 | // cy.mount()
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/jest**/*.test.ts'],
5 | verbose: true,
6 | forceExit: true,
7 | clearMocks: true
8 | };
9 |
--------------------------------------------------------------------------------
/jest/super.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import { expect, test, describe, beforeEach } from '@jest/globals'
3 | import request from 'supertest'
4 | import { Chance } from 'chance'
5 | import dotenv from 'dotenv'
6 | import app from '../server/server'
7 | import promController from '../server/controllers/promController'
8 | import clusterController from '../server/controllers/clusterController'
9 | import execController from '../server/controllers/crudController'
10 | import k6Controller from '../server/controllers/k6Controller'
11 | import mapController from '../server/controllers/mapController'
12 | import lokiController from '../server/controllers/lokiController'
13 |
14 | dotenv.config()
15 |
16 | let server: any
17 | beforeEach(() => {
18 | server = app.listen(1212)
19 | })
20 | afterEach(() => {
21 | server.close()
22 | })
23 | // Test for error handler
24 | describe('404 error handler', () => {
25 | test('should respond with a 404 status code for unknown API route', async () => {
26 | const res = await request(app).get('/non-existent-request')
27 | expect(res.statusCode).toEqual(404)
28 | })
29 | })
30 |
31 | // ?-----------------------------------------Prom Controller------------------------------------->
32 | describe('Prometheus Controller', () => {
33 | const chance = new Chance()
34 | const hourNumber = chance.integer({ min: 1, max: 24 }).toString()
35 | const properties = Object.keys(promController)
36 | const getMetricsTestData = [
37 | { type: 'cpu', hour: hourNumber },
38 | { type: 'mem', hour: hourNumber },
39 | { type: 'trans', hour: hourNumber },
40 | { type: 'rec', hour: hourNumber }
41 | ]
42 | const getMemOrCpuTestData = [
43 | { type: 'cpu' },
44 | { type: 'mem' }
45 | ]
46 | // const testGlobalHandleError = [
47 | // {endpoint:`/api/prom/metrics?type=sas&hour=24&ep=${process.env.PROMETHEUS_EP}`},
48 | // {endpoint:`/api/prom/metrics`},
49 | // ]
50 | getMetricsTestData.forEach((entry) => {
51 | test(`/GET ${entry.type} for Line Chart returns data`, async () => {
52 | const res = await request(app).get(`/api/prom/metrics?type=${entry.type}&hour=${entry.hour}&ep=${process.env.PROMETHEUS_EP ?? ''}`)
53 | expect(res.status).toEqual(200)
54 | expect(Array.isArray(res.body)).toEqual(true)
55 | // expect(res.body).toMatchSnapshot()
56 | })
57 | })
58 | getMemOrCpuTestData.forEach((entry) => {
59 | test(`/Get ${entry.type} for Guage Chart returns data`, async () => {
60 | const res = await request(app).get(`/api/prom/${entry.type}?type=${entry.type}&hour=24¬Time=true&ep=${process.env.PROMETHEUS_EP ?? ''}`)
61 | expect(res.status).toEqual(200)
62 | expect(typeof res.body === 'object').toEqual(true)
63 | // expect(res.body).toMatchSnapshot()
64 | })
65 | })
66 | // testGlobalHandleError.forEach((entry)=>{
67 | // test('should handle global middleware errors', async()=>{
68 | // const defaultErr = {
69 | // log: 'Error caught in global handler',
70 | // status: 400,
71 | // message:{error: 'Error getting Data'}
72 | // };
73 | // const response = await request(app)
74 | // .get(entry.endpoint)
75 | // .expect(400)
76 | // expect(response.body).toEqual(defaultErr.message);
77 | // })
78 | // })
79 | test(`should have ${Object.keys(promController) as unknown as string}`, () => {
80 | properties.forEach((prop) => { expect(promController).toHaveProperty(prop) })
81 | })
82 | test('All Properties should be a function', () => {
83 | properties.forEach((prop) => {
84 | expect(typeof (promController as any)[prop]).toBe('function')
85 | })
86 | })
87 | })
88 | // ?----------------------------------ClusterController------------------------------------------>
89 | describe('Cluster Controller', () => {
90 | const properties = Object.keys(clusterController)
91 | const ClusterEndPoint = [
92 | { type: 'namespaces' },
93 | { type: 'pods' },
94 | { type: 'nodes' },
95 | { type: 'services' },
96 | { type: 'deployments' },
97 | { type: 'ingresses' }
98 | ]
99 | ClusterEndPoint.forEach((endpoint) => {
100 | test(`/Get ${endpoint.type} returns data`, async () => {
101 | const res = await request(app).get(`/api/cluster/${endpoint.type}`)
102 | expect(res.status).toEqual(200)
103 | })
104 | })
105 | test(`should have ${Object.keys(clusterController) as unknown as string}`, () => {
106 | properties.forEach((prop) => { expect(clusterController).toHaveProperty(`${prop}`) })
107 | })
108 |
109 | test('All Properties should be a function', () => {
110 | properties.forEach((prop) => {
111 | expect(typeof (clusterController as any)[prop]).toBe('function')
112 | /* expect(prop.constructor.name).toBe('function'); */
113 | })
114 | })
115 | })
116 | // ?-------------------------------Exec Controller------------------------------------------------>
117 | describe('Exec Controller', () => {
118 | const properties = Object.keys(execController)
119 | // const ExecEndPoint = [
120 | // { type: 'ns' },
121 | // { type: 'dep' },
122 | // ]
123 | // ExecEndPoint.forEach((endpoint) => {
124 | // test(`/Get ${endpoint.type} returns data`, async () => {
125 | // const res = await request(app).get(`/api/exec/${endpoint.type}`)
126 | // expect(res.status).toEqual(400)
127 | // })
128 | // })
129 | test(`should have ${Object.keys(execController) as unknown as string}`, () => {
130 | properties.forEach((prop) => { expect(execController).toHaveProperty(`${prop}`) })
131 | })
132 |
133 | test('All Properties should be a function', () => {
134 | properties.forEach((prop) => {
135 | expect(typeof (execController as any)[prop]).toBe('function')
136 | })
137 | })
138 | })
139 | // ?---------------------------------K6 Controller------------------------------------------------>
140 | describe('k6Controller Controller', () => {
141 | const properties = Object.keys(k6Controller)
142 | test('/Get elements returns data', async () => {
143 | const res = await request(app).get('/api/k6/test')
144 | expect(res.status).toEqual(200)
145 | })
146 | test(`should have ${Object.keys(k6Controller) as unknown as string}`, () => {
147 | properties.forEach((prop) => { expect(k6Controller).toHaveProperty(prop) })
148 | })
149 | })
150 | // ?-----------------------------------------Map Controller---------------------------------------->
151 | describe('mapController Controller', () => {
152 | const properties = Object.keys(mapController)
153 | test('/Get elements returns data', async () => {
154 | const res = await request(app).get('/api/map/elements')
155 | expect(res.status).toEqual(200)
156 | })
157 | test(`should have ${Object.keys(mapController) as unknown as string}`, () => {
158 | properties.forEach((prop) => { expect(mapController).toHaveProperty(prop) })
159 | })
160 |
161 | test('All Properties should be a function', () => {
162 | properties.forEach((prop) => {
163 | expect(typeof (mapController as any)[prop]).toBe('function')
164 | })
165 | })
166 | })
167 | // ?-------------------------------------Loki Controller----------------------------------------->
168 | describe('Loki Controller', () => {
169 | const properties = Object.keys(lokiController)
170 |
171 | test('/Get logs returns data', async () => {
172 | const res = await request(app).get('/api/loki/logs?namespace=gmp-system')
173 | expect(res.status).toEqual(200)
174 | })
175 | test(`should have ${Object.keys(lokiController) as unknown as string}`, () => {
176 | properties.forEach((prop) => { expect(lokiController).toHaveProperty(prop) })
177 | })
178 | test('All Properties should be a function', () => {
179 | properties.forEach((prop) => {
180 | expect(typeof (lokiController as any)[prop]).toBe('function')
181 | })
182 | })
183 | })
184 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kubernautical",
3 | "description": "Kubernetes project",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "scripts": {
7 | "supertest": "NODE_ENV=test concurrently \"jest --watchAll --updateSnapshot --detectOpenHandles --coverage\" \"server/server.ts\" \"kubectl --namespace=prometheus port-forward deploy/prometheus-server 9090\"",
8 | "cypress:open": "cypress open",
9 | "start": "NODE_ENV=production nodemon server/server.ts",
10 | "build": "NODE_ENV=production webpack",
11 | "dev": "NODE_ENV=development nodemon server/server.ts & NODE_ENV=development webpack serve --open --hot",
12 | "devc": "NODE_ENV=development concurrently \"webpack serve --open\" \"nodemon server/server.ts\"",
13 | "devServer": "NODE_ENV=production nodemon server/server.ts",
14 | "devkube": "NODE_ENV=development concurrently \"webpack serve --open --hot\" \"nodemon server/server.ts\"",
15 | "lint": "eslint --ext .tsx,.tx,.js --ignore-path .gitignore ."
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/oslabs-beta/Kubernautical"
20 | },
21 | "keywords": [
22 | "Kubernetes"
23 | ],
24 | "author": "Jeremiah Hogue",
25 | "license": "ISC",
26 | "dependencies": {
27 | "@babel/preset-typescript": "^7.22.5",
28 | "@babel/register": "^7.22.5",
29 | "@kubernetes/client-node": "^0.18.1",
30 | "@types/k6": "^0.45.1",
31 | "@types/uuid": "^9.0.2",
32 | "chart.js": "^4.3.3",
33 | "cookie-parser": "^1.4.6",
34 | "d3": "^7.8.5",
35 | "dotenv": "^16.3.1",
36 | "express": "^4.18.2",
37 | "jest": "^29.6.2",
38 | "react": "^18.2.0",
39 | "react-chartjs-2": "^5.2.0",
40 | "react-dom": "^18.2.0",
41 | "react-force-graph": "^1.43.0",
42 | "react-graph-vis": "^1.0.7",
43 | "react-router-dom": "^6.14.2",
44 | "sass": "^1.64.1",
45 | "supertest": "^6.3.3",
46 | "three": "^0.155.0",
47 | "ts-jest": "^29.1.1",
48 | "ts-loader": "^9.4.4",
49 | "ts-node": "^10.9.1",
50 | "uuid": "^9.0.0",
51 | "vis-network": "^9.1.6"
52 | },
53 | "devDependencies": {
54 | "@babel/core": "^7.22.9",
55 | "@babel/preset-env": "^7.22.9",
56 | "@babel/preset-react": "^7.22.5",
57 | "@testing-library/react": "^13.4.0",
58 | "@testing-library/user-event": "^14.4.3",
59 | "@types/chance": "^1.1.3",
60 | "@types/cors": "^2.8.12",
61 | "@types/express": "^4.17.14",
62 | "@types/jest": "^29.1.1",
63 | "@types/node": "^18.7.20",
64 | "@types/react": "^18.0.21",
65 | "@types/react-dom": "^18.0.6",
66 | "@types/supertest": "^2.0.12",
67 | "@types/vis": "^4.21.24",
68 | "@typescript-eslint/eslint-plugin": "^5.62.0",
69 | "@typescript-eslint/parser": "^5.40.0",
70 | "babel": "^6.23.0",
71 | "babel-loader": "^9.1.3",
72 | "chance": "^1.1.11",
73 | "concurrently": "^8.2.0",
74 | "css-loader": "^6.8.1",
75 | "cypress": "^12.17.3",
76 | "eslint": "^8.47.0",
77 | "eslint-config-airbnb": "^19.0.4",
78 | "eslint-config-airbnb-base": "^15.0.0",
79 | "eslint-config-standard-with-typescript": "^37.0.0",
80 | "eslint-plugin-import": "^2.28.0",
81 | "eslint-plugin-jsx-a11y": "^6.7.1",
82 | "eslint-plugin-n": "^16.0.1",
83 | "eslint-plugin-promise": "^6.1.1",
84 | "eslint-plugin-react": "^7.33.2",
85 | "eslint-plugin-react-hooks": "^4.6.0",
86 | "file-loader": "^6.2.0",
87 | "html-webpack-plugin": "^5.5.3",
88 | "nodemon": "^3.0.1",
89 | "sass-loader": "^13.3.2",
90 | "style-loader": "^3.3.3",
91 | "typescript": "^5.1.6",
92 | "url-loader": "^4.1.1",
93 | "webpack": "^5.88.1",
94 | "webpack-cli": "^5.1.4",
95 | "webpack-dev-server": "^4.15.1",
96 | "webpack-manifest-plugin": "^5.0.0"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/scripts/loadtest.js:
--------------------------------------------------------------------------------
1 | import http from 'k6/http';
2 | import { sleep } from 'k6';
3 |
4 | export default function () {
5 | //pull ip dynamically
6 | //? IN TERMINAL : export CLUSTER_ENDPOINT= 'your endpoint here'
7 | const response = http.get(`${__ENV.INGRESS_EP}`);
8 | if (response.status === 200) {
9 | console.log('Request successful!');
10 | } else {
11 | console.log(`Request failed with status code: ${response.status}`);
12 | }
13 | sleep(1);
14 | }
15 |
--------------------------------------------------------------------------------
/server/controllers/clusterController.ts:
--------------------------------------------------------------------------------
1 | import os from 'os'
2 | import * as k8s from '@kubernetes/client-node'
3 | import type { Request, Response, NextFunction, RequestHandler } from 'express'
4 | import { type ClusterController, type ClientObj, type container } from '../../types/types'
5 |
6 | const clusterController: ClusterController = {
7 | // setContext called before every route to obtain the correct cluster information
8 | setContext: (async (req: Request, res: Response, next: NextFunction): Promise => {
9 | // TODO fix typing
10 | const context = req.query.context as string
11 | console.log('context:', context)
12 | try {
13 | // pull kube config from secret dir
14 | const KUBE_FILE_PATH = `${os.homedir()}/.kube/config`
15 | // create kubernetes client using kube config
16 | const kc = new k8s.KubeConfig()
17 | kc.loadFromFile(KUBE_FILE_PATH)
18 | // expose all necessary apis for further use in middleware using given context
19 | if (context !== undefined && context !== '') kc.setCurrentContext(context)
20 | res.locals.currentContext = kc.getCurrentContext()
21 | res.locals.contexts = kc.getContexts()
22 | res.locals.k8sApi = kc.makeApiClient(k8s.CoreV1Api) // used for nodes, pods, namespaces
23 | res.locals.k8sApi2 = kc.makeApiClient(k8s.AppsV1Api) // used for deployments and services
24 | res.locals.k8sApi3 = kc.makeApiClient(k8s.NetworkingV1Api) // used for ingress
25 | next()
26 | } catch (error) {
27 | next(error)
28 | }
29 | }) as RequestHandler,
30 | getAllPods: (async (req: Request, res: Response, next: NextFunction): Promise => {
31 | const { k8sApi } = res.locals
32 | try {
33 | const result = await k8sApi.listPodForAllNamespaces()
34 | const pods = result.body.items.map((pod: ClientObj) => {
35 | const { name, namespace, uid } = pod.metadata
36 | const { nodeName, serviceAccount, subdomain } = pod.spec
37 | const { containerStatuses, phase } = pod.status
38 | const containerNames = containerStatuses?.map((container: container) => (container.name))
39 | return {
40 | name,
41 | namespace,
42 | uid,
43 | containerNames,
44 | nodeName,
45 | serviceAccount,
46 | subdomain,
47 | phase
48 | }
49 | })
50 | res.locals.pods = pods
51 | next()
52 | } catch (error) {
53 | next({
54 | log: `Error in clusterController.getAllPods${error as string}`,
55 | status: 400,
56 | message: { error: 'Error getting data' }
57 | })
58 | }
59 | }) as RequestHandler,
60 | getAllNodes: (async (req: Request, res: Response, next: NextFunction): Promise => {
61 | const { k8sApi } = res.locals
62 | try {
63 | const result = await k8sApi.listNode()
64 | const nodes = result.body.items.map((node: ClientObj) => {
65 | const { name, uid, labels } = node.metadata
66 | const { allocatable, capacity, conditions, nodeInfo } = node.status
67 | return {
68 | name,
69 | labels,
70 | uid,
71 | allocatable,
72 | capacity,
73 | conditions,
74 | nodeInfo
75 | }
76 | })
77 | res.locals.nodes = nodes
78 | next()
79 | } catch (error) {
80 | next({
81 | log: `Error in clusterController.getAllNodes${error as string}`,
82 | status: 400,
83 | message: { error: 'Error getting data' }
84 | })
85 | }
86 | }) as RequestHandler,
87 | getAllNamespaces: (async (req: Request, res: Response, next: NextFunction): Promise => {
88 | const { k8sApi } = res.locals
89 | const { all } = req.query
90 | try {
91 | const result = await k8sApi.listNamespace()
92 | const filtered = all === 'true'
93 | ? result.body.items
94 | : result.body.items.filter((namespace: ClientObj) => {
95 | const name = namespace.metadata?.name
96 | return !!(name?.slice(0, 4) !== 'kube' && name?.slice(0, 7) !== 'default' && name?.slice(0, 10) !== 'gmp-public')
97 | // && name?.slice(0, 4) !== 'loki'
98 | })
99 | const namespaces = filtered.map((namespace: ClientObj) => {
100 | const { metadata } = namespace
101 | return {
102 | name: metadata?.name,
103 | uid: metadata?.uid
104 | }
105 | })
106 | res.locals.namespaces = namespaces
107 | next()
108 | } catch (error) {
109 | next({
110 | log: `Error in clusterController.getAllNameSpaces${error as string}`,
111 | status: 400,
112 | message: { error: 'Error getting data' }
113 | })
114 | }
115 | }) as RequestHandler,
116 | getAllServices: (async (req: Request, res: Response, next: NextFunction): Promise => {
117 | const { k8sApi } = res.locals
118 | try {
119 | const result = await k8sApi.listServiceForAllNamespaces()
120 | const services = result.body.items
121 | .map((service: ClientObj) => {
122 | const { name, uid, namespace } = service.metadata
123 | const { ipFamilies, ports, type } = service.spec
124 | const ips = service.status?.loadBalancer?.ingress
125 | let ingressIP
126 | if (ips !== undefined) ingressIP = ips[0]?.ip ?? ips[0]?.hostname
127 | // TODO pull more data as needed here
128 | return {
129 | name,
130 | uid,
131 | namespace,
132 | ipFamilies,
133 | ports,
134 | ingressIP,
135 | type
136 | }
137 | })
138 | res.locals.services = services
139 | next()
140 | } catch (error) {
141 | next({
142 | log: `Error in clusterController.getAllServices${error as string}`,
143 | status: 400,
144 | message: { error: 'Error getting data' }
145 | })
146 | }
147 | }) as RequestHandler,
148 | getAllDeployments: (async (req: Request, res: Response, next: NextFunction): Promise => {
149 | const { k8sApi2 } = res.locals
150 | try {
151 | const result = await k8sApi2.listDeploymentForAllNamespaces()
152 | const deployments = result.body.items
153 | .map((deployment: ClientObj) => {
154 | const { name, uid, namespace } = deployment.metadata
155 | const { strategy } = deployment.spec
156 | const { type } = strategy
157 | const { availableReplicas } = deployment.status
158 | // TODO pull more data as needed here
159 | return {
160 | name,
161 | uid,
162 | namespace,
163 | type,
164 | availableReplicas
165 | }
166 | })
167 | res.locals.deployments = deployments
168 | next()
169 | } catch (error) {
170 | next({
171 | log: `Error in clusterController.getAllDeployments${error as string}`,
172 | status: 400,
173 | message: { error: 'Error getting data' }
174 | })
175 | }
176 | }) as RequestHandler,
177 | //! We dont have any ingresses yet
178 | getAllIngresses: (async (req: Request, res: Response, next: NextFunction): Promise => {
179 | const { k8sApi3 } = res.locals
180 | try {
181 | const result = await k8sApi3.listIngressForAllNamespaces()
182 | res.locals.ingresses = result
183 | next()
184 | } catch (error) {
185 | next({
186 | log: `Error in clusterController.getAllIngresses${error as string}`,
187 | status: 400,
188 | message: { error: 'Error getting data' }
189 | })
190 | }
191 | }) as RequestHandler
192 | }
193 |
194 | export default clusterController
195 |
--------------------------------------------------------------------------------
/server/controllers/crudController.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'child_process'
2 | import type { Request, Response, NextFunction, RequestHandler } from 'express'
3 | import { type CrudController } from '../../types/types'
4 |
5 | const crudController: CrudController = {
6 | namespace: (async (req: Request, res: Response, next: NextFunction): Promise => {
7 | const { namespace, crud, context } = req.query
8 | try {
9 | const command = `kubectl config use-context ${context as string}\
10 | && kubectl ${crud as string} namespace ${namespace as string}`
11 | exec(command, (err, stdout, stderr) => {
12 | if (err != null) {
13 | console.log('Error executing command:', err)
14 | throw new Error()
15 | }
16 | console.log('stdout:', stdout)
17 | next()
18 | })
19 | } catch (error) {
20 | next({
21 | log: `Error in crudController.namespace${error as string}`,
22 | status: 400,
23 | message: { error: 'Error getting data' }
24 | })
25 | }
26 | }) as RequestHandler,
27 | deployment: (async (req: Request, res: Response, next: NextFunction): Promise => {
28 | const {
29 | namespace,
30 | crud,
31 | image,
32 | deployment,
33 | replicas,
34 | type,
35 | port,
36 | targetPort,
37 | old,
38 | context,
39 | name
40 | } = req.query
41 | try {
42 | let action = ''
43 | switch (crud) {
44 | case 'create':
45 | action = `--image=${image as string}`
46 | break
47 | case 'scale':
48 | action = `--replicas=${replicas as string}`
49 | break
50 | case 'expose':
51 | action = `--port=${port as string} --target-port=${targetPort as string}\
52 | --type=${type as string} --name=${name as string}`
53 | break
54 | default:
55 | break
56 | }
57 | // TODO handle errors/edge cases from front end
58 | const command = `kubectl config use-context ${context as string} \
59 | && kubectl ${crud as string} deployment ${deployment as string}\
60 | ${action !== undefined ? `${action}` : ''} -n ${namespace as string}`
61 | console.log('command:', command)
62 | exec(command, (err, stdout, stderr) => {
63 | if (err != null) {
64 | console.log('Error executing command:', err)
65 | throw new Error()
66 | }
67 | console.log('stdout:', stdout)
68 | let timeOut = 2000
69 | if (old !== undefined && replicas !== undefined) {
70 | timeOut = old < replicas ? 5000 : 45000
71 | }
72 | return setTimeout(() => {
73 | next()
74 | }, timeOut) // 45 works
75 | })
76 | } catch (error) {
77 | next({
78 | log: `Error in crudController.deployment${error as string}`,
79 | status: 400,
80 | message: { error: 'Error getting data' }
81 | })
82 | }
83 | }) as RequestHandler,
84 | service: (async (req: Request, res: Response, next: NextFunction): Promise => {
85 | const { namespace, crud, service, context } = req.query
86 | // type port and targetPort
87 | try {
88 | const action = ''
89 | switch (crud) {
90 | case 'create':
91 | // action = `--image=${image}`;
92 | break
93 | case 'scale':
94 | // action = `--replicas=${replicas}`;
95 | break
96 | case 'expose':
97 | // action = `--port=${port} --target-port=${targetPort} --type=${type}`;
98 | break
99 | default:
100 | break
101 | }
102 | // TODO handle errors/edge cases from front end
103 | const command = `kubectl config use-context ${context as string}\
104 | && kubectl ${crud as string} svc ${service as string}\
105 | ${action !== undefined ? `${action}` : ''} -n ${namespace as string}`
106 | console.log('command:', command)
107 | exec(command, (err, stdout, stderr) => {
108 | if (err != null) {
109 | console.log('Error executing command:', err)
110 | throw new Error()
111 | }
112 | console.log('stdout:', stdout)
113 | return setTimeout(() => { next() }, 1000)
114 | })
115 | } catch (error) {
116 | next({
117 | log: `Error in crudController.deployment${error as string}`,
118 | status: 400,
119 | message: { error: 'Error getting data' }
120 | })
121 | }
122 | }) as RequestHandler
123 | }
124 |
125 | export default crudController
126 |
--------------------------------------------------------------------------------
/server/controllers/k6Controller.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'child_process'
2 | import type { Request, Response, NextFunction, RequestHandler } from 'express'
3 | import { type K6Controller } from '../../types/types'
4 |
5 | const k6Controller: K6Controller = {
6 | testing: (async (req: Request, res: Response, next: NextFunction): Promise => {
7 | const { vus, duration, ip } = req.query
8 | try {
9 | let command = `export INGRESS_EP=http://${ip} && k6 run`
10 | if (vus !== undefined && duration !== undefined) {
11 | command += ` --vus ${vus as string} --duration ${duration as string}s scripts/loadtest.js`
12 | } else throw new Error()
13 | console.log(command)
14 | exec(command, (err) => {
15 | if (err !== null) {
16 | throw new Error()
17 | }
18 | })
19 | next()
20 | } catch (error) {
21 | next({
22 | log: `Error in k6Controller.testing ${error as string}`,
23 | status: 400,
24 | message: { error: 'Error getting data' }
25 | })
26 | }
27 | }) as RequestHandler
28 | }
29 |
30 | export default k6Controller
31 |
--------------------------------------------------------------------------------
/server/controllers/lokiController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction, RequestHandler } from 'express'
2 | import { type LokiController } from '../../types/types'
3 |
4 | // require('dotenv').config()
5 |
6 | // const GATE = process.env.LOKI_GATE;
7 | const lokiController: LokiController = {
8 | testing: (async (req: Request, res: Response, next: NextFunction): Promise => {
9 | // {cluster=~".+"} |= "level=error"
10 | try {
11 | const { namespace, log, pod, ep } = req.query
12 | // limit, start, and end
13 | // lokiEndpoint using exposed gateway IP via load balancer
14 | const lokiEndpoint = `http://${ep as string}/loki/api/v1/query_range?query=`
15 | let logQuery = `{namespace="${namespace as string}"${pod !== undefined && pod !== '' ? `, pod="${pod as string}"` : ''}}`
16 |
17 | // havent tested start and end yet
18 | // if (start && end) logQuery += `{time >= ${start} and time <= ${end}}`;
19 |
20 | // query by type, error or info
21 | if (log !== undefined && log !== '') logQuery += ` |= "level=${log as string}"`
22 |
23 | // if (limit) logQuery += `&limit=${limit}`;
24 | const query = lokiEndpoint + logQuery
25 | const response = await fetch(query)
26 | const data = await response.json()
27 |
28 | res.locals.data = data
29 |
30 | next()
31 | } catch (error) {
32 | next({
33 | log: `Error happened at lokiController.logs${error as string}`,
34 | status: 400,
35 | message: { error: 'Error getting data' }
36 | })
37 | }
38 | }) as RequestHandler
39 | }
40 |
41 | export default lokiController
42 |
--------------------------------------------------------------------------------
/server/controllers/mapController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction, RequestHandler } from 'express'
2 | import { type MapController } from '../../types/types'
3 |
4 | const mapController: MapController = {
5 |
6 | getElements: (async (req: Request, res: Response, next: NextFunction): Promise => {
7 | try {
8 | const { pods, namespaces, deployments, services, contexts, currentContext } = res.locals
9 | res.locals.elements = {
10 | pods,
11 | namespaces,
12 | deployments,
13 | services,
14 | contexts,
15 | currentContext
16 | }
17 | next()
18 | } catch (error) {
19 | next({
20 | log: `Error in mapController.getElements${error as string}`,
21 | status: 400,
22 | message: { error: 'Error getting data' }
23 | })
24 | }
25 | }) as RequestHandler
26 | }
27 |
28 | export default mapController
29 |
--------------------------------------------------------------------------------
/server/controllers/promController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction, RequestHandler } from 'express'
2 | import type { prometheusController } from '../../types/types'
3 |
4 | // start and end total time period, step is how often to grab data point
5 | // [time in minutes] averages to smoothe the graph
6 | // sum
7 | // summation
8 | // rate
9 | // per second usage
10 | // 100 does (changed for 2.82 (940*3/1000) cores)
11 | // number of cores used * 100/2.82 (cores used * 35.46)
12 | // how to find available cores dynamically ---- Steves next task
13 |
14 | const promController: prometheusController = {
15 | // TODO refactor controllers
16 |
17 | getMetrics: (async (req: Request, res: Response, next: NextFunction): Promise => {
18 | const { type, hour, scope, name, notTime, ep } = req.query // pods--->scope, podname---->name
19 | console.log(ep)
20 | // ? default start, stop, step, query string
21 | let start = new Date(Date.now() - 1440 * 60000).toISOString() // 24 hours
22 | const end = new Date(Date.now()).toISOString()
23 | let step = 10
24 | let query = `http://${ep as string}/api/v1/${notTime !== undefined ? 'query' : 'query_range'}?query=`
25 |
26 | // api/prom/metrics?type=cpu&hour=24&name=gmp-system
27 | // ^hour is required
28 | const userCores = 100 / res.locals.available
29 | start = new Date(Date.now() - Number(hour) * 3600000).toISOString()
30 | step = Math.ceil((step / (24 / Number(hour))))
31 |
32 | //! <----------------------------QUERIES (NOW MODULARIZED)------------------------------------->
33 | if (type === 'cpu') {
34 | query += `sum(rate(container_cpu_usage_seconds_total{container!="",
35 | ${scope !== undefined ? `${scope as string}="${name as string}"` : ''}}[10m]))
36 | ${notTime === undefined ? `*${userCores}` : ''}`
37 | }
38 | if (type === 'mem') {
39 | query += `sum(container_memory_usage_bytes{container!="",
40 | ${scope !== undefined ? `${scope as string}="${name as string}"` : ''}})`
41 | }
42 | if (type === 'trans') {
43 | query += `sum(rate(container_network_transmit_bytes_total
44 | ${scope !== undefined ? `{${scope as string}="${name as string}"}` : ''}[10m]))`
45 | }
46 | if (type === 'rec') {
47 | query += `sum(rate(container_network_receive_bytes_total
48 | ${scope !== undefined ? `{${scope as string}="${name as string}"}` : ''}[10m]))`
49 | }
50 | if (notTime === undefined) query += `&start=${start}&end=${end}&step=${step}m`
51 | try {
52 | const response = await fetch(query)
53 | const result = await response.json()
54 | if (notTime !== undefined && type === 'cpu') {
55 | const usedCpu = result.data.result[0].value[1]
56 | res.locals.usedCpu = usedCpu
57 | } else if (notTime !== undefined && type === 'mem') {
58 | const usedMem = result.data.result[0].value[1]
59 | res.locals.usedMem = usedMem
60 | } else res.locals.data = result.data.result
61 | next()
62 | } catch (error) {
63 | next({
64 | log: `Error in promController.getMetrics${error as string}`,
65 | status: 400,
66 | message: { error: 'Error getting Data' }
67 | })
68 | }
69 | }) as RequestHandler,
70 | getCores: (async (req: Request, res: Response, next: NextFunction): Promise => {
71 | const { ep } = req.query
72 | try {
73 | const response = await fetch(`http://${ep as string}/api/v1/query?query=sum(kube_node_status_allocatable{resource="cpu"})`)
74 | const result = await response.json()
75 | const availableCores = result.data.result[0].value[1]
76 | res.locals.available = availableCores
77 | next()
78 | } catch (error) {
79 | next({
80 | log: `Error in promController.getCores${error as string}`,
81 | status: 400,
82 | message: { error: 'Error getting Data' }
83 | })
84 | }
85 | }) as RequestHandler,
86 | getCpu: (async (req: Request, res: Response, next: NextFunction): Promise => {
87 | const { ep } = req.query
88 | const requestedQuery = `http://${ep as string}/api/v1/query?query=sum(kube_pod_container_resource_requests{resource="cpu"})`
89 | try {
90 | const { usedCpu, available } = res.locals
91 | const response = await fetch(requestedQuery)
92 | const requested = Number((await response.json()).data.result[0].value[1])
93 | const remaining = available - (requested + Number(usedCpu))
94 | const cpuArr = [usedCpu, requested, remaining]
95 | const cpuPercents = cpuArr.map((value) => (value / available) * 100)
96 | res.locals.cpuPercents = [
97 | { usedCpu: cpuPercents[0] },
98 | { requestedCpu: cpuPercents[1] },
99 | { availableCpu: cpuPercents[2] }
100 | ]
101 | next()
102 | } catch (error) {
103 | next({
104 | log: `Error in promController.getCpu${error as string}`,
105 | status: 400,
106 | message: { error: 'Error getting Data' }
107 | })
108 | }
109 | }) as RequestHandler,
110 | getMem: (async (req: Request, res: Response, next: NextFunction): Promise => {
111 | const { ep } = req.query
112 | const query = `http://${ep as string}/api/v1/query?query=`
113 | const totalMemory = `${query}sum(node_memory_MemTotal_bytes)`
114 | const reqMemory = `${query}sum(kube_pod_container_resource_requests{resource="memory"})`
115 |
116 | try {
117 | const { usedMem } = res.locals
118 | const response = await fetch(totalMemory)
119 | const totalMem = Number((await response.json()).data.result[0].value[1])
120 | const response2 = await fetch(reqMemory)
121 | const reqMem = Number((await response2.json()).data.result[0].value[1])
122 | const remaining = totalMem - (reqMem + Number(usedMem))
123 | const memArr = [usedMem, reqMem, remaining]
124 | const memoryPercents = memArr.map((value) => (value / totalMem) * 100)
125 | res.locals.memoryPercents = [
126 | { usedMemory: memoryPercents[0] },
127 | { requestedMemory: memoryPercents[1] },
128 | { availableMemory: memoryPercents[2] }
129 | ]
130 | next()
131 | } catch (error) {
132 | next({
133 | log: `Error in promController.getMem${error as string}`,
134 | status: 400,
135 | message: { error: 'Error getting Data' }
136 | })
137 | }
138 | }) as RequestHandler
139 | }
140 |
141 | export default promController
142 |
--------------------------------------------------------------------------------
/server/routers/apiRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import promRouter from './promRouter'
3 | import clusterRouter from './clusterRouter'
4 | import mapRouter from './mapRouter'
5 | import k6Router from './k6Router'
6 | import crudRouter from './crudRouter'
7 | import lokiRouter from './lokiRouter'
8 |
9 | const router = express.Router()
10 |
11 | // routes for various functional parts of the application
12 | router.use('/prom', promRouter)
13 | router.use('/cluster', clusterRouter)
14 | router.use('/map', mapRouter)
15 | router.use('/k6', k6Router)
16 | router.use('/crud', crudRouter)
17 | router.use('/loki', lokiRouter)
18 |
19 | export default router
20 |
--------------------------------------------------------------------------------
/server/routers/clusterRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import clusterController from '../controllers/clusterController'
4 |
5 | const router = express.Router()
6 |
7 | // routes for retrieving imorantant cluster information
8 | // setContext called before every route to obatin the correct cluster information
9 | router.get(
10 | '/namespaces',
11 | clusterController.setContext,
12 |
13 | clusterController.getAllNamespaces,
14 |
15 | (req: Request, res: Response, next: NextFunction) => {
16 | res.status(200).json(res.locals.namespaces)
17 | }
18 | )
19 | router.get(
20 | '/pods',
21 | clusterController.setContext,
22 |
23 | clusterController.getAllPods,
24 |
25 | (req: Request, res: Response, next: NextFunction) => {
26 | res.status(200).json(res.locals.pods)
27 | }
28 | )
29 | router.get(
30 | '/nodes',
31 | clusterController.setContext,
32 |
33 | clusterController.getAllNodes,
34 |
35 | (req: Request, res: Response, next: NextFunction) => {
36 | res.status(200).json(res.locals.nodes)
37 | }
38 | )
39 | router.get(
40 | '/services',
41 | clusterController.setContext,
42 |
43 | clusterController.getAllServices,
44 |
45 | (req: Request, res: Response, next: NextFunction) => {
46 | res.status(200).json(res.locals.services)
47 | }
48 | )
49 | router.get(
50 | '/deployments',
51 | clusterController.setContext,
52 |
53 | clusterController.getAllDeployments,
54 |
55 | (req: Request, res: Response, next: NextFunction) => {
56 | res.status(200).json(res.locals.deployments)
57 | }
58 | )
59 |
60 | //! ------------------------------These Routes are for testing (for now)--------------------------->
61 | router.get(
62 | '/ingresses',
63 | clusterController.setContext,
64 |
65 | clusterController.getAllIngresses,
66 |
67 | (req: Request, res: Response, next: NextFunction) => {
68 | res.status(200).json(res.locals.ingresses)
69 | }
70 | )
71 |
72 | export default router
73 |
--------------------------------------------------------------------------------
/server/routers/crudRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import crudController from '../controllers/crudController'
4 |
5 | const router = express.Router()
6 |
7 | router.get(
8 | '/ns',
9 | crudController.namespace,
10 |
11 | (req: Request, res: Response, next: NextFunction) => {
12 | res.status(200).json({ message: 'success' })
13 | }
14 | )
15 | router.get(
16 | '/dep',
17 | crudController.deployment,
18 |
19 | (req: Request, res: Response, next: NextFunction) => {
20 | res.status(200).json({ message: 'success' })
21 | }
22 | )
23 | router.get(
24 | '/svc',
25 | crudController.service,
26 |
27 | (req: Request, res: Response, next: NextFunction) => {
28 | res.status(200).json({ message: 'success' })
29 | }
30 | )
31 |
32 | export default router
33 |
--------------------------------------------------------------------------------
/server/routers/k6Router.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import k6Controller from '../controllers/k6Controller'
4 |
5 | const router = express.Router()
6 |
7 | router.get(
8 | '/test',
9 | k6Controller.testing,
10 |
11 | (req: Request, res: Response, next: NextFunction) => {
12 | res.status(200).json({ message: 'Load test triggered succeessfully' })
13 | }
14 | )
15 |
16 | export default router
17 |
--------------------------------------------------------------------------------
/server/routers/lokiRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import lokiController from '../controllers/lokiController'
4 |
5 | const router = express.Router()
6 |
7 | router.get(
8 | '/logs',
9 | lokiController.testing,
10 |
11 | (req: Request, res: Response, next: NextFunction) => {
12 | res.status(200).json(res.locals.data)
13 | }
14 | )
15 |
16 | export default router
17 |
--------------------------------------------------------------------------------
/server/routers/mapRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import mapController from '../controllers/mapController'
4 | import clusterController from '../controllers/clusterController'
5 |
6 | const router = express.Router()
7 |
8 | router.get(
9 | '/elements',
10 | clusterController.setContext,
11 | clusterController.getAllPods,
12 | // clusterController.getAllNodes,
13 | clusterController.getAllNamespaces,
14 | clusterController.getAllDeployments,
15 | clusterController.getAllServices,
16 | mapController.getElements,
17 | (req: Request, res: Response, next: NextFunction) => {
18 | res.status(200).json(res.locals.elements) // fix to send elements back
19 |
20 | // res.status(200).json(res.locals.result);
21 | }
22 | )
23 |
24 | export default router
25 |
--------------------------------------------------------------------------------
/server/routers/promRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import promController from '../controllers/promController'
4 |
5 | const router = express.Router()
6 |
7 | router.get(
8 | '/metrics',
9 | promController.getCores,
10 | promController.getMetrics,
11 | (req: Request, res: Response, next: NextFunction) => {
12 | res.status(200).json(res.locals.data)
13 | }
14 | )
15 |
16 | router.get(
17 | '/mem',
18 | promController.getMetrics,
19 | promController.getMem,
20 | (req: Request, res: Response, next: NextFunction) => {
21 | res.status(200).json(res.locals.memoryPercents)
22 | }
23 | )
24 |
25 | router.get(
26 | '/cpu',
27 | promController.getCores,
28 | promController.getMetrics,
29 | promController.getCpu,
30 | (req: Request, res: Response, next: NextFunction) => {
31 | res.status(200).json(res.locals.cpuPercents)
32 | }
33 | )
34 |
35 | export default router
36 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import type { Request, Response, NextFunction } from 'express'
3 | import type { ServerError } from '../types/types'
4 | import apiRouter from './routers/apiRouter'
5 |
6 | const PORT = 3000
7 |
8 | const app = express()
9 | app.use(express.json())
10 |
11 | // general endpoint for routes
12 | app.use('/api', apiRouter)
13 |
14 | // error handler for bad routes/requests to backend
15 | app.use((req, res) => {
16 | res.sendStatus(404)
17 | })
18 |
19 | // global error handler for all middleware and routes
20 | app.use((err: ServerError, req: Request, res: Response, next: NextFunction) => {
21 | const defaultErr = {
22 | log: 'Error caught in global handler',
23 | status: 500,
24 | message: { err: 'An error occurred' }
25 | }
26 | const errorObj = { ...defaultErr, ...err }
27 | console.log(errorObj.log)
28 | console.log(err)
29 | return res.status(errorObj.status).json(errorObj.message)
30 | })
31 |
32 | if (process.env.NODE_ENV !== 'test') {
33 | app.listen(PORT, () => {
34 | console.log(`App listening on port ${PORT}`)
35 | })
36 | }
37 | export default app
38 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "dist", "coverage"],
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 | /* Projects */
5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
11 | /* Language and Environment */
12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
14 | "jsx": "react", /* Specify what JSX code is generated. */
15 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
24 | /* Modules */
25 | "module": "commonjs", /* Specify what module code is generated. */
26 | // "rootDir": "./", /* Specify the root folder within your source files. */
27 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
32 | "types": ["node", "jest"], /* Specify type package names to be included without being referenced in a source file. */
33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
35 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
36 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
37 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
38 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
39 | // "resolveJsonModule": true, /* Enable importing .json files. */
40 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
42 | /* JavaScript Support */
43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
46 | /* Emit */
47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
53 | // "outDir": "./", /* Specify an output folder for all emitted files. */
54 | "removeComments": true, /* Disable emitting comments. */
55 | // "noEmit": true, /* Disable emitting files from a compilation. */
56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 | /* Interop Constraints */
71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
72 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 | /* Type Checking */
78 | "strict": true, /* Enable all strict type-checking options. */
79 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
80 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
81 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
82 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
83 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
84 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
85 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
86 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
87 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
88 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
92 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
97 | /* Completeness */
98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
100 | },
101 | // "include": ["server/**/*", "client/**/*"],
102 | "esclude": ["node_modules"]
103 | }
--------------------------------------------------------------------------------
/types/png.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png"
2 | declare module "*.gif"
--------------------------------------------------------------------------------
/types/react-graph-vis.d.ts:
--------------------------------------------------------------------------------
1 | declare module "react-graph-vis" {
2 | import { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis";
3 | export { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis";
4 | import { Component } from "react";
5 |
6 | export interface graphEvents {
7 | [event: NetworkEvents]: (params?: any) => void;
8 | }
9 | //Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis.
10 | export interface graphData {
11 | nodes: Node[];
12 | edges: Edge[];
13 | }
14 |
15 | export interface NetworkGraphProps {
16 | graph: graphData;
17 | options?: Options;
18 | events?: graphEvents;
19 | getNetwork?: (network: Network) => void;
20 | identifier?: string;
21 | style?: React.CSSProperties;
22 | getNodes?: (nodes: DataSet) => void;
23 | getEdges?: (edges: DataSet) => void;
24 | }
25 |
26 | export interface NetworkGraphState {
27 | identifier: string;
28 | }
29 |
30 | export default class NetworkGraph extends Component<
31 | NetworkGraphProps,
32 | NetworkGraphState
33 | > {
34 | render();
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/types/types.ts:
--------------------------------------------------------------------------------
1 | // import type { V1Container, V1ContainerImage, V1PodIP } from '@kubernetes/client-node';
2 | import { type IncomingMessage } from 'http'
3 | import type { RequestHandler } from 'express'
4 | import { type Dispatch, type SetStateAction } from 'react'
5 |
6 | export interface ServerError {
7 | err: '400'
8 | }
9 |
10 | export interface ClusterController {
11 | getAllPods: RequestHandler
12 | getAllNodes: RequestHandler
13 | getAllNamespaces: RequestHandler
14 | getAllServices: RequestHandler
15 | getAllDeployments: RequestHandler
16 | getAllIngresses: RequestHandler
17 | // getAllContexts: RequestHandler
18 | // setContext: RequestHandler
19 | // getAllPodLogs: RequestHandler
20 | setContext: RequestHandler
21 | }
22 |
23 | export interface prometheusController {
24 | getMetrics: RequestHandler
25 | getCores: RequestHandler
26 | getMem: RequestHandler
27 | getCpu: RequestHandler
28 | }
29 |
30 | export interface K6Controller {
31 | testing: RequestHandler
32 | }
33 |
34 | export interface LokiController {
35 | testing: RequestHandler
36 | }
37 |
38 | export interface CrudController {
39 | namespace: RequestHandler
40 | deployment: RequestHandler
41 | service: RequestHandler
42 | }
43 |
44 | export interface MapController {
45 | getElements: RequestHandler
46 | }
47 | export interface ClusterNode {
48 | // kind: string;
49 | id: string
50 | title?: string
51 | label?: string | undefined
52 | // font: {
53 | // color: string;
54 | // size?: number;
55 | // };
56 | // labels?: any;
57 | // matchLabels?: any;
58 | size?: number
59 | image?: any
60 | shape?: string
61 | }
62 | export interface ClusterEdge {
63 | from: string
64 | to: string
65 | length?: number
66 | }
67 | export interface clusterGraphData {
68 | nodes: ClusterNode[]
69 | edges: ClusterEdge[]
70 | }
71 | export interface Props {
72 | type?: string
73 | // graphType?: string
74 | // title?: string
75 | header?: string
76 | // yAxisTitle?: string
77 | // color?: string | string[]
78 | // graphTextColor?: string
79 | // backgroundColor?: string | string[]
80 | // borderColor?: string | string[]
81 | hour?: string
82 | style?: number
83 | clusterData?: ClusterData
84 | namespace?: string
85 | ep?: string
86 | // logType?: string
87 | // pod?: string
88 | }
89 | export interface InvisibleNavbarModalProps {
90 | style: number
91 | setShowModal: Dispatch>
92 | }
93 | export interface GaugeGraphProps {
94 | type: string
95 | title: string
96 | graphTextColor: string
97 | backgroundColor: string | string[]
98 | borderColor: string | string[]
99 | }
100 | export interface LineGraphProps {
101 | type: string
102 | title: string
103 | yAxisTitle: string
104 | color: string | string[]
105 | graphTextColor: string
106 | }
107 | export interface LogProps {
108 | namespace: string
109 | logType: string
110 | pod: string
111 | }
112 | export interface MiniProps {
113 | style: number
114 | modalType: string
115 | setShowModal: Dispatch>
116 | crudSelection: string
117 | ns: string
118 | service: string
119 | deployment: string
120 | }
121 | export interface SelectorProps {
122 | type: string
123 | state: any
124 | stateSetter: Dispatch>
125 | modalPos: number
126 | setModalPos: Dispatch>
127 | modalType: string
128 | setModalType: Dispatch>
129 | showModal: boolean
130 | setShowModal: Dispatch>
131 | crudSelection?: string
132 | setCrudSelection: Dispatch>
133 | ns?: string
134 | }
135 | export interface CLusterObj {
136 | // [key: string]: number | string | CLusterObj | undefined
137 | [key: string]: any
138 | name: string
139 | namespace?: string
140 | uid: string
141 | containerNames?: string[] // array of strings (for now)
142 | nodeName?: string
143 | serviceAccount?: string
144 | subdomain?: string
145 | phase?: string
146 | ipFamilies?: string[]
147 | ports?: portObj[] // array of obj (portObj)
148 | type?: string
149 | // strategy?: strategyObj //object with type as string
150 | availableReplicas?: number
151 | // conditions?: string
152 | }
153 | export interface ClusterData {
154 | [key: string]: any
155 | pods?: CLusterObj
156 | namespaces?: CLusterObj
157 | deployments?: CLusterObj
158 | services?: CLusterObj
159 | contexts?: ContextObj
160 | }
161 | export interface ContextObj {
162 | [key: string]: any
163 | cluster: string
164 | name?: string
165 | user?: string
166 | namespace?: string
167 | }
168 | export interface portObj {
169 | [key: string]: any
170 | name: string
171 | port: number
172 | protocol: string
173 | targetPort: number
174 | }
175 | export type nestedObj = Record
176 | export interface Pod {
177 | name: string
178 | namespace: string
179 | uid: string
180 | // labels: any;
181 | containerNames: string[]
182 | nodeName: string
183 | serviceAccount: string
184 | phase: string
185 | subdomain: string
186 | }
187 | export interface log {
188 | response: IncomingMessage
189 | body: string
190 | }
191 | export interface globalServiceObj {
192 | name: string
193 | ip: string
194 | ports?: portObj[]
195 | }
196 | export interface ClientObj {
197 | metadata: CLusterObj
198 | spec: CLusterObj
199 | status: CLusterObj
200 | }
201 | export interface container {
202 | name: string
203 | }
204 |
205 | export interface LogEntry {
206 | timestamp: string
207 | message: string
208 | namespace: string
209 | container: string
210 | pod: string
211 | job: string
212 | // values?: [string, string][];
213 | }
214 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './client/index.tsx',
6 | mode: process.env.NODE_ENV,
7 | resolve: {
8 | extensions: ['.jsx', '.js', '.tsx', '.ts'],
9 | },
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /.(js|jsx)$/,
18 | exclude: /node_modules/,
19 | use: {
20 | loader: 'babel-loader',
21 | options: {
22 | presets: ['@babel/preset-env', '@babel/preset-react'],
23 | },
24 | },
25 | },
26 | {
27 | test: /\.(ts|tsx)$/,
28 | exclude: /node_modules/,
29 | use: ['ts-loader'],
30 | },
31 | {
32 | test: /.(css|scss)$/,
33 | exclude: [/node_modules/],
34 | use: ['style-loader', 'css-loader', 'sass-loader'],
35 | },
36 | {
37 | test: /\.(png|jpe?g|gif)$/i,
38 | use: [
39 | {
40 | loader: 'file-loader',
41 | },
42 | ],
43 | },
44 | ],
45 | },
46 | plugins: [
47 | //removes all files under output.path and all unused assets after every successful rebuild
48 | // new CleanWebpackPlugin(),
49 | // we want our bundle.js file to be loaded into an HTMl file
50 | new HtmlWebpackPlugin({
51 | //where to inject the bundle.js file
52 | template: './client/index.html',
53 | }),
54 | // generates a manifest.json file
55 | // new WebpackManifestPlugin({
56 | // fileName: 'manifest.json',
57 | // }),
58 | ],
59 | devServer: {
60 | host: 'localhost',
61 | port: 8000,
62 | hot: false,
63 | liveReload: false,
64 | historyApiFallback: true,
65 | static: {
66 | // what the public sees so they dont know the path
67 | publicPath: '/build',
68 | directory: path.join(__dirname, './build'),
69 | },
70 | proxy: {
71 | //TODO: change to route that you need
72 | '/api': {
73 | target: 'http://localhost:3000',
74 | secure: false,
75 | },
76 | },
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/yamls/Daemonset.yaml:
--------------------------------------------------------------------------------
1 | --- # Daemonset.yaml
2 | apiVersion: apps/v1
3 | kind: DaemonSet
4 | metadata:
5 | name: promtail-daemonset
6 | spec:
7 | selector:
8 | matchLabels:
9 | name: promtail
10 | template:
11 | metadata:
12 | labels:
13 | name: promtail
14 | spec:
15 | serviceAccount: promtail-serviceaccount
16 | containers:
17 | - name: promtail-container
18 | image: grafana/promtail
19 | args:
20 | - -config.file=/etc/promtail/promtail.yaml
21 | env:
22 | - name: 'HOSTNAME' # needed when using kubernetes_sd_configs
23 | valueFrom:
24 | fieldRef:
25 | fieldPath: 'spec.nodeName'
26 | volumeMounts:
27 | - name: logs
28 | mountPath: /var/log
29 | - name: promtail-config
30 | mountPath: /etc/promtail
31 | - mountPath: /var/lib/docker/containers
32 | name: varlibdockercontainers
33 | readOnly: true
34 | volumes:
35 | - name: logs
36 | hostPath:
37 | path: /var/log
38 | - name: varlibdockercontainers
39 | hostPath:
40 | path: /var/lib/docker/containers
41 | - name: promtail-config
42 | configMap:
43 | name: promtail-config
44 | --- # configmap.yaml
45 | apiVersion: v1
46 | kind: ConfigMap
47 | metadata:
48 | name: promtail-config
49 | data:
50 | promtail.yaml: |
51 | server:
52 | http_listen_port: 9080
53 | grpc_listen_port: 0
54 |
55 | clients:
56 | - url: https://loki-gateway.loki.svc.cluster.local/loki/api/v1/push
57 |
58 | positions:
59 | filename: /tmp/positions.yaml
60 | target_config:
61 | sync_period: 10s
62 | scrape_configs:
63 | - job_name: pod-logs
64 | kubernetes_sd_configs:
65 | - role: pod
66 | pipeline_stages:
67 | - docker: {}
68 | relabel_configs:
69 | - source_labels:
70 | - __meta_kubernetes_pod_node_name
71 | target_label: __host__
72 | - action: labelmap
73 | regex: __meta_kubernetes_pod_label_(.+)
74 | - action: replace
75 | replacement: $1
76 | separator: /
77 | source_labels:
78 | - __meta_kubernetes_namespace
79 | - __meta_kubernetes_pod_name
80 | target_label: job
81 | - action: replace
82 | source_labels:
83 | - __meta_kubernetes_namespace
84 | target_label: namespace
85 | - action: replace
86 | source_labels:
87 | - __meta_kubernetes_pod_name
88 | target_label: pod
89 | - action: replace
90 | source_labels:
91 | - __meta_kubernetes_pod_container_name
92 | target_label: container
93 | - replacement: /var/log/pods/*$1/*.log
94 | separator: /
95 | source_labels:
96 | - __meta_kubernetes_pod_uid
97 | - __meta_kubernetes_pod_container_name
98 | target_label: __path__
99 |
100 | --- # Clusterrole.yaml
101 | apiVersion: rbac.authorization.k8s.io/v1
102 | kind: ClusterRole
103 | metadata:
104 | name: promtail-clusterrole
105 | rules:
106 | - apiGroups: [""]
107 | resources:
108 | - nodes
109 | - services
110 | - pods
111 | verbs:
112 | - get
113 | - watch
114 | - list
115 |
116 | --- # ServiceAccount.yaml
117 | apiVersion: v1
118 | kind: ServiceAccount
119 | metadata:
120 | name: promtail-serviceaccount
121 |
122 | --- # Rolebinding.yaml
123 | apiVersion: rbac.authorization.k8s.io/v1
124 | kind: ClusterRoleBinding
125 | metadata:
126 | name: promtail-clusterrolebinding
127 | subjects:
128 | - kind: ServiceAccount
129 | name: promtail-serviceaccount
130 | namespace: default
131 | roleRef:
132 | kind: ClusterRole
133 | name: promtail-clusterrole
134 | apiGroup: rbac.authorization.k8s.io
135 |
--------------------------------------------------------------------------------
/yamls/values.yml:
--------------------------------------------------------------------------------
1 | minio:
2 | enabled: true
--------------------------------------------------------------------------------