├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── App.tsx
├── Components
│ └── namespace
│ │ ├── Namespace.tsx
│ │ └── namespaceSlice.ts
├── Containers
│ └── SidebarContainer.tsx
├── assets
│ ├── LogeWhiteBG.png
│ ├── MaestroSmall.png
│ ├── Node-pod-overview.gif
│ ├── homepage-logs3.gif
│ ├── logo-cube.png
│ ├── maestro-logo-white.ai
│ ├── maestro-logo-white.png
│ └── metrics-page.gif
├── global.d.ts
├── home
│ ├── Components
│ │ ├── AlertCard.tsx
│ │ ├── EventCard.tsx
│ │ ├── HomeGraphCard.tsx
│ │ ├── LineChartTemplate.tsx
│ │ ├── LogCard.tsx
│ │ ├── StatusBubble.tsx
│ │ ├── StatusCard.tsx
│ │ └── utils
│ │ │ ├── GraphColors.tsx
│ │ │ └── customSelectTheme.tsx
│ └── Containers
│ │ ├── BubblesContainer.tsx
│ │ ├── DashboardContainer.tsx
│ │ ├── EventsCardContainer.tsx
│ │ ├── EventsContainer.tsx
│ │ ├── HomeGraphContainer.tsx
│ │ ├── MainContainer.tsx
│ │ ├── OverviewContainer.tsx
│ │ ├── StatusContainer.tsx
│ │ └── utils
│ │ └── edgeCaseHandling.tsx
├── index.html
├── index.tsx
├── metrics
│ └── Container
│ │ ├── GraphContainer.tsx
│ │ └── MetricsContainer.tsx
├── state
│ ├── hooks.ts
│ └── store.ts
├── styles
│ └── index.css
└── types.ts
├── electron
├── dataController
│ ├── formatData
│ │ ├── formatAlerts.ts
│ │ ├── formatEvents.ts
│ │ ├── formatMatrixData.ts
│ │ └── formatk8sApiData.ts
│ └── getData
│ │ └── getMatrixData.ts
├── main.ts
├── preload.ts
└── utils.ts
├── package-lock.json
├── package.json
├── tsconfig.json
└── webpack.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "airbnb",
8 | "airbnb-typescript",
9 | "eslint:recommended",
10 | "plugin:react/recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "jsx": true
17 | },
18 | "ecmaVersion": "latest",
19 | "sourceType": "module"
20 | },
21 | "plugins": [
22 | "react",
23 | "@typescript-eslint"
24 | ],
25 | "rules": {
26 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }]
27 | },
28 | "settings": {
29 | "import/resolver": {
30 | "node": {
31 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # MAESTRO
4 |
5 | A Kubernetes monitoring tool built on electron.
6 |
7 | ## Summary
8 |
9 | Maestro is an open-source monitoring tool for keeping track of the health of your Kubernetes cluster. Maestro is lightweight and allows users to view key metrics at a glance. This tool leverages the K8s API to obtain important cluster data, and promQL queries to scrape key metrics and display them in a digestible format.
10 |
11 | ## Features
12 |
13 | 1. At a glance overview of nodes, pods, services, and deployments visualized as cubes. Hover over any cube to see which node, pod, deployment, or service you are looking at.
14 |
15 | 
16 |
17 | 2. Log GUI to quickly view alerts and events in an easy to read format, with the ability to sort by severity.
18 |
19 | 
20 |
21 | 3. Graphs displaying key metrics such at CPU usage, memory usage, and network I/O. Multi-colored line graphs map to corresponding properties in the legend, where you can remove properties tht you want to get a more narrowly focused graph.
22 |
23 | 
24 |
25 | ## Getting Started
26 |
27 | ### 1. Prerequisites
28 | Users must have Prometheus installed on their Kubernetes cluster.
29 |
30 | ### 2. Clone this repo using the following command
31 |
32 | ```
33 | https://github.com/oslabs-beta/maestro.git
34 | ```
35 |
36 | ### 3. Make sure your cluster is ported forward to port 9090 using the following command
37 |
38 | ```
39 | kubectl port-forward -n default svc/prometheus-kube-prometheus-prometheus 9090
40 | ```
41 |
42 | ### 4. In the Maestro directory in your terminal, run the following commands
43 |
44 | ```
45 | npm install
46 | npm run webpack-start
47 | npm run start
48 | ```
49 |
50 | ### 5. Enjoy your Maestro experience!
51 |
52 | ## Built With
53 |
54 | - [Electron](https://www.electronjs.org/)
55 | - [React](https://reactjs.org/)
56 | - [React Router](https://reactrouter.com/)
57 | - [Redux](https://redux.js.org/)
58 | - [Node](https://nodejs.org/)
59 | - [Kubernetes-client](https://github.com/kubernetes-client/)
60 | - [Prometheus](https://prometheus.io/)
61 | - [Material UI](https://mui.com/)
62 | - [Chart.js](https://www.chartjs.org/)
63 |
64 | ## The Team
65 |
66 | - Peter Kennedy [Github](https://github.com/peterkennedy97) [LinkedIn](https://www.linkedin.com/in/peter-kennedy/)
67 | - Aliya Yano [Github](https://github.com/ajyano22) [LinkedIn](https://www.linkedin.com/in/aliya-yano-8a2548126/)
68 | - Jakob Schillinger [Github](https://github.com/gandalf77)
69 | - Alex Ryu [Github](https://github.com/RyuBoyCoding) [LinkedIn](www.linkedin.com/in/ryu-alex)
70 |
--------------------------------------------------------------------------------
/app/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardContainer from "./home/Containers/DashboardContainer";
3 | import "./styles/index.css";
4 |
5 | function App() {
6 | return (
7 |
8 | );
9 | }
10 |
11 | export default App;
12 |
--------------------------------------------------------------------------------
/app/Components/namespace/Namespace.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from "react";
2 | import { useAppDispatch } from "../../state/hooks";
3 | import { setCurrentNamespace } from "./namespaceSlice";
4 | import { getNamespacesForState } from "./namespaceSlice";
5 | import Select from "react-select";
6 | import { customSelectStyles } from "../../home/Components/utils/customSelectTheme";
7 | import { customSelectThemeNamespaces } from "../../home/Components/utils/customSelectTheme";
8 |
9 | const Namespace: React.FC = () => {
10 | const dispatch = useAppDispatch();
11 | const [namespaces, setNamespaces] = useState([]);
12 |
13 | const initApp = useCallback(async () => {
14 | await dispatch(getNamespacesForState());
15 | }, [dispatch]);
16 |
17 | useEffect(() => {
18 | initApp();
19 | getNamespaces();
20 | }, []);
21 |
22 | const getNamespaces = async () => {
23 | const namespaces = await window.electron.getNamespacesList();
24 | setNamespaces(namespaces);
25 | };
26 |
27 | const options: any = namespaces.map((el: string) => {
28 | return { value: el, label: el };
29 | });
30 |
31 | const handleNamespaceChange = (e: any) =>
32 | dispatch(setCurrentNamespace(e.value));
33 |
34 | return (
35 |
36 |
45 |
46 | );
47 | };
48 |
49 | export default Namespace;
50 |
--------------------------------------------------------------------------------
/app/Components/namespace/namespaceSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
2 |
3 | interface NamespaceState {
4 | currentNamespace: string,
5 | allNamespaces: string[]
6 | }
7 |
8 | const initialState: NamespaceState = {
9 | currentNamespace: '',
10 | allNamespaces: []
11 | };
12 |
13 | export const getNamespacesForState = createAsyncThunk(
14 | 'namespace/getNamespaces',
15 | async (_, thunkAPI) => {
16 | const res = await window.electron.getNamespacesList()
17 | return res
18 | }
19 | );
20 |
21 | export const namespaceSlice = createSlice({
22 | name: 'namespace',
23 | initialState,
24 | reducers: {
25 | setCurrentNamespace: (state:any, action: PayloadAction) => {
26 | state.currentNamespace = action.payload
27 | }
28 | },
29 | extraReducers: (builder) => {
30 | builder.addCase(getNamespacesForState.fulfilled, (state, action) => {
31 | state.allNamespaces = action.payload
32 | })
33 | builder.addCase(getNamespacesForState.pending, (state, action) => {
34 | state.allNamespaces = action.payload
35 | })
36 | builder.addCase(getNamespacesForState.rejected, (state:any, action) => {
37 | state.error = action.error.message
38 | })
39 | }
40 | });
41 |
42 | export const { setCurrentNamespace } = namespaceSlice.actions;
43 | export default namespaceSlice.reducer;
44 |
45 |
--------------------------------------------------------------------------------
/app/Containers/SidebarContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import { MdHome, MdOutlineQueryStats } from "react-icons/md";
4 | import { VscGraphLine } from "react-icons/vsc";
5 |
6 | const SidebarContainer = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default SidebarContainer;
25 |
--------------------------------------------------------------------------------
/app/assets/LogeWhiteBG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/LogeWhiteBG.png
--------------------------------------------------------------------------------
/app/assets/MaestroSmall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/MaestroSmall.png
--------------------------------------------------------------------------------
/app/assets/Node-pod-overview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/Node-pod-overview.gif
--------------------------------------------------------------------------------
/app/assets/homepage-logs3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/homepage-logs3.gif
--------------------------------------------------------------------------------
/app/assets/logo-cube.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/logo-cube.png
--------------------------------------------------------------------------------
/app/assets/maestro-logo-white.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/maestro-logo-white.ai
--------------------------------------------------------------------------------
/app/assets/maestro-logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/maestro-logo-white.png
--------------------------------------------------------------------------------
/app/assets/metrics-page.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/maestro/ad97da9f46c22bdca5e1fbd3b3cd611a6ece79d7/app/assets/metrics-page.gif
--------------------------------------------------------------------------------
/app/global.d.ts:
--------------------------------------------------------------------------------
1 | import { fetchData } from './types';
2 | declare global {
3 | /**
4 | * We define all IPC APIs here to give devs auto-complete
5 | * use window.electron anywhere in app
6 | * Also note the capital "Window" here
7 | */
8 | interface Window {
9 | electron: {
10 | getAlerts: () => Promise;
11 | getEvents: () => Promise;
12 | getCPUUsageByNode: (namespace: string) => Promise
13 | getMemoryUsageByNode: (namespace: string) => Promise
14 | bytesRecievedByNode: (namespace: string) => Promise
15 | bytesTransmittedByNode: (namespace: string) => Promise
16 | getCPUUsageByNamespace: (namespace: string) => Promise
17 | getMemoryUsageByNamespace: (namespace: string) => Promise
18 | bytesRecievedByNamespace: (namespace: string) => Promise
19 | bytesTransmittedByNamespace: (namespace: string) => Promise
20 | getCPUUsageByPod: (namespace: string) => Promise
21 | getMemoryUsageByPod: (namespace: string) => Promise
22 | bytesRecievedByPod: (namespace: string) => Promise
23 | bytesTransmittedByPod: (namespace: string) => Promise
24 | getNodesList: () => Promise
25 | getNamespacesList: () => Promise
26 | getDeploymentsList: () => Promise
27 | getServicesList: () => Promise
28 | getPodsList: () => Promise
29 | };
30 | }
31 | }
--------------------------------------------------------------------------------
/app/home/Components/AlertCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface AlertCardType {
4 | group: any,
5 | state: any,
6 | name: any,
7 | severity: any,
8 | description: any,
9 | summary: any,
10 | // alerts: any
11 | }
12 |
13 | function AlertCard({
14 | group, state, name, severity, description, summary,
15 | }: AlertCardType): JSX.Element {
16 | return (
17 |
18 |
19 |
Severity:
20 |
{severity[0].toUpperCase() + severity.slice(1)}
21 |
22 |
23 |
State:
24 |
{state[0].toUpperCase() + state.slice(1)}
25 |
26 |
27 |
Group:
28 |
{group}
29 |
30 |
31 |
Name:
32 |
{name}
33 |
34 |
35 |
Description:
36 |
{description}
37 |
38 |
39 |
Summary:
40 |
{summary}
41 |
42 | {/*
43 |
Alerts:
44 |
{alerts}
45 |
*/}
46 |
47 | );
48 | }
49 |
50 | export default AlertCard;
51 |
--------------------------------------------------------------------------------
/app/home/Components/EventCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface EventCardType {
4 | last_seen: string,
5 | message: string,
6 | object: string,
7 | reason: string,
8 | severity: string,
9 | }
10 |
11 | function EventCard({
12 | last_seen, message, object, reason, severity,
13 | }: EventCardType): JSX.Element {
14 | return (
15 |
16 |
17 |
Severity:
18 |
{severity}
19 |
20 |
21 |
Reason:
22 |
{reason}
23 |
24 |
25 |
Message:
26 |
{message}
27 |
28 |
29 |
Object:
30 |
{object}
31 |
32 |
33 |
Last Seen:
34 |
{last_seen}
35 |
36 |
37 | );
38 | }
39 |
40 | export default EventCard;
41 |
--------------------------------------------------------------------------------
/app/home/Components/HomeGraphCard.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React, { useState, useEffect } from 'react';
3 | import { useAppSelector } from '../../state/hooks';
4 | import LineChart from './LineChartTemplate';
5 |
6 | interface HomeGraphCardType {
7 | type: any,
8 | source: any,
9 | }
10 |
11 | function HomeGraphCard({ type, source }: HomeGraphCardType): JSX.Element {
12 | const [graphData, setGraphData] = useState([]);
13 | let namespace: string = useAppSelector((state) => state.namespace.currentNamespace);
14 |
15 | // conditional rendering based on type and source options
16 | const getData = async (): Promise => {
17 | if (!namespace) namespace = 'default';
18 |
19 | // namespace metrics
20 | if (source === 'Namespace') {
21 | if (type === 'Memory') {
22 | const getMemoryUsageByNamespace = await window.electron.getMemoryUsageByNamespace(namespace);
23 | const data = Object.entries(getMemoryUsageByNamespace);
24 | setGraphData(data);
25 | } else if (type === 'CPU') {
26 | const getCPUUsageByNamespace = await window.electron.getCPUUsageByNamespace(namespace);
27 | const data = Object.entries(getCPUUsageByNamespace);
28 | setGraphData(data);
29 | } else if (type === 'Bytes') {
30 | const bytesRecievedByNamespace = await window.electron.bytesRecievedByNamespace(namespace);
31 | const data = Object.entries(bytesRecievedByNamespace);
32 | setGraphData(data);
33 | }
34 | } else if (source === 'Nodes') {
35 | if (type === 'Memory') {
36 | const getMemoryUsageByNode = await window.electron.getMemoryUsageByNode(namespace);
37 | const data = Object.entries(getMemoryUsageByNode);
38 | setGraphData(data);
39 | } else if (type === 'CPU') {
40 | const getCPUUsageByNode = await window.electron.getCPUUsageByNode(namespace);
41 | const data = Object.entries(getCPUUsageByNode);
42 | setGraphData(data);
43 | } else if (type === 'Bytes') {
44 | const bytesRecievedByNode = await window.electron.bytesTransmittedByNode(namespace);
45 | const data = Object.entries(bytesRecievedByNode);
46 | setGraphData(data);
47 | }
48 | } else if (source === 'Pods') {
49 | if (type === 'Memory') {
50 | const getMemoryUsageByPod = await window.electron.getMemoryUsageByPod(namespace);
51 | const data = Object.entries(getMemoryUsageByPod);
52 | setGraphData(data);
53 | } else if (type === 'CPU') {
54 | const getCPUUsageByPod = await window.electron.getCPUUsageByPod(namespace);
55 | const data = Object.entries(getCPUUsageByPod);
56 | setGraphData(data);
57 | } else if (type === 'Bytes') {
58 | const bytesRecievedByPod = await window.electron.bytesRecievedByPod(namespace);
59 | const data = Object.entries(bytesRecievedByPod);
60 | setGraphData(data);
61 | }
62 | }
63 | };
64 |
65 | useEffect(() => {
66 | getData();
67 | }, [type, source, namespace]);
68 |
69 | return (
70 | <>
71 |
72 | {`${type[0].toUpperCase() + type.slice(1)} Usage by ${source}`}
73 |
74 |
77 | >
78 | );
79 | }
80 |
81 | export default HomeGraphCard;
82 |
--------------------------------------------------------------------------------
/app/home/Components/LineChartTemplate.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-plusplus */
2 | import React, { useState } from 'react';
3 | import {
4 | Chart as ChartJS,
5 | CategoryScale,
6 | LinearScale,
7 | PointElement,
8 | LineElement,
9 | Title,
10 | Tooltip,
11 | Legend,
12 | } from 'chart.js';
13 | import { Line } from 'react-chartjs-2';
14 | import { Button } from '@mui/material';
15 | import mdColors from './utils/GraphColors';
16 |
17 | ChartJS.register(
18 | CategoryScale,
19 | LinearScale,
20 | PointElement,
21 | LineElement,
22 | Title,
23 | Tooltip,
24 | Legend,
25 | );
26 |
27 | interface LineChartType {
28 | chartData: any,
29 | }
30 |
31 | function LineChart({ chartData }: LineChartType): JSX.Element {
32 | // React hooks for collapsing/expanding legend
33 | const [buttonClicked, setButtonClicked] = useState(false);
34 |
35 | const options: any = {
36 | responsive: true,
37 | pointRadius: 0,
38 | indexAxis: 'x',
39 | plugins: {
40 | legend: {
41 | display: buttonClicked,
42 | position: 'bottom' as const,
43 | },
44 | datalabels: {
45 | // hide datalabels for all datasets
46 | display: false,
47 | },
48 | },
49 | scales: {
50 | x: {
51 | grid: {
52 | color: 'rgb(240, 240, 240)',
53 | },
54 | ticks: {
55 | color: '#797676',
56 | },
57 | },
58 | y: {
59 | grid: {
60 | color: 'rgb(240, 240, 240)',
61 | },
62 | ticks: {
63 | color: '#797676',
64 | },
65 | },
66 | },
67 | };
68 |
69 | // if chart data is empty render "No data available"
70 | if (!chartData) return No data available in
;
71 |
72 | // Format chart data for line chart with varying colors
73 | const objArr: any = [];
74 | const zeroPad: any = (num: number, places: number) => String(num).padStart(places, '0');
75 | const now: Date = new Date();
76 | const nowHours = now.getHours();
77 | const nowMinutes = now.getMinutes();
78 | const times = [];
79 | for (let i = 1; i <= 12; i++) {
80 | times.push(`${zeroPad((nowHours + i * 2) % 24, 2)}:${zeroPad(nowMinutes, 2)}`);
81 | }
82 |
83 | for (let i = 0; i < chartData.length; i++) {
84 | const colors: any = mdColors;
85 | objArr.push({
86 | data: chartData[i][1].timeSeriesValues,
87 | label: chartData[i][0],
88 | borderColor: `${colors[(i * 3) % mdColors.length]}`,
89 | });
90 | }
91 |
92 | const data: any = {
93 | labels: times,
94 | datasets: objArr,
95 | };
96 |
97 | // Collapse or expand legend
98 | const handleLegendClick = () => {
99 | setButtonClicked((prevCheck) => !prevCheck);
100 | };
101 |
102 | return (
103 |
104 |
105 |
113 |
114 | );
115 | }
116 |
117 | export default LineChart;
118 |
--------------------------------------------------------------------------------
/app/home/Components/LogCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function LogCard() {
4 | return (
5 | LogCard
6 | );
7 | }
8 |
9 | export default LogCard;
10 |
--------------------------------------------------------------------------------
/app/home/Components/StatusBubble.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Box from '@mui/material/Box';
3 | import Tooltip from '@mui/material/Tooltip';
4 |
5 | interface StatusBubbleProps {
6 | name?: any,
7 | status?: any,
8 | type?: any,
9 | }
10 |
11 | const statusBubble = ({ name, status, type }: StatusBubbleProps): JSX.Element => {
12 | const [statusColor, setStatusColor] = useState('#3371e3');
13 | if (type === 'nodes') {
14 | // add node status handling
15 | }
16 |
17 | if (type === 'pods') {
18 | if (status !== 'Running') {
19 | const color = '#e3d733';
20 | setStatusColor(color);
21 | }
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default statusBubble;
34 |
--------------------------------------------------------------------------------
/app/home/Components/StatusCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface StatusDataProps {
4 | name: string,
5 | quantity: number,
6 | boxes: any,
7 | }
8 |
9 | function StatusCard({ name, quantity, boxes }: StatusDataProps): JSX.Element {
10 | return (
11 |
12 |
{name}
13 |
{boxes}
14 |
{quantity}
15 |
16 | );
17 | }
18 |
19 | export default StatusCard;
20 |
--------------------------------------------------------------------------------
/app/home/Components/utils/GraphColors.tsx:
--------------------------------------------------------------------------------
1 | const mdColors = [
2 | '#3371e3',
3 | '#333',
4 | '#797676',
5 | '#20c997',
6 | // '#ed6a5a',
7 | // '#20c997',
8 | // '#797676',
9 | // '#3371e3',
10 | ]
11 |
12 | export default mdColors;
--------------------------------------------------------------------------------
/app/home/Components/utils/customSelectTheme.tsx:
--------------------------------------------------------------------------------
1 | export function customSelectTheme(theme: any) {
2 | return {
3 | ...theme,
4 | color: '#3371e3',
5 | colors: {
6 | ...theme.colors,
7 | primary25: '#f1f6ff',
8 | primary: '#3371e3',
9 | neutral5:'#333',
10 | neutral10:'#333',
11 | neutral20:'#3371e3',
12 | neutral30:'#3371e3',
13 | neutral40:'#3371e3',
14 | neutral50:'#3371e3',
15 | neutral60:'#3371e3',
16 | neutral70:'#3371e3',
17 | neutral80:'#3371e3',
18 | neutral90:'#3371e3',
19 | },
20 | borderRadius: 5,
21 | }
22 | }
23 |
24 | export function customSelectThemeSeverity(theme: any) {
25 | return {
26 | ...theme,
27 | colors: {
28 | ...theme.colors,
29 | primary25: '#f1f6ff',
30 | primary: '#ed6a5a',
31 | neutral5: '#ed6a5a',
32 | neutral10: '#ed6a5a',
33 | neutral20: '#ed6a5a',
34 | neutral30: '#ed6a5a',
35 | neutral40: '#ed6a5a',
36 | neutral50: '#ed6a5a',
37 | neutral60: '#ed6a5a',
38 | neutral70: '#ed6a5a',
39 | neutral80: '#ed6a5a',
40 | neutral90: '#ed6a5a',
41 | },
42 | }
43 | }
44 |
45 |
46 |
47 | export function customSelectThemeNamespaces(theme: any) {
48 | return {
49 | ...theme,
50 | colors: {
51 | ...theme.colors,
52 | primary25: '#f1f6ff',
53 | primary: '#333',
54 | neutral5: '#333',
55 | neutral10: '#333',
56 | neutral20: '#333',
57 | neutral30: '#333',
58 | neutral40: '#333',
59 | neutral50: '#333',
60 | neutral60: '#333',
61 | neutral70: '#333',
62 | neutral80: '#333',
63 | neutral90: '#333',
64 | },
65 | }
66 | }
67 |
68 | export const customSelectStyles = {
69 | control: (base: any, state: any) => ({
70 | ...base,
71 | background: '#20C997',
72 | borderColor: '#20C997',
73 | // Removes weird border around container
74 | // boxShadow: state.isFocused ? null : null,
75 | "&:hover": {
76 | // Overwrittes the different states of border
77 | borderColor: state.isFocused ? '#333' : '#20C997'
78 | }
79 | }),
80 | placeholder: (defaultStyles: any) => {
81 | return {
82 | ...defaultStyles,
83 | color: '#333',
84 | }
85 | },
86 | menu: (provided: any, state: any) => ({
87 | ...provided,
88 | width: '100%',
89 | // borderBottom: '1px dotted pink',
90 | colors: {
91 | primary25: '#f1f6ff',
92 | primary: '#333',
93 | neutral120: '#333',
94 | neutral150: '#333',
95 | neutral180: '#333',
96 | },
97 | // backgroundColor: 'red'
98 | }),
99 | }
--------------------------------------------------------------------------------
/app/home/Containers/BubblesContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import StatusCard from '../Components/StatusCard';
3 | import StatusBubble from '../Components/StatusBubble';
4 | import { useAppSelector } from '../../state/hooks';
5 |
6 | const StatusContainer = () => {
7 | let namespace: string = useAppSelector(state => state.namespace.currentNamespace);
8 | const [nodeQuant, setNodeQuant] = useState();
9 | const [deploymentQuant, setDeploymentQuant] = useState();
10 | const [podQuant, setPodQuant] = useState();
11 | const [servicesQuant, setServicesQuant] = useState();
12 | const [nodeBoxes, setNodeBoxes] = useState([]);
13 | const [deploymentBoxes, setDeploymentBoxes] = useState([]);
14 | const [podBoxes, setPodBoxes] = useState([]);
15 | const [servicesBoxes, setServicesBoxes] = useState([]);
16 |
17 | const getNodesForState = async (): Promise => {
18 | const nodesList: any = await window.electron.getNodesList();
19 | setNodeQuant(nodesList.length);
20 |
21 | let boxesArr =[];
22 | for(let i =0; i < nodesList.length; i++){
23 | boxesArr.push(
24 |
30 | );
31 | }
32 | setNodeBoxes(boxesArr);
33 | }
34 |
35 | const getDeploymentsForState = async (): Promise => {
36 | const deploymentsList: any = await window.electron.getDeploymentsList();
37 | const deploymentsListByNamespace = deploymentsList.filter((deployment: any) => {
38 | return deployment.namespace === namespace;
39 | });
40 | setDeploymentQuant(deploymentsListByNamespace.length);
41 |
42 | let boxesArr =[];
43 | for(let i =0; i < deploymentsListByNamespace.length; i++){
44 | boxesArr.push(
45 |
50 | );
51 | }
52 | setDeploymentBoxes(boxesArr);
53 | };
54 |
55 | const getPodsForState = async (): Promise => {
56 | const podsList: any = await window.electron.getPodsList();
57 | const podsListByNamespace = podsList.filter((pod: any) => {
58 | return pod.namespace === namespace;
59 | })
60 |
61 | let boxesArr =[];
62 | setPodQuant(podsListByNamespace.length);
63 | for(let i =0; i < podsListByNamespace.length; i++){
64 | boxesArr.push(
65 |
71 | );
72 | }
73 | setPodBoxes(boxesArr);
74 | };
75 |
76 | const getServicesForState = async (): Promise => {
77 | const servicesList: any = await window.electron.getServicesList();
78 | const servicesListByNamespace = servicesList.filter((service: any) => {
79 | return service.namespace === namespace;
80 | })
81 |
82 | let boxesArr =[];
83 | setServicesQuant(servicesListByNamespace.length);
84 | for(let i =0; i < servicesListByNamespace.length; i++){
85 | boxesArr.push(
86 |
91 | );
92 | }
93 | setServicesBoxes(boxesArr);
94 | };
95 |
96 | useEffect(() => {
97 | if (namespace === '') namespace = 'default';
98 | getNodesForState();
99 | getDeploymentsForState();
100 | getPodsForState();
101 | getServicesForState();
102 | }, [namespace]);
103 |
104 | return (
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default StatusContainer;
--------------------------------------------------------------------------------
/app/home/Containers/DashboardContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router } from "react-router-dom";
3 | import Namespace from "../../Components/namespace/Namespace";
4 | import SidebarContainer from "../../Containers/SidebarContainer";
5 | import MainContainer from "./MainContainer";
6 |
7 | const DashboardContainer = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
MAESTRO
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default DashboardContainer;
--------------------------------------------------------------------------------
/app/home/Containers/EventsCardContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import AlertCard from '../Components/AlertCard';
3 | import EventCard from '../Components/EventCard';
4 | import { useAppSelector } from '../../state/hooks';
5 | import { noEventsAvailable, noAlertsAvailable } from './utils/edgeCaseHandling';
6 |
7 |
8 | const EventsCardContainer = (props: any) => {
9 | let namespace: string = useAppSelector(state => state.namespace.currentNamespace)
10 | const [alertsBySeverity, setAlertsBySeverity] = useState([])
11 | const [namespaceEventsBySeverity, setNamespaceEventsBySeverity] = useState([])
12 |
13 | const renderThis = async (): Promise => {
14 | if (namespace === '') namespace = 'default';
15 |
16 | // logic for alerts
17 | let allAlerts: any = await window.electron.getAlerts();
18 |
19 | if ((props.severity).toLowerCase() !== 'all' && allAlerts.length !== 0) {
20 | let alertsBySeverity = allAlerts.reduce((acc:any, el:any) => {
21 | if ((el.severity).toLowerCase() === props.severity) acc.push(el);
22 | return acc;
23 | }, [])
24 | setAlertsBySeverity(alertsBySeverity);
25 | }
26 |
27 | else {
28 | if (!allAlerts.length) allAlerts = [noAlertsAvailable];
29 | setAlertsBySeverity(allAlerts);
30 | }
31 |
32 | // logic for events
33 | const allEvents: any = await window.electron.getEvents();
34 | let namespaceEvents = allEvents.reduce((acc: any, el: any) => {
35 | if (el.namespace === namespace) acc.push(el);
36 | return acc;
37 | }, []);
38 |
39 | if ((props.severity).toLowerCase() !== 'all' && namespaceEvents.length !== 0) {
40 | let namespaceEventsBySeverity = namespaceEvents.reduce((acc:any, el:any) => {
41 | if ((el.type).toLowerCase() === props.severity) acc.push(el);
42 | return acc;
43 | }, [])
44 | setNamespaceEventsBySeverity(namespaceEventsBySeverity);
45 | }
46 |
47 | else {
48 | if (!namespaceEvents.length) namespaceEvents = [noEventsAvailable];
49 | setNamespaceEventsBySeverity(namespaceEvents);
50 | }
51 | }
52 |
53 | useEffect(() => {
54 | renderThis();
55 | }, []);
56 |
57 | useEffect(() => {
58 | renderThis();
59 | }, [props, namespace]);
60 |
61 | const alertsCard: any = alertsBySeverity.map((el: any, i: number) =>
62 |
72 | );
73 |
74 | const namespaceEventCards: any = namespaceEventsBySeverity.map((el: any, i: number) =>
75 |
83 | );
84 |
85 | return (
86 |
87 | {props.eventType === 'alerts' && alertsCard}
88 | {props.eventType === 'events' && namespaceEventCards}
89 |
90 | );
91 | };
92 |
93 | export default EventsCardContainer;
--------------------------------------------------------------------------------
/app/home/Containers/EventsContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import EventsCardContainer from './EventsCardContainer';
3 | import Select from 'react-select';
4 | import { customSelectTheme, customSelectThemeSeverity } from '../Components/utils/customSelectTheme';
5 |
6 | const eventTypes: any = [
7 | {value:'events', label: 'Events'},
8 | {value:'alerts', label:'Alerts'},
9 | ];
10 |
11 | const severityTypes: any = [
12 | {value:'critical', label:'Critical'},
13 | {value:'warning', label: 'Warning'},
14 | {value:'normal', label: 'Normal'},
15 | {value:'all', label: 'All'}
16 | ];
17 |
18 | function EventsContainer() {
19 | const [eventOption, setEventOption] = useState('events');
20 | const [severityOption, setSeverityOption] = useState('all');
21 |
22 | const handleEventChange = (e: any) => {
23 | setEventOption(e.value);
24 | };
25 |
26 | const handleSeverityChange = (e: any) => {
27 | setSeverityOption(e.value);
28 | };
29 |
30 | return (
31 |
32 |
33 |
41 |
49 |
50 |
54 |
55 | );
56 | };
57 |
58 | export default EventsContainer;
--------------------------------------------------------------------------------
/app/home/Containers/HomeGraphContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import HomeGraphCard from '../Components/HomeGraphCard';
3 | import Select from 'react-select';
4 | import { customSelectTheme } from '../Components/utils/customSelectTheme';
5 |
6 | function HomeGraphContainer() {
7 | const dataOptions: any = [
8 | {value:'Memory', label:'Memory'},
9 | {value:'CPU', label: 'CPU'},
10 | {value:'Bytes', label: 'Bytes'}
11 | ];
12 |
13 | const sourceOptions: any = [
14 | {value:'Namespace', label:'Namespace'},
15 | {value:'Nodes', label: 'Nodes'},
16 | {value:'Pods', label: 'Pods'}
17 | ];
18 |
19 | //state for chosen data type
20 | const [dataChoice, setDataChoice] = useState('Memory');
21 | //state for chosen source
22 | const [sourceChoice, setSourceChoice] = useState('Pods');
23 |
24 | const handleDataSelect = (e: any) =>{
25 | setDataChoice(e.value);
26 | };
27 |
28 | const handleSourceSelect = (e: any) =>{
29 | setSourceChoice(e.value);
30 | };
31 |
32 | return (
33 |
34 |
35 |
42 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default HomeGraphContainer;
--------------------------------------------------------------------------------
/app/home/Containers/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Routes, Route } from "react-router-dom";
3 | import MetricsContainer from "../../metrics/Container/MetricsContainer";
4 | import OverviewContainer from "./OverviewContainer";
5 |
6 | const MainContainer = () => {
7 | return (
8 |
9 |
10 | } />
11 | } />
12 |
13 |
14 | );
15 | };
16 |
17 | export default MainContainer;
18 |
--------------------------------------------------------------------------------
/app/home/Containers/OverviewContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import StatusContainer from "./StatusContainer";
3 | import EventsContainer from "./EventsContainer";
4 |
5 | const OverviewContainer = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default OverviewContainer;
--------------------------------------------------------------------------------
/app/home/Containers/StatusContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HomeGraphContainer from './/HomeGraphContainer';
3 | import BubblesContainer from './BubblesContainer';
4 |
5 | function StatusContainer() {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default StatusContainer;
--------------------------------------------------------------------------------
/app/home/Containers/utils/edgeCaseHandling.tsx:
--------------------------------------------------------------------------------
1 | // Shows a message if for the selected namespace there are no events available
2 | export const noEventsAvailable = {
3 | last_seen: '',
4 | message: '',
5 | object: '',
6 | reason: '',
7 | type: "There are no events for this namespace...",
8 | };
9 |
10 | // Shows a message if for the selected namespace there are no alerts available
11 | export const noAlertsAvailable = {
12 | group: '',
13 | state: '',
14 | name: '',
15 | severity: "There are no alerts for this namespace...",
16 | description: '',
17 | summary: '',
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Maestro
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 | import { Provider } from 'react-redux';
5 | import store from './state/store';
6 |
7 | const root = createRoot(document.getElementById('app'))
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/metrics/Container/GraphContainer.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React, { useState, useEffect } from 'react'
3 | import LineChart from '../../home/Components/LineChartTemplate'
4 | import { useAppSelector } from '../../state/hooks'
5 |
6 | function GraphContainer() {
7 | const namespace: string = useAppSelector(state => state.namespace.currentNamespace)
8 | const [memoryUsageByNode, setMemoryUsageByNode] = useState([]);
9 | const [memoryUsageByNamespace, setMemoryUsageByNamespace] = useState([]);
10 | const [memoryUsageByPod, setMemoryUsageByPod] = useState([]);
11 | const [cpuUsageByNode, setCpuUsageByNode] = useState([]);
12 | const [cpuUsageByNamespace, setCpuUsageByNamespace] = useState([]);
13 | const [cpuUsageByPod, setCpuUsageByPod] = useState([]);
14 | const [bytesRecievedByNode, setBytesRecievedByNode] = useState([]);
15 | const [bytesRecievedByNamespace, setBytesRecievedByNamespace] = useState([]);
16 | const [bytesRecievedByPod, setBytesRecievedByPod] = useState([]);
17 | const [bytesTransmittedByNamespace, setBytesTransmittedByNamespace] = useState([]);
18 | const [bytesTransmittedByNode, setBytesTransmittedByNode] = useState([]);
19 | const [bytesTransmittedByPod, setBytesTransmittedByPod] = useState([]);
20 |
21 | const setStateForData = async (namespace = 'default') => {
22 | const getCPUUsageByNode = await window.electron.getCPUUsageByNode(namespace);
23 | const getMemoryUsageByNode = await window.electron.getMemoryUsageByNode(namespace);
24 | const getbytesRecievedByNode = await window.electron.bytesRecievedByNode(namespace);
25 | const getbytesTransmittedByNode = await window.electron.bytesTransmittedByNode(namespace);
26 | const getCPUUsageByNamespace = await window.electron.getCPUUsageByNamespace(namespace);
27 | const getMemoryUsageByNamespace = await window.electron.getMemoryUsageByNamespace(namespace);
28 | const getbytesRecievedByNamespace = await window.electron.bytesRecievedByNamespace(namespace);
29 | const getbytesTransmittedByNamespace = await window.electron.bytesTransmittedByNamespace(namespace);
30 | const getCPUUsageByPod = await window.electron.getCPUUsageByPod(namespace);
31 | const getMemoryUsageByPod = await window.electron.getMemoryUsageByPod(namespace);
32 | const getbytesRecievedByPod = await window.electron.bytesRecievedByPod(namespace);
33 | const getbytesTransmittedByPod = await window.electron.bytesTransmittedByPod(namespace);
34 |
35 | setCpuUsageByNode(Object.entries(getCPUUsageByNode));
36 | setMemoryUsageByNode(Object.entries(getMemoryUsageByNode));
37 | setBytesRecievedByNode(Object.entries(getbytesRecievedByNode));
38 | setBytesTransmittedByNode(Object.entries(getbytesTransmittedByNode));
39 | setCpuUsageByNamespace(Object.entries(getCPUUsageByNamespace));
40 | setMemoryUsageByNamespace(Object.entries(getMemoryUsageByNamespace));
41 | setBytesRecievedByNamespace(Object.entries(getbytesRecievedByNamespace));
42 | setBytesTransmittedByNamespace(Object.entries(getbytesTransmittedByNamespace));
43 | setCpuUsageByPod(Object.entries(getCPUUsageByPod));
44 | setMemoryUsageByPod(Object.entries(getMemoryUsageByPod));
45 | setBytesRecievedByPod(Object.entries(getbytesRecievedByPod));
46 | setBytesTransmittedByPod(Object.entries(getbytesTransmittedByPod));
47 | };
48 |
49 | useEffect(() => {
50 | setStateForData(namespace)
51 | }, [namespace])
52 |
53 | return (
54 |
55 |
56 |
57 | CPU Usage by Node
58 |
59 |
60 |
61 |
62 |
63 | Memory Usage by Node
64 |
65 |
66 |
67 |
68 |
69 | Bytes Received by Node
70 |
71 |
72 |
73 |
74 |
75 | Bytes Transmitted by Node
76 |
77 |
78 |
79 |
80 |
81 | CPU Usage by Namespace
82 |
83 |
84 |
85 |
86 |
87 | Memory Usage by Namespace
88 |
89 |
90 |
91 |
92 |
93 | Bytes Received by Namespace
94 |
95 |
96 |
97 |
98 |
99 | Bytes Transmitted by Namespace
100 |
101 |
102 |
103 |
104 |
105 | CPU Usage by Pod
106 |
107 |
108 |
109 |
110 |
111 | Memory Usage by Pod
112 |
113 |
114 |
115 |
116 |
117 | Bytes Received by Pod
118 |
119 |
120 |
121 |
122 |
123 | Bytes Transmitted by Pod
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
131 | export default GraphContainer;
132 |
--------------------------------------------------------------------------------
/app/metrics/Container/MetricsContainer.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-filename-extension */
2 | import React from 'react';
3 | import GraphContainer from './GraphContainer';
4 |
5 | function MetricsContainer() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default MetricsContainer;
14 |
--------------------------------------------------------------------------------
/app/state/hooks.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
2 | import type { RootState, AppDispatch } from './store';
3 |
4 | // Use throughout app instead of plain `useDispatch` and `useSelector`
5 | export const useAppDispatch: () => AppDispatch = useDispatch;
6 | export const useAppSelector: TypedUseSelectorHook = useSelector;
7 |
--------------------------------------------------------------------------------
/app/state/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import namespaceReducer from '../Components/namespace/namespaceSlice';
3 |
4 | const store = configureStore({
5 | reducer: {
6 | namespace: namespaceReducer,
7 | },
8 | });
9 |
10 | export type RootState = ReturnType;
11 | export type AppDispatch = typeof store.dispatch;
12 | export default store;
13 |
--------------------------------------------------------------------------------
/app/styles/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0 auto;
3 | text-align: center;
4 | font-family: "Roboto", sans-serif;
5 | background-color: rgb(255, 255, 255);
6 | }
7 |
8 | .main {
9 | margin-top: 70px;
10 | width: 100%;
11 | height: 100vh;
12 | overflow-y: scroll;
13 | }
14 |
15 | .home-container {
16 | background-color: #20C997;
17 | }
18 |
19 | .sidebar-container {
20 | width: 50px;
21 | background-color: #333;
22 | min-height: 100%;
23 | z-index: 1;
24 | }
25 |
26 | .sidebar-buttons {
27 | display: flex;
28 | flex-direction: column;
29 | color: #fff;
30 | font-size: 30px;
31 | padding: 10px;
32 | height: 100%
33 | }
34 |
35 | .sidebar-link {
36 | margin-bottom: 20px;
37 | color: #fff;
38 | }
39 |
40 | .sidebar-link:hover {
41 | color: #20C997;
42 | }
43 |
44 | .overview-container {
45 | display: flex;
46 | width: 95vw;
47 | }
48 |
49 | .status-card {
50 | display: flex;
51 | }
52 |
53 | .namespace-container {
54 | width: 14em;
55 | margin-left: 0.5em;
56 | margin-bottom: 15px;
57 | margin-bottom: 0;
58 | border: solid 1px #333;
59 | border-radius: 5px;
60 | align-self: center;
61 | padding: 0;
62 | }
63 |
64 | .header {
65 | position: fixed;
66 | display: flex;
67 | width: 100%;
68 | background-color: #20C997;
69 | justify-content: space-between;
70 | align-content: center;
71 | padding: 0.5em;
72 | }
73 |
74 | .header-elements {
75 | display: flex;
76 | justify-content: space-between;
77 | width: 95%;
78 | }
79 |
80 | .logo {
81 | margin: 0;
82 | font-family: "Nunito", sans-serif;
83 | color: #333;
84 | }
85 |
86 | .events-container {
87 | border-left: 1px solid #f0efef;
88 | min-width: 50%;
89 | margin-right: 1em;
90 | }
91 |
92 | .events-dropdown-container {
93 | display: flex;
94 | margin-left: 22px;
95 | margin-top: 14px;
96 | margin-bottom: 20px;
97 | }
98 |
99 | .events-dropdown {
100 | width: 10em;
101 | margin-right: 2em;
102 | color: #3371e3;
103 | }
104 |
105 | .severity-dropdown {
106 | width: 10em;
107 | color: #ed6a5a;
108 | }
109 |
110 | .events-card-container {
111 | display: flex;
112 | flex-direction: column;
113 | padding-top: 0.2em;
114 | padding-bottom: 1em;
115 | padding-right: 1em;
116 | padding-left: 1em;
117 | max-height: 85vh;
118 | margin: 5px;
119 | margin-top: 0px;
120 | overflow-y: scroll;
121 | text-align: left;
122 | overflow-x: hidden;
123 | width: 90%;
124 | }
125 |
126 | .alert-card,
127 | .event-card {
128 | padding: 10px;
129 | padding-left: 15px;
130 | padding-top: 15x;
131 | margin-bottom: 10px;
132 | box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 3px 0px, rgba(27, 31, 35, 0.15) 0px 0px 0px 1px;
133 | border-radius: 5px;
134 | color: #333;
135 | margin-bottom: 1.5em;
136 | }
137 |
138 | .alert-card:hover,
139 | .event-card:hover {
140 | background-color: rgb(250, 250, 250);
141 | box-shadow: 0 8px 8px rgba(0,0,0,0.1), 0 12px 12px rgba(0,0,0,0.01);
142 | margin-bottom: 1.5em;
143 | }
144 |
145 | .alert-content-group,
146 | .alert-content-state,
147 | .alert-content-name,
148 | .alert-content-severity,
149 | .alert-content-description,
150 | .alert-content-summary,
151 | .event-content-last_seen,
152 | .event-content-message,
153 | .event-content-object,
154 | .event-content-reason,
155 | .event-content-severity {
156 | color:#797676;
157 | overflow-wrap: break-word;
158 | max-width: 82%;
159 | }
160 |
161 | .alert-content-group {
162 | margin-left: 49px;
163 | }
164 |
165 | .alert-content-state {
166 | margin-left: 55px;
167 | }
168 |
169 | .alert-content-name {
170 | margin-left: 49px;
171 | }
172 |
173 | .alert-content-severity {
174 | margin-left: 35px;
175 | }
176 |
177 | .alert-content-description {
178 | margin-left: 10px;
179 | }
180 |
181 | .alert-content-summary {
182 | margin-left: 22px;
183 | }
184 |
185 | .event-content-last_seen {
186 | margin-left: 10px
187 | }
188 |
189 | .event-content-message {
190 | margin-left: 15px
191 | }
192 |
193 | .event-content-object {
194 | margin-left: 34px
195 | }
196 |
197 | .event-content-reason {
198 | margin-left: 27px
199 | }
200 |
201 | .event-content-severity {
202 | margin-left: 23px
203 | }
204 |
205 | .alert-description,
206 | .event-description {
207 | color:#333;
208 | font-weight: 500;
209 | }
210 |
211 | .alert-line,
212 | .event-line {
213 | display: flex;
214 | }
215 |
216 | .summary-name {
217 | font-weight: bold;
218 | }
219 |
220 | .select-namespace {
221 | margin: 0 auto;
222 | max-width: 70%;
223 | margin-left: 3.5em;
224 | }
225 |
226 | /* --- CSS for status container --- */
227 |
228 | .status-container {
229 | display: flex;
230 | flex-direction: column;
231 | text-align: center;
232 | margin: 1em;
233 | margin-right: 0.4em;
234 | min-width: 50%;
235 | }
236 |
237 | .bubble-container {
238 | display: flex;
239 | flex-direction: column;
240 | background-color: rgb(255, 255, 255);
241 | border-radius: 1em;
242 | padding-bottom: 0.5em;
243 | padding-top: 0em;
244 | justify-content: space-between;
245 | }
246 |
247 | .status-card {
248 | display: flex;
249 | flex-direction: row;
250 | justify-content: flex-start;
251 | padding-bottom: 1em;
252 | }
253 |
254 | .bubble-box {
255 | display: flex;
256 | flex-direction: start;
257 | width: 100%;
258 | flex-wrap: wrap;
259 | }
260 |
261 | /* individual status bubbles */
262 | .status-box {
263 | background-color: #3371e3;
264 | border-radius: 3px;
265 | width: 1.5rem;
266 | height: 1.5rem;
267 | margin: 1px;
268 | }
269 |
270 | .status-name {
271 | text-align: left;
272 | min-width: 25%;
273 | padding-right: 1em;
274 | padding-top: 0.3em;
275 | width: 5em;
276 | color:#333;
277 | font-weight: 500;
278 | }
279 |
280 | /* quantity of type of status */
281 | .quantity-box{
282 | padding-right: 1em;
283 | color:#333;
284 | font-weight: 500;
285 | }
286 |
287 | /*----- Graph Container ---*/
288 |
289 | .graph-dropdown-container{
290 | display: flex;
291 | }
292 |
293 | .data-type-dropdown {
294 | margin-right: 30px;
295 | }
296 |
297 | .data-type-dropdown,
298 | .data-source-dropdown {
299 | width: 10em;
300 | color: #3371e3;
301 | }
302 |
303 | .graph-card-title {
304 | color:#333;
305 | background-color: #faf6f6;
306 | text-align: start;
307 | margin-top: 15px;
308 | height: 1em;
309 | margin-bottom: 10px;
310 | margin-right: 16px;
311 | padding: 1em;
312 | border-radius: 2px;
313 | font-weight: 500;
314 | }
315 |
316 | /* metrics page*/
317 | .graph-container {
318 | display: grid;
319 | grid-template-columns: repeat(4, 1fr);
320 | grid-template-rows: repeat(3, 1fr);
321 | grid-column-gap: 10px;
322 | grid-row-gap: 10px;
323 | margin-top: 5px;
324 | margin-left: 15px;
325 | margin-right: 15px;
326 | padding-right: 150px;
327 | overflow-y: scroll;
328 | }
329 |
330 | .graph-card {
331 | min-width: 300px;
332 | margin-left: 1.5em;
333 | height: 300px;
334 | }
335 |
336 | .metrics-graph-card {
337 | height: 200px;
338 | border: 1px dotted #797676;
339 | margin-right: 20px;
340 | }
341 |
342 | .dashboard {
343 | display: flex;
344 | overflow: hidden;
345 | height: 100vh;
346 | }
347 |
348 | @media (max-width: 1400px) {
349 | body {
350 | overflow-y: scroll;
351 | }
352 |
353 | .namespace-container {
354 | margin-left: 3.6em;
355 | }
356 |
357 | .sidebar-container {
358 | position: fixed;
359 | }
360 |
361 | .overview-container {
362 | display: flex;
363 | flex-direction: column;
364 | justify-content: space-around;
365 | overflow-y: scroll;
366 | margin-left: 60px;
367 | }
368 |
369 | .status-container {
370 | margin-bottom: 0;
371 | padding-bottom: 0;
372 | }
373 |
374 | .events-container {
375 | margin-top: 350px;
376 | padding-top: 0;
377 | width: 100%;
378 | }
379 |
380 | .graph-container {
381 | margin-left: 65px;
382 | }
383 | }
--------------------------------------------------------------------------------
/app/types.ts:
--------------------------------------------------------------------------------
1 | export type fetchData = {
2 | data: any;
3 | };
--------------------------------------------------------------------------------
/electron/dataController/formatData/formatAlerts.ts:
--------------------------------------------------------------------------------
1 | // formats alerts data for processing in the frontend
2 | export const formatAlerts = (data: any) => {
3 | const groups = data.data.groups;
4 | const tableData = [];
5 | for (let group of groups) {
6 | for (let rule of group.rules) {
7 | if (rule.state) {
8 | const ruleObj = {
9 | group: group.name,
10 | state: rule.state,
11 | name: rule.name,
12 | severity: rule.labels?.severity,
13 | description: rule?.annotations.description,
14 | summary: rule?.annotations.summary,
15 | alerts: rule.alerts,
16 | };
17 | tableData.push(ruleObj);
18 | }
19 | }
20 | }
21 | return tableData;
22 | };
--------------------------------------------------------------------------------
/electron/dataController/formatData/formatEvents.ts:
--------------------------------------------------------------------------------
1 | // formats events coming from kubectl call for processing in frontend
2 | export const formatEvents = (arr: string[]) => {
3 | arr.pop();
4 | const trimmed: string[][] = arr.map((el: string) => el.split(/[ ]{2,}/));
5 | const headers: string[] = trimmed[0].map((el) =>
6 | el.toLowerCase().replace(" ", "_")
7 | );
8 | trimmed.shift();
9 | return trimmed.map((row: string[]) => {
10 | let obj: {} = {};
11 | row.forEach((r: string, i: number) => {
12 | (obj as any)[headers[i]] = row[i];
13 | });
14 | return obj;
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/electron/dataController/formatData/formatMatrixData.ts:
--------------------------------------------------------------------------------
1 | import { convertUnixToISOString, bytesToGb } from '../../utils';
2 |
3 | interface matrixData {
4 | result: [
5 | metric: {},
6 | values: Array<[number, string]>,
7 | ],
8 | resultType: string,
9 | }
10 |
11 | interface output {
12 | [key: string]: {
13 | timestamps: string[]
14 | timeSeriesValues: number[]
15 | }
16 | }
17 |
18 | /**
19 | * @param data
20 | * @param unitType
21 | * @returns object with group type (node, namespace, pod), timestamps and timeSeriesData
22 | * @note timeSeriesValues for memory are converted to GB
23 | */
24 |
25 | export function formatMatrixData(data: matrixData, unitType?: string) {
26 | const output: output = {};
27 |
28 | data.result.forEach((obj: any) => {
29 | const group: string = obj.metric[Object.keys(obj.metric)[0]];
30 | output[group] = {
31 | timestamps: [],
32 | timeSeriesValues: []
33 | };
34 |
35 | output[group].timestamps = obj.values.map((el: [number, string]) => convertUnixToISOString(el[0]));
36 |
37 | //convert bytes to GB when unit type is bytes
38 | output[group].timeSeriesValues = unitType === 'bytes' ?
39 | obj.values.map((el: [number, string]) => bytesToGb(Number(el[1]))) :
40 | obj.values.map((el: [number, string]) => Number(el[1]));
41 | });
42 |
43 | return output;
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/electron/dataController/formatData/formatk8sApiData.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param data
4 | * @param unitType
5 | * @returns array with objets for (nodes, namespaces, deployments, services, pods)
6 | */
7 |
8 | export function formatk8sApiData(data: any) {
9 | const output: any = data.items.reduce((result: any, obj: any) => {
10 | const resObj: any = {
11 | name: obj.metadata.name
12 | }
13 | if (obj.metadata.namespace) resObj.namespace = obj.metadata.namespace
14 | if (data.kind === "NodeList") resObj.conditions = obj.status.conditions;
15 | if (data.kind === "PodList") resObj.status = obj.status.phase;
16 | result.push(resObj)
17 | return result;
18 | }, [])
19 | return output;
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/electron/dataController/getData/getMatrixData.ts:
--------------------------------------------------------------------------------
1 | import { formatMatrixData } from '../formatData/formatMatrixData';
2 | const fetch: any = (...args: any) =>
3 | import('node-fetch').then(({ default: fetch }:any) => fetch(...args));
4 |
5 | export async function fetchMetricsData(query: string, unitType?: string) {
6 | try {
7 | const res = await fetch(query);
8 | const data = await res.json();
9 |
10 | return formatMatrixData(data.data, unitType);
11 | // return data
12 | } catch (err) {
13 | console.log(err);
14 | }
15 | }
--------------------------------------------------------------------------------
/electron/main.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain, protocol } from "electron";
2 | const fetch: any = (...args: any) =>
3 | import("node-fetch").then(({ default: fetch }: any) => fetch(...args));
4 | import * as child_process from "child_process";
5 | import { getStartAndEndDateTime } from "./utils";
6 | import { fetchMetricsData } from "./dataController/getData/getMatrixData";
7 | import { formatk8sApiData } from "./dataController/formatData/formatk8sApiData";
8 | import { formatAlerts } from "./dataController/formatData/formatAlerts";
9 | import { formatEvents } from "./dataController/formatData/formatEvents";
10 | import * as k8s from '@kubernetes/client-node';
11 | import path from 'path'
12 | // K8s API
13 | const kc = new k8s.KubeConfig();
14 | kc.loadFromDefault();
15 | const k8sApiCore = kc.makeApiClient(k8s.CoreV1Api);
16 | const k8sApiApps = kc.makeApiClient(k8s.AppsV1Api);
17 |
18 | const prometheusURL = "http://127.0.0.1:9090/api/v1/";
19 |
20 | let mainWindow: BrowserWindow;
21 |
22 | app.on("ready", createWindow);
23 |
24 | function createWindow(): void {
25 | mainWindow = new BrowserWindow({
26 | width: 800,
27 | height: 700,
28 | webPreferences: {
29 | preload: __dirname + "/preload.js",
30 | },
31 | });
32 | mainWindow.loadURL("http://localhost:8080/");
33 | mainWindow.on("ready-to-show", () => mainWindow.show());
34 | }
35 |
36 | // closes electron on mac devices
37 | app.on("window-all-closed", () => {
38 | if (process.platform !== "darwin") {
39 | app.quit();
40 | }
41 | });
42 |
43 | // TO DO: Type data/responses
44 |
45 | /* K8 API */
46 |
47 | // gets list of all nodes (incl. status) in the k8s cluster
48 | ipcMain.handle('getNodesList', async () => {
49 | try {
50 | const data = await k8sApiCore.listNode('default')
51 | return formatk8sApiData(data.body)
52 | }
53 | catch (err) {
54 | console.log(`Error in 'getNodesList' function: ERROR: ${err}`);
55 | }
56 | });
57 |
58 | // gets list of all namespaces in the k8s cluster
59 | ipcMain.handle('getNamespacesList', async () => {
60 | try {
61 | const data = await k8sApiCore.listNamespace()
62 | const namespaces: string[] = [];
63 | data.body.items.forEach((namespace) => namespaces.push(namespace.metadata.name))
64 | return namespaces;
65 | }
66 | catch (err) {
67 | console.log(`Error in 'getNamespacesList' function: ERROR: ${err}`);
68 | }
69 | });
70 |
71 | // gets list of all deployments in the k8s cluster
72 | ipcMain.handle('getDeploymentsList', async () => {
73 | try {
74 | const data = await k8sApiApps.listDeploymentForAllNamespaces()
75 | return formatk8sApiData(data.body)
76 | }
77 | catch (err) {
78 | console.log(`Error in 'getDeploymentsList' function: ERROR: ${err}`);
79 | }
80 | });
81 |
82 | // gets list of all services in the k8s cluster
83 | ipcMain.handle('getServicesList', async () => {
84 | try {
85 | const data = await k8sApiCore.listServiceForAllNamespaces()
86 | return formatk8sApiData(data.body)
87 | }
88 | catch (err) {
89 | console.log(`Error in 'getServicesList' function: ERROR: ${err}`);
90 | }
91 | });
92 |
93 | // gets list of all pods in the k8s cluster
94 | ipcMain.handle('getPodsList', async () => {
95 | try {
96 | const data = await k8sApiCore.listPodForAllNamespaces()
97 | return formatk8sApiData(data.body)
98 | }
99 | catch (err) {
100 | console.log(`Error in 'getPodsList' function: ERROR: ${err}`);
101 | }
102 | });
103 |
104 | /* Kubectl command line calls */
105 |
106 | // gets list of events for all namespaces
107 | ipcMain.handle("getEvents", () => {
108 | try {
109 | const response: any = child_process.execSync("kubectl get events --all-namespaces", { encoding: "utf8" });
110 | const data = response.split("\n");
111 | const formattedEvents = formatEvents(data);
112 | return formattedEvents;
113 | }
114 | catch (err) {
115 | console.log(`Error in 'getEvents' function: ERROR: ${err}`);
116 | }
117 | });
118 |
119 | /* Prometheus API */
120 |
121 | // gets alerts from Prometheus for alerts page
122 | ipcMain.handle("getAlerts", async () => {
123 | try {
124 | const response = await fetch(`${prometheusURL}/rules`);
125 | const data: any = await response.json();
126 | const formattedAlerts = formatAlerts(data);
127 | return formattedAlerts;
128 | }
129 | catch (err) {
130 | console.log(`Error in 'getAlerts' function: ERROR: ${err}`);
131 | }
132 | });
133 |
134 | // NAMESPACE METRICS
135 | // gets cpu usage by namespace (%)
136 | ipcMain.handle("getCPUUsageByNamespace", async (event, namespace: string) => {
137 | try {
138 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
139 | const namespaceStr = namespace && namespace !== 'ALL' ? `namespace="${namespace}"` : '';
140 | const query = `${prometheusURL}query_range?query=(avg by (namespace) (irate(node_cpu_seconds_total{mode!="idle",${namespaceStr}}[1m]))*100)
141 | &start=${startDateTime}&end=${endDateTime}&step=10m`
142 | return await fetchMetricsData(query);
143 | }
144 | catch (err) {
145 | console.log(`Error in 'getCPUUsageByNamespace' function: ERROR: ${err}`);
146 | }
147 | });
148 |
149 | // gets memory usage by namespace (GB)
150 | ipcMain.handle('getMemoryUsageByNamespace', async(event, namespace: string) => {
151 | try {
152 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
153 | const namespaceStr = namespace && namespace !== 'ALL' ? `{namespace="${namespace}"}` : '';
154 | const query = `${prometheusURL}query_range?query=sum(container_memory_working_set_bytes${namespaceStr})
155 | by (namespace)&start=${startDateTime}&end=${endDateTime}&step=${'1m'}`;
156 | return await fetchMetricsData(query, 'bytes');
157 | }
158 | catch (err) {
159 | console.log(`Error in 'getMemoryUsageByNamespace' function: ERROR: ${err}`);
160 | }
161 | });
162 |
163 | // gets network I/O recieved by namespace
164 | ipcMain.handle('bytesRecievedByNamespace', async(event, namespace: string) => {
165 | try {
166 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
167 | const query = `${prometheusURL}query_range?query=sum(irate(container_network_receive_bytes_total[${'1m'}]))
168 | by (namespace)&start=${startDateTime}&end=${endDateTime}&step=${'1m'}`;
169 | return await fetchMetricsData(query);
170 | }
171 | catch (err) {
172 | console.log(`Error in 'bytesRecievedByNamespace' function: ERROR: ${err}`);
173 | }
174 | });
175 |
176 | // gets network I/O transmitted by namespace
177 | ipcMain.handle('bytesTransmittedByNamespace', async(event, namespace: string) => {
178 | try {
179 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
180 | const query = `${prometheusURL}query_range?query=sum(irate(container_network_transmit_bytes_total[${'1m'}]))
181 | by (namespace)&start=${startDateTime}&end=${endDateTime}&step=${'1m'}`;
182 | return await fetchMetricsData(query);
183 | }
184 | catch (err) {
185 | console.log(`Error in 'bytesTransmittedByNamespace' function: ERROR: ${err}`);
186 | }
187 | });
188 |
189 | // NODE METRICS
190 | // gets cpu usage by node (%)
191 | ipcMain.handle("getCPUUsageByNode", async (event, namespace: string) => {
192 | try {
193 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
194 | const query = `${prometheusURL}query_range?query=(avg by (node) (irate(node_cpu_seconds_total{mode!="idle"}[1m]))*100)
195 | &start=${startDateTime}&end=${endDateTime}&step=10m`
196 | return await fetchMetricsData(query);
197 | }
198 | catch (err) {
199 | console.log(`Error in 'getCPUUsageByNode' function: ERROR: ${err}`);
200 | }
201 | });
202 |
203 | // gets memory usage by node (GB)
204 | ipcMain.handle("getMemoryUsageByNode", async (event, namespace: string) => {
205 | try {
206 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
207 | const query = `${prometheusURL}query_range?query=sum(container_memory_working_set_bytes)
208 | by (node)&start=${startDateTime}&end=${endDateTime}&step=${"10m"}`;
209 | return await fetchMetricsData(query, 'bytes');
210 | }
211 | catch (err) {
212 | console.log(`Error in 'getMemoryUsageByNode' function: ERROR: ${err}`);
213 | }
214 | });
215 |
216 | // gets network I/O recieved by node
217 | ipcMain.handle("bytesRecievedByNode", async (event, namespace: string) => {
218 | try {
219 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
220 | const query = `${prometheusURL}query_range?query=sum(irate(container_network_receive_bytes_total[${"1m"}]))
221 | by (node)&start=${startDateTime}&end=${endDateTime}&step=${"1m"}`;
222 | return await fetchMetricsData(query);
223 | }
224 | catch (err) {
225 | console.log(`Error in 'bytesRecievedByNode' function: ERROR: ${err}`);
226 | }
227 | });
228 |
229 | // gets network I/O transmitted by node
230 | ipcMain.handle("bytesTransmittedByNode", async (event, namespace: string) => {
231 | try {
232 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
233 | const query = `${prometheusURL}query_range?query=sum(irate(container_network_transmit_bytes_total[${"10m"}]))
234 | by (node)&start=${startDateTime}&end=${endDateTime}&step=${"1m"}`;
235 | return await fetchMetricsData(query);
236 | }
237 | catch (err) {
238 | console.log(`Error in 'bytesTransmittedByNode' function: ERROR: ${err}`);
239 | }
240 | });
241 |
242 | // POD METRICS
243 | // gets cpu usage by pod (%)
244 | ipcMain.handle("getCPUUsageByPod", async (event, namespace: string) => {
245 | try {
246 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
247 | const query = `${prometheusURL}query_range?query=(avg by (pod) (irate(node_cpu_seconds_total{mode!="idle"}[1m]))*100)
248 | &start=${startDateTime}&end=${endDateTime}&step=10m`
249 | return await fetchMetricsData(query);
250 | }
251 | catch (err) {
252 | console.log(`Error in 'getCPUUsageByPod' function: ERROR: ${err}`);
253 | }
254 | });
255 |
256 | // gets memory usage by pod (GB)
257 | ipcMain.handle("getMemoryUsageByPod", async (event, namespace: string) => {
258 | try {
259 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
260 | const namespaceStr = namespace && namespace !== "ALL" ? `{namespace="${namespace}"}` : "";
261 | const query = `${prometheusURL}query_range?query=sum(container_memory_working_set_bytes${namespaceStr})
262 | by (pod)&start=${startDateTime}&end=${endDateTime}&step=${"1m"}`;
263 | return await fetchMetricsData(query, 'bytes');
264 | }
265 | catch (err) {
266 | console.log(`Error in 'getMemoryUsageByPod' function: ERROR: ${err}`);
267 | }
268 | });
269 |
270 | // gets network I/O recieved by pod
271 | ipcMain.handle("bytesRecievedByPod", async (event, namespace: string) => {
272 | try {
273 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
274 | const query = `${prometheusURL}query_range?query=sum(irate(container_network_receive_bytes_total[${'1m'}]))
275 | by (pod)&start=${startDateTime}&end=${endDateTime}&step=${'1m'}`;
276 | return await fetchMetricsData(query);
277 | }
278 | catch (err) {
279 | console.log(`Error in 'bytesRecievedByPod' function: ERROR: ${err}`);
280 | }
281 | });
282 |
283 | // gets network I/O transmitted by pod
284 | ipcMain.handle("bytesTransmittedByPod", async (event, namespace: string) => {
285 | try {
286 | const { startDateTime, endDateTime } = getStartAndEndDateTime();
287 | const query = `${prometheusURL}query_range?query=sum(irate(container_network_transmit_bytes_total[${'1m'}]))
288 | by (pod)&start=${startDateTime}&end=${endDateTime}&step=${'1m'}`;
289 | return await fetchMetricsData(query);
290 | }
291 | catch (err) {
292 | console.log(`Error in 'bytesTransmittedByPod' function: ERROR: ${err}`);
293 | }
294 | });
295 |
--------------------------------------------------------------------------------
/electron/preload.ts:
--------------------------------------------------------------------------------
1 |
2 | import { contextBridge, ipcRenderer } from 'electron';
3 |
4 |
5 | // makes the functions from main.ts available in the frontend via context bridge
6 | contextBridge.exposeInMainWorld('electron', {
7 | getAlerts: async () =>
8 | ipcRenderer.invoke('getAlerts'),
9 | getEvents: async () =>
10 | ipcRenderer.invoke('getEvents'),
11 | getCPUUsageByNode: async (namespace: string) =>
12 | ipcRenderer.invoke('getCPUUsageByNode', namespace),
13 | getMemoryUsageByNode: async (namespace: string) =>
14 | ipcRenderer.invoke('getMemoryUsageByNode', namespace),
15 | bytesRecievedByNode: async (namespace: string) =>
16 | ipcRenderer.invoke('bytesRecievedByNode', namespace),
17 | bytesTransmittedByNode: async (namespace: string) =>
18 | ipcRenderer.invoke('bytesTransmittedByNode', namespace),
19 | getCPUUsageByNamespace: async (namespace: string) =>
20 | ipcRenderer.invoke('getCPUUsageByNamespace', namespace),
21 | getMemoryUsageByNamespace: async (namespace: string) =>
22 | ipcRenderer.invoke('getMemoryUsageByNamespace', namespace),
23 | bytesRecievedByNamespace: async (namespace: string) =>
24 | ipcRenderer.invoke('bytesRecievedByNamespace', namespace),
25 | bytesTransmittedByNamespace: async (namespace: string) =>
26 | ipcRenderer.invoke('bytesTransmittedByNamespace', namespace),
27 | getCPUUsageByPod: async (namespace: string) =>
28 | ipcRenderer.invoke('getCPUUsageByPod', namespace),
29 | getMemoryUsageByPod: async (namespace: string) =>
30 | ipcRenderer.invoke('getMemoryUsageByPod', namespace),
31 | bytesRecievedByPod: async (namespace: string) =>
32 | ipcRenderer.invoke('bytesRecievedByPod', namespace),
33 | bytesTransmittedByPod: async (namespace: string) =>
34 | ipcRenderer.invoke('bytesTransmittedByPod', namespace),
35 | getNodesList: async () =>
36 | ipcRenderer.invoke('getNodesList'),
37 | getNamespacesList: async () =>
38 | ipcRenderer.invoke('getNamespacesList'),
39 | getDeploymentsList: async () =>
40 | ipcRenderer.invoke('getDeploymentsList'),
41 | getServicesList: async () =>
42 | ipcRenderer.invoke('getServicesList'),
43 | getPodsList: async () =>
44 | ipcRenderer.invoke('getPodsList'),
45 | });
46 |
--------------------------------------------------------------------------------
/electron/utils.ts:
--------------------------------------------------------------------------------
1 | // converts Unix timestamp to ISO string
2 | export function convertUnixToISOString (unixTimestamp: number) {
3 | return new Date(unixTimestamp*1000).toISOString()
4 | }
5 |
6 | // gets ISO formatted start and end time for fetch requests in main.ts
7 | export function getStartAndEndDateTime() {
8 | let now: Date = new Date();
9 | let nowCopy: Date = new Date(now.getTime());
10 | nowCopy.setHours(nowCopy.getHours() - 24);
11 | let startDateTime: string = nowCopy.toISOString();
12 | let endDateTime: string = now.toISOString();
13 | return {
14 | startDateTime: startDateTime,
15 | endDateTime: endDateTime
16 | }
17 | }
18 |
19 | // converts bites to Gb
20 | export function bytesToGb(num: number) {
21 | if (num === 0) return 0;
22 | const k = 1024;
23 | const i = Math.floor(Math.log(num) / Math.log(k));
24 | return (num / Math.pow(k, i));
25 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "y",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "./dist/electron/main.js",
6 | "scripts": {
7 | "webpack-start": "webpack serve",
8 | "build": "tsc",
9 | "start": "npm run build && electron .",
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "both:dev": "npm run start & nodemon --watch ./electron --exec 'npm run electron'",
12 | "both": "npm run webpack-start && npm run start"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@emotion/react": "^11.9.3",
18 | "@emotion/styled": "^11.9.3",
19 | "@headlessui/react": "^1.6.5",
20 | "@heroicons/react": "^1.0.6",
21 | "@kubernetes/client-node": "^0.17.0",
22 | "@mui/icons-material": "^5.8.4",
23 | "@mui/material": "^5.8.7",
24 | "@reduxjs/toolkit": "^1.8.3",
25 | "@types/node-fetch": "^2.6.2",
26 | "@types/react-router-dom": "^5.3.3",
27 | "chart.js": "^3.8.0",
28 | "electron": "^19.0.7",
29 | "express": "^4.18.1",
30 | "file-loader": "^6.2.0",
31 | "html-loader": "^4.1.0",
32 | "image-webpack-loader": "^8.1.0",
33 | "node-cmd": "^5.0.0",
34 | "node-fetch": "^2.x.x",
35 | "nodemon": "^2.0.18",
36 | "react": "^18.2.0",
37 | "react-chartjs-2": "^4.3.1",
38 | "react-dom": "^18.2.0",
39 | "react-icons": "^4.4.0",
40 | "react-redux": "^8.0.2",
41 | "react-router-dom": "^6.3.0",
42 | "react-select": "^5.4.0",
43 | "redux-thunk": "^2.4.1",
44 | "ts-node": "^10.8.1"
45 | },
46 | "devDependencies": {
47 | "@babel/preset-env": "^7.18.6",
48 | "@types/express": "^4.17.13",
49 | "@types/node": "^18.0.0",
50 | "@types/react": "^18.0.14",
51 | "@types/react-dom": "^18.0.5",
52 | "@typescript-eslint/eslint-plugin": "^5.30.7",
53 | "@typescript-eslint/parser": "^5.30.7",
54 | "autoprefixer": "^10.4.7",
55 | "babel-loader": "^8.2.5",
56 | "css-loader": "^6.7.1",
57 | "electron-reload": "^2.0.0-alpha.1",
58 | "eslint": "^8.20.0",
59 | "eslint-config-airbnb": "^19.0.4",
60 | "eslint-config-airbnb-typescript": "^17.0.0",
61 | "eslint-plugin-react": "^7.30.1",
62 | "html-webpack-plugin": "^5.5.0",
63 | "postcss": "^8.4.14",
64 | "postcss-loader": "^7.0.0",
65 | "sass-loader": "^13.0.2",
66 | "style-loader": "^3.3.1",
67 | "ts-loader": "^9.3.1",
68 | "typescript": "^4.7.4",
69 | "webpack": "^5.73.0",
70 | "webpack-cli": "^4.10.0",
71 | "webpack-dev-server": "^4.9.3"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | "jsx": "react", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
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 | // "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. */
52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
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 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
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 |
78 | /* Type Checking */
79 | // "strict": true, /* Enable all strict type-checking options. */
80 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | },
103 | "include": [
104 | "app/**/*",
105 | "electron/**/*"
106 | ]
107 | }
108 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | // const ANTDCSSFILEPATH = path.resolve(__dirname, './node_modules/antd/dist/antd.less');
5 | module.exports = {
6 | entry: './app/index.tsx',
7 | output: {
8 | path: path.resolve(process.cwd(), 'dist'), //__dirname?
9 | filename: 'index_bundle.js',
10 | },
11 | devtool: 'eval-source-map',
12 | module: {
13 | rules: [
14 | {
15 | test: /\.(js|jsx)$/,
16 | exclude: /node_modules/,
17 | use: ['babel-loader'],
18 | },
19 | {
20 | test: /\.(ts|tsx)$/,
21 | exclude: /node_modules/,
22 | use: ['ts-loader'],
23 | },
24 | {
25 | test: /\.s?css$/,
26 | use: ['style-loader', 'css-loader', 'postcss-loader'],
27 | },
28 | {
29 | test: /\.(jpg|jpeg|png|ttf|svg|gif)$/,
30 | use: [
31 | 'file-loader',
32 | {
33 | loader: 'image-webpack-loader',
34 | options: {
35 | mozjpeg: {
36 | quality: 10,
37 | },
38 | },
39 | },
40 | ],
41 | exclude: /node_modules/,
42 | },
43 | ],
44 | },
45 | mode: 'development',
46 | devServer: {
47 | hot: true,
48 | historyApiFallback: true,
49 | // contentBase: path.resolve(process.cwd(), 'app'), //__dirname
50 | },
51 | plugins: [
52 | new HtmlWebpackPlugin({
53 | template: 'app/index.html',
54 | }),
55 | ],
56 | resolve: {
57 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.gif', '.png', '.svg'],
58 | },
59 | };
--------------------------------------------------------------------------------