├── .gitignore
├── README.md
├── client
├── .eslintrc.json
├── .prettierrc.json
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── App.js
│ ├── App.test.js
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── app
│ │ ├── createAppSlice.js
│ │ ├── createAppSlice.ts
│ │ ├── hooks.js
│ │ ├── hooks.ts
│ │ ├── store.js
│ │ └── store.ts
│ ├── features
│ │ ├── captive-portal
│ │ │ ├── ProtectedRoute.js
│ │ │ ├── ProtectedRoute.test.tsx
│ │ │ ├── ProtectedRoute.tsx
│ │ │ ├── captivePortalSlice.js
│ │ │ ├── captivePortalSlice.ts
│ │ │ ├── components
│ │ │ │ ├── CaptiveForm.js
│ │ │ │ ├── CaptiveForm.tsx
│ │ │ │ ├── CaptiveFormContent.js
│ │ │ │ └── CaptiveFormContent.tsx
│ │ │ └── containers
│ │ │ │ ├── CaptivePortal.js
│ │ │ │ ├── CaptivePortal.test.js
│ │ │ │ ├── CaptivePortal.test.tsx
│ │ │ │ └── CaptivePortal.tsx
│ │ ├── cluster-log
│ │ │ ├── clusterLogsApiSlice.js
│ │ │ ├── clusterLogsApiSlice.ts
│ │ │ ├── components
│ │ │ │ ├── ClusterLog.js
│ │ │ │ ├── ClusterLog.test.js
│ │ │ │ ├── ClusterLog.test.tsx
│ │ │ │ └── ClusterLog.tsx
│ │ │ └── containers
│ │ │ │ ├── ClusterLogContainer.js
│ │ │ │ ├── ClusterLogContainer.test.js
│ │ │ │ ├── ClusterLogContainer.test.tsx
│ │ │ │ ├── ClusterLogContainer.tsx
│ │ │ │ └── clusterLogContainer.js
│ │ ├── cluster-view
│ │ │ ├── clusterViewApiSlice.js
│ │ │ ├── clusterViewApiSlice.ts
│ │ │ ├── components
│ │ │ │ ├── CustomNode.js
│ │ │ │ └── CustomNode.tsx
│ │ │ └── containers
│ │ │ │ ├── ClusterViewContainer.js
│ │ │ │ └── ClusterViewContainer.tsx
│ │ ├── grafana-dashboard
│ │ │ ├── Grafana.css
│ │ │ ├── GrafanaDashboardApiSlice.js
│ │ │ ├── GrafanaDashboardApiSlice.ts
│ │ │ ├── GrafanaViewContainer.js
│ │ │ └── GrafanaViewContainer.tsx
│ │ ├── info-drawer
│ │ │ ├── InfoPopover.js
│ │ │ └── InfoPopover.tsx
│ │ ├── landing-page
│ │ │ ├── LandingPage.js
│ │ │ ├── LandingPage.test.js
│ │ │ ├── LandingPage.test.tsx
│ │ │ ├── LandingPage.tsx
│ │ │ ├── components
│ │ │ │ ├── MeetTheTeam.css
│ │ │ │ ├── MeetTheTeam.js
│ │ │ │ ├── MeetTheTeam.tsx
│ │ │ │ ├── ReadMe.css
│ │ │ │ ├── ReadMe.js
│ │ │ │ └── ReadMe.tsx
│ │ │ └── landingpage.css
│ │ ├── mini-drawer
│ │ │ ├── MiniDrawer.js
│ │ │ └── MiniDrawer.tsx
│ │ └── settings
│ │ │ ├── settings.js
│ │ │ ├── settings.test.js
│ │ │ ├── settings.test.tsx
│ │ │ └── settings.tsx
│ ├── index.css
│ ├── main.js
│ ├── main.tsx
│ ├── public
│ │ ├── AlexPFP.jpeg
│ │ ├── JonathanPFP.jpeg
│ │ ├── MichaelPFP.jpg
│ │ ├── VincentPFP.png
│ │ ├── logo.png
│ │ └── logo.svg
│ ├── setupTests.js
│ ├── setupTests.ts
│ ├── utils
│ │ ├── test-utils.js
│ │ └── test-utils.tsx
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.d.ts
├── vite.config.js
└── vite.config.ts
├── k8state.png
└── server
├── controllers
├── generalController.js
├── generalController.ts
├── kubernetesController.js
└── kubernetesController.ts
├── package-lock.json
├── package.json
├── routes
├── kubernetesRouter.js
└── kubernetesRouter.ts
├── server.js
├── server.ts
├── services
├── generalService.js
├── generalService.ts
├── kubernetesService.js
└── kubernetesService.ts
├── tsconfig.json
├── vite.config.js
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | # Dependency directories
11 | node_modules/
12 | client/node_modules/
13 | server/node_modules/
14 |
15 | # Build outputs
16 | dist/
17 | client/dist/
18 | server/dist/
19 | dist-ssr/
20 |
21 | # Local environment files
22 | *.local
23 | .env
24 | .env0
25 |
26 | # TypeScript build info files
27 | *.tsbuildinfo
28 |
29 | # Editor directories and files
30 | .vscode/*
31 | !.vscode/extensions.json
32 | .idea/
33 | .DS_Store
34 | *.suo
35 | *.ntvs*
36 | *.njsproj
37 | *.sln
38 | *.sw?
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # K8State: Kubernetes Dashboard
2 |
3 | K8State is a comprehensive Kubernetes dashboard application designed to provide real-time insights into your Kubernetes clusters. With K8State, you can monitor, manage, and optimize your Kubernetes resources through a user-friendly interface built on modern web technologies.
4 |
5 | ## Features
6 |
7 | • Real-Time Monitoring: View the status of pods, nodes, and services in real-time.
8 | • Resource Visualization: Graphical representation of resource utilization, leveraging Prometheus and Grafana.
9 | • Interactive UI: Built with React, Redux, React-Flow and Material-UI, offering a responsive and intuitive interface.
10 | • Scalability: Designed to scale with your Kubernetes clusters, supporting multiple namespaces and contexts.
11 | • Extensibility: Easily extendable through a modular architecture to accommodate custom metrics and features.
12 |
13 | ## Prerequisites
14 |
15 | • Node.js (v14.x or higher)
16 | • Kubernetes Cluster URL / Bearer Token
17 | • Prometheus and Grafana (Installed on the Kubernetes Cluster)
18 | • Helm (for deploying Grafana on the cluster)
19 | • npm or Yarn
20 |
21 | More information about how to generate a bearer token:
22 | https://kubernetes.io/docs/reference/kubectl/generated/kubectl_create/kubectl_create_token/
23 |
24 | ## Installation
25 |
26 | 1. Clone the Repository
27 |
28 | ```sh
29 | git clone https://github.com/your-username/k8state.git
30 | cd k8state
31 | ```
32 |
33 | 2. Install dependencies in both the client and server folders
34 |
35 | Using npm:
36 |
37 | ```sh
38 | cd client
39 | npm install
40 |
41 | cd server
42 | npm install
43 |
44 | ```
45 |
46 | Using yarn:
47 |
48 | ```sh
49 | yarn install
50 | ```
51 |
52 | ## Grafana / Prometheus
53 |
54 | In order to import your Grafana dashboard, Grafana must be installed in the
55 | cluster. If you already have a Grafana dashboard, ensure the 'allow_embedding'
56 | property is enabled and restart Grafana.
57 |
58 | Check this documentation to set up grafana:
59 | https://grafana.com/docs/grafana/latest/setup-grafana/installation/kubernetes/
60 |
61 | After installing Grafana, you can then create and import that dashboard to be
62 | used in the application. This allows you to centralize monitoring and viewing
63 | tools within a single application.
64 |
65 | We recommend also installing Prometheus to automatically scrape
66 | the cluster for necessary metrics. Both Prometheus and Grafana can be installed
67 | using Helm.
68 |
69 | To create your dashboard, log in to Grafana and add your Prometheus URL as a new
70 | data source under 'Connections'. Once saved and tested, you'll be able to
71 | customize a dashboard on Grafana, allowing you to generate a link for embedding.
72 | Input this link in the Grafana Dashboard prompt in the application.
73 |
74 | ## Scripts
75 |
76 | - `dev` - start dev server on port 3000
77 | - `server` - start backend server on port 8080
78 |
79 | ## Running the Application
80 |
81 | 1. Start the Backend
82 |
83 | In the root directory:
84 |
85 | ```sh
86 | cd server
87 | npm run server
88 | ```
89 |
90 | Starts the server on http://localhost:8080
91 |
92 | 2. Start the Frontend
93 |
94 | In the root directory:
95 |
96 | ```sh
97 | cd client
98 | npm run build
99 | npm run start
100 | ```
101 |
102 | Starts the client on http://localhost:3000
103 |
104 | ## Technology Stack (This application uses Typescript)
105 |
106 | • Frontend: React, Redux, React-Flow, Material-UI
107 | • Backend: Node.js, Express
108 | • Metrics and Monitoring: Prometheus, Grafana
109 | • Data Management: Kubernetes API
110 | • Deployment: Kubernetes, Helm
111 |
112 | ## Creators
113 |
114 | - [Vincent Collis](https://github.com/VincentCollis)
115 | - [Jonathan Wu](https://github.com/Jon-Wu1)
116 | - [Michael Chen](https://github.com/mochamochaccino)
117 | - [Alex Greenberg](https://github.com/AlexG0718)
118 |
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "react-app",
5 | "plugin:react/jsx-runtime",
6 | "prettier"
7 | ],
8 | "parser": "@typescript-eslint/parser",
9 | "parserOptions": { "project": true, "tsconfigRootDir": "./" },
10 | "plugins": ["@typescript-eslint"],
11 | "root": true,
12 | "ignorePatterns": ["dist"],
13 | "rules": {
14 | "@typescript-eslint/consistent-type-imports": [
15 | 2,
16 | { "fixStyle": "separate-type-imports" }
17 | ],
18 | "@typescript-eslint/no-restricted-imports": [
19 | 2,
20 | {
21 | "paths": [
22 | {
23 | "name": "react-redux",
24 | "importNames": ["useSelector", "useStore", "useDispatch"],
25 | "message": "Please use pre-typed versions from `src/app/hooks.ts` instead."
26 | }
27 | ]
28 | }
29 | ]
30 | },
31 | "overrides": [
32 | { "files": ["*.{c,m,}{t,j}s", "*.{t,j}sx"] },
33 | { "files": ["*{test,spec}.{t,j}s?(x)"], "env": { "jest": true } }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/client/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "avoid"
4 | }
5 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 |
17 |
18 |
22 |
23 |
24 |
25 | K8State
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-template-redux",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "concurrently \"npm:watch\" \"vite\" ",
8 | "start": "concurrently \"npm:watch\" \"vite\" ",
9 | "build": "concurrently \"npm:watch\" \"vite build\"",
10 | "preview": "concurrently \"npm:watch\" \"vite\" \"preview\"",
11 | "test": "vitest run",
12 | "format": "prettier --write .",
13 | "lint": "eslint .",
14 | "lint:fix": "eslint --fix .",
15 | "type-check": "tsc --noEmit",
16 | "watch": "tsc --watch"
17 | },
18 | "dependencies": {
19 | "@emotion/react": "^11.13.0",
20 | "@emotion/styled": "^11.13.0",
21 | "@fontsource/roboto": "^5.0.14",
22 | "@mui/icons-material": "^5.16.7",
23 | "@mui/material": "^5.16.7",
24 | "@reduxjs/toolkit": "^2.0.1",
25 | "@xyflow/react": "^12.1.1",
26 | "concurrently": "^8.2.2",
27 | "dotenv": "^16.4.5",
28 | "react": "^18.2.0",
29 | "react-dom": "^18.2.0",
30 | "react-redux": "^9.1.0",
31 | "react-router-dom": "^6.26.1",
32 | "styled-components": "^6.1.13"
33 | },
34 | "devDependencies": {
35 | "@testing-library/dom": "^9.3.4",
36 | "@testing-library/jest-dom": "^6.5.0",
37 | "@testing-library/react": "^14.1.2",
38 | "@testing-library/user-event": "^14.5.2",
39 | "@types/jest": "^29.5.12",
40 | "@types/react": "^18.2.47",
41 | "@types/react-dom": "^18.2.18",
42 | "@vitejs/plugin-react": "^4.2.1",
43 | "eslint": "^8.56.0",
44 | "eslint-config-prettier": "^9.1.0",
45 | "eslint-config-react-app": "^7.0.1",
46 | "eslint-plugin-prettier": "^5.1.3",
47 | "jsdom": "^23.2.0",
48 | "prettier": "^3.2.1",
49 | "typescript": "^5.3.3",
50 | "vite": "^5.0.11",
51 | "vitest": "^1.2.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { Box } from "@mui/material";
3 | import MiniDrawer from "./features/mini-drawer/MiniDrawer";
4 | const App = () => {
5 | return (_jsx("div", { className: "App", children: _jsx(Box, { children: _jsx(MiniDrawer, {}) }) }));
6 | };
7 | export default App;
8 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { screen } from "@testing-library/react";
3 | import App from "./App";
4 | import { renderWithProviders } from "./utils/test-utils";
5 | // Testing to ensure App component renders
6 | test("renders MiniDrawer component", () => {
7 | // Render the App component
8 | renderWithProviders(_jsx(App, {}));
9 | // Check if MiniDrawer is in the document
10 | // Check if the menu button (IconButton) is in the document
11 | const menuButton = screen.getByLabelText(/open drawer/i);
12 | expect(menuButton).toBeInTheDocument();
13 | });
14 |
--------------------------------------------------------------------------------
/client/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { screen } from "@testing-library/react"
2 | import App from "./App"
3 | import { renderWithProviders } from "./utils/test-utils"
4 |
5 | // Testing to ensure App component renders
6 | test("renders MiniDrawer component", () => {
7 | // Render the App component
8 | renderWithProviders()
9 |
10 | // Check if MiniDrawer is in the document
11 | // Check if the menu button (IconButton) is in the document
12 | const menuButton = screen.getByLabelText(/open drawer/i)
13 | expect(menuButton).toBeInTheDocument()
14 | })
15 |
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@mui/material"
2 | import MiniDrawer from "./features/mini-drawer/MiniDrawer"
3 |
4 | const App = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default App
15 |
--------------------------------------------------------------------------------
/client/src/app/createAppSlice.js:
--------------------------------------------------------------------------------
1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit";
2 | // This function creates a slice with async thunks
3 | export const createAppSlice = buildCreateSlice({
4 | creators: { asyncThunk: asyncThunkCreator },
5 | });
6 |
--------------------------------------------------------------------------------
/client/src/app/createAppSlice.ts:
--------------------------------------------------------------------------------
1 | import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit"
2 |
3 | // This function creates a slice with async thunks
4 | export const createAppSlice = buildCreateSlice({
5 | creators: { asyncThunk: asyncThunkCreator },
6 | })
7 |
--------------------------------------------------------------------------------
/client/src/app/hooks.js:
--------------------------------------------------------------------------------
1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks.
2 | // These imports are restricted elsewhere to ensure consistent
3 | // usage of typed hooks throughout the application.
4 | // We disable the ESLint rule here because this is the designated place
5 | // for importing and re-exporting the typed versions of hooks.
6 | /* eslint-disable @typescript-eslint/no-restricted-imports */
7 | import { useDispatch, useSelector } from "react-redux";
8 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
9 | export const useAppDispatch = useDispatch.withTypes();
10 | export const useAppSelector = useSelector.withTypes();
11 |
--------------------------------------------------------------------------------
/client/src/app/hooks.ts:
--------------------------------------------------------------------------------
1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks.
2 | // These imports are restricted elsewhere to ensure consistent
3 | // usage of typed hooks throughout the application.
4 | // We disable the ESLint rule here because this is the designated place
5 | // for importing and re-exporting the typed versions of hooks.
6 | /* eslint-disable @typescript-eslint/no-restricted-imports */
7 | import { useDispatch, useSelector } from "react-redux"
8 | import type { AppDispatch, RootState } from "./store"
9 |
10 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
11 | export const useAppDispatch = useDispatch.withTypes()
12 | export const useAppSelector = useSelector.withTypes()
13 |
--------------------------------------------------------------------------------
/client/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore, combineReducers } from "@reduxjs/toolkit";
2 | import { setupListeners } from "@reduxjs/toolkit/query/react";
3 | import { clusterApi } from "../features/cluster-view/clusterViewApiSlice";
4 | import { clusterLogsApi } from "../features/cluster-log/clusterLogsApiSlice";
5 | import portalSliceReducer from "../features/captive-portal/captivePortalSlice";
6 | import clusterViewReducer from "../features/cluster-view/clusterViewApiSlice";
7 | import clusterLogsReducer from "../features/cluster-log/clusterLogsApiSlice";
8 | import iframeReducer from "../features/grafana-dashboard/GrafanaDashboardApiSlice";
9 | // Combine the slices and RTK Query APIs into the root reducer
10 | const rootReducer = combineReducers({
11 | [clusterApi.reducerPath]: clusterApi.reducer, // Adding the RTK Query reducer
12 | [clusterLogsApi.reducerPath]: clusterLogsApi.reducer, // Adding the RTK Query reducer
13 | clusterView: clusterViewReducer, // Adding the clusterView slice reducer
14 | portalSlice: portalSliceReducer, // Adding the portal slice reducer
15 | clusterLogs: clusterLogsReducer, // Adding the clusterLogs slice reducer
16 | iframe: iframeReducer,
17 | });
18 | // The store setup is wrapped in `makeStore` to allow reuse
19 | // when setting up tests that need the same store config
20 | export const makeStore = (preloadedState) => {
21 | const store = configureStore({
22 | reducer: rootReducer,
23 | middleware: getDefaultMiddleware => getDefaultMiddleware().concat(clusterApi.middleware, clusterLogsApi.middleware), // Adding RTK Query middleware
24 | preloadedState,
25 | });
26 | // configure listeners using the provided defaults
27 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors
28 | setupListeners(store.dispatch);
29 | return store;
30 | };
31 | export const store = makeStore();
32 |
--------------------------------------------------------------------------------
/client/src/app/store.ts:
--------------------------------------------------------------------------------
1 | import type { Action, ThunkAction } from "@reduxjs/toolkit"
2 | import { configureStore, combineReducers } from "@reduxjs/toolkit"
3 | import { setupListeners } from "@reduxjs/toolkit/query/react"
4 | import { clusterApi } from "../features/cluster-view/clusterViewApiSlice"
5 | import { clusterLogsApi } from "../features/cluster-log/clusterLogsApiSlice"
6 | import portalSliceReducer from "../features/captive-portal/captivePortalSlice"
7 | import clusterViewReducer from "../features/cluster-view/clusterViewApiSlice"
8 | import clusterLogsReducer from "../features/cluster-log/clusterLogsApiSlice"
9 | import iframeReducer from "../features/grafana-dashboard/GrafanaDashboardApiSlice"
10 |
11 | // Combine the slices and RTK Query APIs into the root reducer
12 | const rootReducer = combineReducers({
13 | [clusterApi.reducerPath]: clusterApi.reducer, // Adding the RTK Query reducer
14 | [clusterLogsApi.reducerPath]: clusterLogsApi.reducer, // Adding the RTK Query reducer
15 | clusterView: clusterViewReducer, // Adding the clusterView slice reducer
16 | portalSlice: portalSliceReducer, // Adding the portal slice reducer
17 | clusterLogs: clusterLogsReducer, // Adding the clusterLogs slice reducer
18 | iframe: iframeReducer,
19 | })
20 |
21 | // Infer the `RootState` type from the root reducer
22 | export type RootState = ReturnType
23 |
24 | // The store setup is wrapped in `makeStore` to allow reuse
25 | // when setting up tests that need the same store config
26 | export const makeStore = (preloadedState?: Partial) => {
27 | const store = configureStore({
28 | reducer: rootReducer,
29 | middleware: getDefaultMiddleware =>
30 | getDefaultMiddleware().concat(
31 | clusterApi.middleware,
32 | clusterLogsApi.middleware,
33 | ), // Adding RTK Query middleware
34 |
35 | preloadedState,
36 | })
37 |
38 | // configure listeners using the provided defaults
39 | // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors
40 | setupListeners(store.dispatch)
41 | return store
42 | }
43 |
44 | export const store = makeStore()
45 |
46 | // Infer the type of `store`
47 | export type AppStore = ReturnType
48 | // Infer the `AppDispatch` type from the store itself
49 | export type AppDispatch = AppStore["dispatch"]
50 | export type AppThunk = ThunkAction<
51 | ThunkReturnType,
52 | RootState,
53 | unknown,
54 | Action
55 | >
56 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { useState } from "react";
3 | import { Navigate } from "react-router-dom";
4 | import { useAppDispatch, useAppSelector } from "../../app/hooks";
5 | import { setInit, setAddress, setKey } from "./captivePortalSlice";
6 | export default function ProtectedRoute(props) {
7 | const dispatch = useAppDispatch();
8 | const [loading, setLoading] = useState(true);
9 | const init = useAppSelector(state => state.portalSlice.init);
10 | //performs a fetch request to see if the environment file has been created. if the file exists and has a key or address, the information is assigned to global state and clusterui is rendered
11 | async function checkENVfileForCredentials() {
12 | const response = await fetch(`http://localhost:8080/api/checkenv`);
13 | const data = await response.json();
14 | if (data.address && data.key) {
15 | dispatch(setInit(true));
16 | dispatch(setKey(data.key));
17 | dispatch(setAddress(data.address));
18 | }
19 | setLoading(false);
20 | }
21 | checkENVfileForCredentials();
22 | if (loading) {
23 | return _jsx("div", { children: "Loading..." });
24 | }
25 | if (init === true) {
26 | return props.element;
27 | }
28 | else {
29 | return _jsx(Navigate, { to: "/portal" });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/ProtectedRoute.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from "@testing-library/react";
2 | import { Provider } from "react-redux";
3 | import { BrowserRouter } from "react-router-dom";
4 | import configureStore from "redux-mock-store";
5 | import thunk from "redux-thunk";
6 | import ProtectedRoute from "./ProtectedRoute";
7 |
8 | jest.mock("../../app/hooks", () => ({
9 | useAppDispatch: () => jest.fn(),
10 | useAppSelector: jest.fn(),
11 | }));
12 |
13 | const mockStore = configureStore([thunk]);
14 | const mockFetch = jest.fn();
15 |
16 | global.fetch = mockFetch;
17 |
18 | describe("ProtectedRoute", () => {
19 | let store;
20 |
21 | beforeEach(() => {
22 | store = mockStore({
23 | portalSlice: { init: false },
24 | });
25 | mockFetch.mockClear();
26 | });
27 |
28 | test("renders loading state initially", async () => {
29 | mockFetch.mockResolvedValueOnce({
30 | json: jest.fn().mockResolvedValue({}),
31 | });
32 |
33 | render(
34 |
35 |
36 | ClusterUI} />
37 |
38 |
39 | );
40 |
41 | expect(screen.getByText(/loading.../i)).toBeInTheDocument();
42 | });
43 |
44 | test("renders the element when credentials exist", async () => {
45 | mockFetch.mockResolvedValueOnce({
46 | json: jest.fn().mockResolvedValue({ address: "localhost", key: "12345" }),
47 | });
48 |
49 | render(
50 |
51 |
52 | ClusterUI} />
53 |
54 |
55 | );
56 |
57 | await waitFor(() => expect(screen.getByText(/ClusterUI/i)).toBeInTheDocument());
58 | });
59 |
60 | test("redirects to /portal when init is false", async () => {
61 | mockFetch.mockResolvedValueOnce({
62 | json: jest.fn().mockResolvedValue({}),
63 | });
64 |
65 | render(
66 |
67 |
68 | ClusterUI} />
69 |
70 |
71 | );
72 |
73 | await waitFor(() => {
74 | expect(screen.queryByText(/ClusterUI/i)).not.toBeInTheDocument();
75 | expect(window.location.pathname).toBe("/portal");
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { Navigate } from "react-router-dom"
3 | import { useAppDispatch, useAppSelector } from "../../app/hooks"
4 | import { setInit, setAddress, setKey } from "./captivePortalSlice"
5 |
6 | interface prop {
7 | element: JSX.Element
8 | }
9 |
10 | export default function ProtectedRoute(props: prop) {
11 | const dispatch = useAppDispatch()
12 |
13 | const [loading, setLoading] = useState(true)
14 | const init = useAppSelector(state => state.portalSlice.init)
15 | //performs a fetch request to see if the environment file has been created. if the file exists and has a key or address, the information is assigned to global state and clusterui is rendered
16 |
17 | async function checkENVfileForCredentials() {
18 | const response = await fetch(`http://localhost:8080/api/checkenv`)
19 | const data = await response.json()
20 |
21 | if (data.address && data.key) {
22 | dispatch(setInit(true))
23 | dispatch(setKey(data.key))
24 | dispatch(setAddress(data.address))
25 | }
26 |
27 | setLoading(false)
28 | }
29 | checkENVfileForCredentials()
30 |
31 | if (loading) {
32 | return Loading...
33 | }
34 |
35 | if (init === true) {
36 | return props.element
37 | } else {
38 | return
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/captivePortalSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | const initialState = {
3 | address: "",
4 | key: "",
5 | init: false,
6 | };
7 | //Creates a slice in global state for address, key, and init
8 | export const portalSlice = createSlice({
9 | name: "portalSlice",
10 | initialState,
11 | reducers: {
12 | setAddress: (state, action) => {
13 | state.address = action.payload;
14 | },
15 | setKey: (state, action) => {
16 | state.key = action.payload;
17 | },
18 | setInit: (state, action) => {
19 | state.init = action.payload;
20 | },
21 | },
22 | });
23 | export const { setAddress, setKey, setInit } = portalSlice.actions;
24 | export default portalSlice.reducer;
25 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/captivePortalSlice.ts:
--------------------------------------------------------------------------------
1 | import type { PayloadAction } from "@reduxjs/toolkit"
2 | import { createSlice } from "@reduxjs/toolkit"
3 |
4 | interface initial {
5 | address: string
6 | key: string
7 | init: boolean
8 | }
9 |
10 | const initialState: initial = {
11 | address: "",
12 | key: "",
13 | init: false,
14 | }
15 | //Creates a slice in global state for address, key, and init
16 | export const portalSlice = createSlice({
17 | name: "portalSlice",
18 | initialState,
19 | reducers: {
20 | setAddress: (state, action: PayloadAction) => {
21 | state.address = action.payload
22 | },
23 | setKey: (state, action: PayloadAction) => {
24 | state.key = action.payload
25 | },
26 | setInit: (state, action: PayloadAction) => {
27 | state.init = action.payload
28 | },
29 | },
30 | })
31 | export const { setAddress, setKey, setInit } = portalSlice.actions
32 |
33 | export default portalSlice.reducer
34 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/components/CaptiveForm.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2 | import { useState } from "react";
3 | import { useAppDispatch } from "../../../app/hooks";
4 | import { setAddress, setKey } from "../captivePortalSlice";
5 | import { TextField, Button } from "@mui/material";
6 | import { Navigate } from "react-router-dom";
7 | import Stack from "@mui/material/Stack";
8 | import MuiCard from "@mui/material/Card";
9 | import FormLabel from "@mui/material/FormLabel";
10 | import FormControl from "@mui/material/FormControl";
11 | import Box from "@mui/material/Box";
12 | import { styled } from "@mui/material/styles";
13 | //Card styling for MUI
14 | const Card = styled(MuiCard)(({ theme }) => ({
15 | display: "flex",
16 | flexDirection: "column",
17 | alignSelf: "center",
18 | width: "100%",
19 | padding: theme.spacing(4),
20 | gap: theme.spacing(2),
21 | boxShadow: "hsla(220, 30%, 5%, 0.05) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.05) 0px 15px 35px -5px",
22 | [theme.breakpoints.up("sm")]: {
23 | width: "450px",
24 | },
25 | ...theme.applyStyles("dark", {
26 | boxShadow: "hsla(220, 30%, 5%, 0.5) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.08) 0px 15px 35px -5px",
27 | }),
28 | }));
29 | export default function CaptiveForm() {
30 | const dispatch = useAppDispatch();
31 | const [dest, setDest] = useState("");
32 | const [bearer, setBearer] = useState("");
33 | const [submit, setSubmit] = useState(false);
34 | const [error, setError] = useState("");
35 | //sends a fetch request to the backend to check if the API is valid when the submit button is pressed
36 | const submitHandler = (event) => {
37 | event.preventDefault();
38 | fetch("http://localhost:8080/api/checkAPI", {
39 | method: "POST",
40 | body: JSON.stringify({
41 | key: bearer,
42 | address: dest,
43 | }),
44 | headers: {
45 | "Content-Type": "application/json",
46 | },
47 | })
48 | .then(response => response.json())
49 | .then(data => {
50 | if (data.message !== "ok") {
51 | setError(JSON.stringify(data));
52 | }
53 | else {
54 | setSubmit(true);
55 | dispatch(setAddress(dest));
56 | dispatch(setKey(bearer));
57 | }
58 | });
59 | };
60 | const invalidKeyError = (_jsx("p", { children: "The URL of the Bearer Token entered is invalid. " }));
61 | //Once submitted information returns, users are let into the main clusterui
62 | if (submit === true) {
63 | return _jsx(Navigate, { to: "/clusterui" });
64 | }
65 | return (_jsx(_Fragment, { children: _jsxs("form", { onSubmit: submitHandler, children: [" ", _jsx(Stack, { direction: { xs: "column-reverse", md: "row" }, sx: {
66 | justifyContent: "center",
67 | gap: { xs: 6, sm: 12 },
68 | p: 2,
69 | m: "auto",
70 | }, children: _jsxs(Card, { variant: "outlined", children: [_jsxs(FormControl, { children: [_jsx(FormLabel, { htmlFor: "ip_or_url", children: "IP Address or URL" }), _jsx(TextField, { id: "ip_or_url", type: "url", name: "ip_or_url", placeholder: "http://192.168.1.1 or http://yourURL.com", onChange: input => setDest(input.target.value), autoFocus: true, required: true, fullWidth: true, variant: "outlined", error: !!error, sx: { ariaLabel: "ip_or_url" } })] }), _jsxs(FormControl, { children: [_jsx(Box, { sx: { display: "flex", justifyContent: "space-between" }, children: _jsx(FormLabel, { htmlFor: "bearer_token", children: "Bearer Token" }) }), _jsx(TextField, { name: "bearer_token", placeholder: "Bearer Token", type: "text", id: "password", required: true, fullWidth: true, variant: "outlined", error: !!error, onChange: input => setBearer(input.target.value) })] }), _jsx(Button, { type: "submit", fullWidth: true, variant: "contained", color: "violet", children: "View Cluster" }), error && invalidKeyError] }) })] }) }));
71 | }
72 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/components/CaptiveForm.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react"
2 | import { useState } from "react"
3 | import { useAppDispatch } from "../../../app/hooks"
4 | import { setAddress, setKey } from "../captivePortalSlice"
5 | import { TextField, Button } from "@mui/material"
6 | import { Navigate } from "react-router-dom"
7 |
8 | import Stack from "@mui/material/Stack"
9 | import MuiCard from "@mui/material/Card"
10 | import FormLabel from "@mui/material/FormLabel"
11 | import FormControl from "@mui/material/FormControl"
12 | import Box from "@mui/material/Box"
13 |
14 | import { styled } from "@mui/material/styles"
15 |
16 | //Card styling for MUI
17 | const Card = styled(MuiCard)(({ theme }) => ({
18 | display: "flex",
19 | flexDirection: "column",
20 | alignSelf: "center",
21 | width: "100%",
22 | padding: theme.spacing(4),
23 | gap: theme.spacing(2),
24 | boxShadow:
25 | "hsla(220, 30%, 5%, 0.05) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.05) 0px 15px 35px -5px",
26 | [theme.breakpoints.up("sm")]: {
27 | width: "450px",
28 | },
29 | ...theme.applyStyles("dark", {
30 | boxShadow:
31 | "hsla(220, 30%, 5%, 0.5) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.08) 0px 15px 35px -5px",
32 | }),
33 | }))
34 |
35 | export default function CaptiveForm() {
36 | const dispatch = useAppDispatch()
37 | const [dest, setDest] = useState("")
38 | const [bearer, setBearer] = useState("")
39 | const [submit, setSubmit] = useState(false)
40 | const [error, setError] = useState("")
41 | //sends a fetch request to the backend to check if the API is valid when the submit button is pressed
42 | const submitHandler = (event: React.FormEvent) => {
43 | event.preventDefault()
44 |
45 | fetch("http://localhost:8080/api/checkAPI", {
46 | method: "POST",
47 | body: JSON.stringify({
48 | key: bearer,
49 | address: dest,
50 | }),
51 | headers: {
52 | "Content-Type": "application/json",
53 | },
54 | })
55 | .then(response => response.json())
56 | .then(data => {
57 | if (data.message !== "ok") {
58 | setError(JSON.stringify(data))
59 | } else {
60 | setSubmit(true)
61 | dispatch(setAddress(dest))
62 | dispatch(setKey(bearer))
63 | }
64 | })
65 | }
66 |
67 | const invalidKeyError = (
68 | The URL of the Bearer Token entered is invalid.
69 | )
70 | //Once submitted information returns, users are let into the main clusterui
71 | if (submit === true) {
72 | return
73 | }
74 |
75 | return (
76 | <>
77 |
131 | >
132 | )
133 | }
134 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/components/CaptiveFormContent.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import Box from "@mui/material/Box";
3 | import Stack from "@mui/material/Stack";
4 | import Typography from "@mui/material/Typography";
5 | import ThumbUpAltRoundedIcon from "@mui/icons-material/ThumbUpAltRounded";
6 | import DnsRoundedIcon from "@mui/icons-material/DnsRounded";
7 | import VpnKeyRoundedIcon from "@mui/icons-material/VpnKeyRounded";
8 | //Instruction components for filling out the address and key
9 | const items = [
10 | {
11 | icon: _jsx(DnsRoundedIcon, { sx: { color: "text.secondary" } }),
12 | title: "Cluster IP Address or URL",
13 | description: "Enter the IP address or URL of your Kubernetes cluster. This will allow the tool to connect and retrieve the necessary information to visualize your cluster setup.",
14 | },
15 | {
16 | icon: _jsx(VpnKeyRoundedIcon, { sx: { color: "text.secondary" } }),
17 | title: "Cluster Credentials",
18 | description: "Provide your Kubernetes credentials to authenticate access. Ensure that the credentials have sufficient permissions to access and interact with cluster resources.",
19 | },
20 | {
21 | icon: _jsx(ThumbUpAltRoundedIcon, { sx: { color: "text.secondary" } }),
22 | title: "View Clusterview",
23 | description: 'After entering the necessary details, hit the "View Clusterview" button to dynamically render your cluster. Continue exploring for a seamless and interactive visualization experience.',
24 | },
25 | ];
26 | export default function CaptiveFormContent() {
27 | return (_jsxs(Stack, { sx: {
28 | flexDirection: "column",
29 | alignSelf: "center",
30 | gap: 4,
31 | maxWidth: 450,
32 | }, children: [_jsx(Box, { sx: { display: { xs: "none", md: "flex" } } }), items.map((item, index) => (_jsxs(Stack, { direction: "row", sx: { gap: 2 }, children: [item.icon, _jsxs("div", { children: [_jsx(Typography, { gutterBottom: true, sx: { fontWeight: "medium" }, children: item.title }), _jsx(Typography, { variant: "body2", sx: { color: "text.secondary" }, children: item.description })] })] }, index)))] }));
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/components/CaptiveFormContent.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Box from "@mui/material/Box"
3 | import Stack from "@mui/material/Stack"
4 | import Typography from "@mui/material/Typography"
5 | import ThumbUpAltRoundedIcon from "@mui/icons-material/ThumbUpAltRounded"
6 | import DnsRoundedIcon from "@mui/icons-material/DnsRounded"
7 | import VpnKeyRoundedIcon from "@mui/icons-material/VpnKeyRounded"
8 | //Instruction components for filling out the address and key
9 | const items = [
10 | {
11 | icon: ,
12 | title: "Cluster IP Address or URL",
13 | description:
14 | "Enter the IP address or URL of your Kubernetes cluster. This will allow the tool to connect and retrieve the necessary information to visualize your cluster setup.",
15 | },
16 | {
17 | icon: ,
18 | title: "Cluster Credentials",
19 | description:
20 | "Provide your Kubernetes credentials to authenticate access. Ensure that the credentials have sufficient permissions to access and interact with cluster resources.",
21 | },
22 | {
23 | icon: ,
24 | title: "View Clusterview",
25 | description:
26 | 'After entering the necessary details, hit the "View Clusterview" button to dynamically render your cluster. Continue exploring for a seamless and interactive visualization experience.',
27 | },
28 | ]
29 |
30 | export default function CaptiveFormContent() {
31 | return (
32 |
40 |
41 | {items.map((item, index) => (
42 |
43 | {item.icon}
44 |
45 |
46 | {item.title}
47 |
48 |
49 | {item.description}
50 |
51 |
52 |
53 | ))}
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/containers/CaptivePortal.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import Stack from "@mui/material/Stack";
3 | import CaptiveForm from "../components/CaptiveForm";
4 | import CaptiveFormContent from "../components/CaptiveFormContent";
5 | export default function CaptivePortal() {
6 | //Container to render the captive form information
7 | return (_jsxs(Stack, { direction: { xs: "column-reverse", md: "row" }, sx: {
8 | display: "flex",
9 | justifyContent: "center",
10 | alignItems: "center",
11 | height: "100vh",
12 | }, children: [_jsx(CaptiveFormContent, {}), _jsx(CaptiveForm, {})] }));
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/containers/CaptivePortal.test.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { screen, fireEvent, cleanup } from "@testing-library/react";
3 | import { renderWithProviders } from "../../../utils/test-utils";
4 | import CaptivePortal from "./CaptivePortal";
5 | import { createTheme, ThemeProvider, alpha, getContrastRatio, } from "@mui/material/styles";
6 | describe("check if captive portal renders", () => {
7 | const violetBase = "#7F00FF";
8 | const violetMain = alpha(violetBase, 0.7);
9 | const theme = createTheme({
10 | palette: {
11 | violet: {
12 | main: violetMain,
13 | light: alpha(violetBase, 0.5),
14 | dark: alpha(violetBase, 0.9),
15 | contrastText: getContrastRatio(violetMain, "#fff") > 4.5 ? "#fff" : "#111",
16 | },
17 | },
18 | typography: {
19 | fontFamily: '"Roboto", sans-serif',
20 | h1: {
21 | fontFamily: '"Oswald", sans-serif',
22 | fontWeight: 900,
23 | },
24 | },
25 | });
26 | beforeEach(() => {
27 | renderWithProviders(_jsx(ThemeProvider, { theme: theme, children: _jsx(CaptivePortal, {}) }));
28 | });
29 | afterEach(() => {
30 | cleanup();
31 | });
32 | test("Check if the input boxes and button exist", () => {
33 | const address = screen.getByLabelText(/IP Address or URL/i);
34 | const token = screen.getByPlaceholderText(/Bearer Token/i);
35 | const button = screen.getByRole("button", {
36 | name: /View Cluster/i,
37 | });
38 | expect(address).toBeInTheDocument();
39 | expect(token).toBeInTheDocument();
40 | expect(button).toBeInTheDocument();
41 | });
42 | test("Check if the address input accepts only urls", () => {
43 | const address = screen.getByLabelText(/IP Address or URL/i);
44 | fireEvent.change(address, { target: { value: "jahgifjmzxiwek" } });
45 | expect(address.validity.typeMismatch).toBeTruthy();
46 | expect(address.validity.valid).toBeFalsy();
47 | fireEvent.change(address, { target: { value: "https://test.com" } });
48 | expect(address.validity.typeMismatch).toBeFalsy();
49 | expect(address.validity.valid).toBeTruthy();
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/containers/CaptivePortal.test.tsx:
--------------------------------------------------------------------------------
1 | import { screen, fireEvent, cleanup } from "@testing-library/react"
2 | import { renderWithProviders } from "../../../utils/test-utils"
3 | import CaptivePortal from "./CaptivePortal"
4 | import {
5 | createTheme,
6 | ThemeProvider,
7 | alpha,
8 | getContrastRatio,
9 | } from "@mui/material/styles"
10 |
11 | describe("check if captive portal renders", () => {
12 | const violetBase = "#7F00FF"
13 | const violetMain = alpha(violetBase, 0.7)
14 |
15 | const theme = createTheme({
16 | palette: {
17 | violet: {
18 | main: violetMain,
19 | light: alpha(violetBase, 0.5),
20 | dark: alpha(violetBase, 0.9),
21 | contrastText:
22 | getContrastRatio(violetMain, "#fff") > 4.5 ? "#fff" : "#111",
23 | },
24 | },
25 | typography: {
26 | fontFamily: '"Roboto", sans-serif',
27 | h1: {
28 | fontFamily: '"Oswald", sans-serif',
29 | fontWeight: 900,
30 | },
31 | },
32 | })
33 |
34 | beforeEach(() => {
35 | renderWithProviders(
36 |
37 |
38 | ,
39 | )
40 | })
41 | afterEach(() => {
42 | cleanup()
43 | })
44 |
45 | test("Check if the input boxes and button exist", () => {
46 | const address: HTMLInputElement = screen.getByLabelText(
47 | /IP Address or URL/i,
48 | ) as HTMLInputElement
49 | const token: HTMLElement = screen.getByPlaceholderText(/Bearer Token/i)
50 | const button: HTMLElement = screen.getByRole("button", {
51 | name: /View Cluster/i,
52 | })
53 |
54 | expect(address).toBeInTheDocument()
55 | expect(token).toBeInTheDocument()
56 | expect(button).toBeInTheDocument()
57 | })
58 |
59 | test("Check if the address input accepts only urls", () => {
60 | const address: HTMLInputElement = screen.getByLabelText(
61 | /IP Address or URL/i,
62 | ) as HTMLInputElement
63 |
64 | fireEvent.change(address, { target: { value: "jahgifjmzxiwek" } })
65 |
66 | expect(address.validity.typeMismatch).toBeTruthy()
67 | expect(address.validity.valid).toBeFalsy()
68 | fireEvent.change(address, { target: { value: "https://test.com" } })
69 | expect(address.validity.typeMismatch).toBeFalsy()
70 | expect(address.validity.valid).toBeTruthy()
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/client/src/features/captive-portal/containers/CaptivePortal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Stack from "@mui/material/Stack"
3 |
4 | import CaptiveForm from "../components/CaptiveForm"
5 | import CaptiveFormContent from "../components/CaptiveFormContent"
6 |
7 | export default function CaptivePortal() {
8 | //Container to render the captive form information
9 | return (
10 |
19 |
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/clusterLogsApiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 | import { createSlice } from "@reduxjs/toolkit";
3 | // Define an API service for the log page
4 | export const clusterLogsApi = createApi({
5 | reducerPath: "clusterLogsApi",
6 | baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:8080/" }),
7 | endpoints: builder => ({
8 | getClusterLogs: builder.query({
9 | query: () => "api/getLogs",
10 | }),
11 | }),
12 | });
13 | // Auto-generated hooks for the API queries
14 | export const { useGetClusterLogsQuery } = clusterLogsApi;
15 | const initialState = {
16 | logs: ["no logs"],
17 | };
18 | export const clusterLogsSlice = createSlice({
19 | name: "clusterLogs",
20 | initialState,
21 | reducers: {},
22 | });
23 | // Selectors for any additional state managed in this slice
24 | export const selectClusterLogs = (state) => state.clusterLogs.logs;
25 | export default clusterLogsSlice.reducer;
26 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/clusterLogsApiSlice.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
2 | import { createSlice } from "@reduxjs/toolkit"
3 |
4 | export interface ClusterLog {
5 | name: string
6 | log: Log[]
7 | }
8 |
9 | export interface Log {
10 | date: string
11 | logs: string
12 | name: string
13 | namespace: string
14 | }
15 |
16 | // Define an API service for the log page
17 | export const clusterLogsApi = createApi({
18 | reducerPath: "clusterLogsApi",
19 | baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:8080/" }),
20 | endpoints: builder => ({
21 | getClusterLogs: builder.query({
22 | query: () => "api/getLogs",
23 | }),
24 | }),
25 | })
26 |
27 | // Auto-generated hooks for the API queries
28 | export const { useGetClusterLogsQuery } = clusterLogsApi
29 |
30 | export interface ClusterLogsState {
31 | logs: string[]
32 | }
33 |
34 | const initialState: ClusterLogsState = {
35 | logs: ["no logs"],
36 | }
37 |
38 | export const clusterLogsSlice = createSlice({
39 | name: "clusterLogs",
40 | initialState,
41 | reducers: {},
42 | })
43 |
44 | // Selectors for any additional state managed in this slice
45 | export const selectClusterLogs = (state: { clusterLogs: ClusterLogsState }) =>
46 | state.clusterLogs.logs
47 |
48 | export default clusterLogsSlice.reducer
49 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/components/ClusterLog.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import * as React from "react";
3 | import Accordion from "@mui/material/Accordion";
4 | import AccordionSummary from "@mui/material/AccordionSummary";
5 | import AccordionDetails from "@mui/material/AccordionDetails";
6 | import Typography from "@mui/material/Typography";
7 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
8 | import Button from "@mui/material/Button";
9 | import { useGetClusterLogsQuery } from "../clusterLogsApiSlice";
10 | export default function ClusterLog(props) {
11 | const { clusterLog } = props;
12 | const [expanded, setExpanded] = React.useState(false);
13 | const { refetch: refetchClusterLogs } = useGetClusterLogsQuery();
14 | //opens and closes the accordion
15 | const handleExpansion = () => {
16 | setExpanded(prevExpanded => !prevExpanded);
17 | };
18 | //requests data from the backend to download the log
19 | const downloadLogHandler = () => {
20 | async function sendDownloadLogRequest() {
21 | try {
22 | const response = await fetch(`http://localhost:8080/api/getDownloadLogs/${clusterLog.name}`);
23 | const blob = await response.blob();
24 | const url = await window.URL.createObjectURL(new Blob([blob]));
25 | const link = await document.createElement("a");
26 | link.href = url;
27 | link.setAttribute("download", clusterLog.name);
28 | document.body.appendChild(link);
29 | link.click();
30 | if (link.parentNode) {
31 | link.parentNode.removeChild(link);
32 | }
33 | window.URL.revokeObjectURL(url);
34 | }
35 | catch (error) {
36 | throw new Error(`Something went wrong: ${error.message}`);
37 | }
38 | }
39 | sendDownloadLogRequest();
40 | };
41 | //deletes a log when the user selects delete log
42 | const deleteLogHandler = () => {
43 | async function sendDeleteLogRequest() {
44 | try {
45 | await fetch(`http://localhost:8080/api/deleteLogs/${clusterLog.name}`, {
46 | method: "DELETE",
47 | });
48 | setExpanded(prevExpanded => !prevExpanded);
49 | await refetchClusterLogs();
50 | }
51 | catch (error) {
52 | throw new Error(`Something went wrong: ${error.message}`);
53 | }
54 | }
55 | sendDeleteLogRequest();
56 | };
57 | return (
58 | //each log is placed in an accordion and can expand and shrink.
59 | _jsx("div", { children: _jsxs(Accordion, { expanded: expanded, onChange: handleExpansion, style: {
60 | position: "relative",
61 | width: "700px",
62 | top: "120px",
63 | }, sx: [
64 | expanded
65 | ? {
66 | "& .MuiAccordion-region": {
67 | height: "auto",
68 | },
69 | "& .MuiAccordionDetails-root": {
70 | display: "block",
71 | },
72 | }
73 | : {
74 | "& .MuiAccordion-region": {
75 | height: 0,
76 | },
77 | "& .MuiAccordionDetails-root": {
78 | display: "none",
79 | },
80 | },
81 | ], children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMoreIcon, {}), "aria-controls": "panel1-content", id: "panel1-header", children: _jsxs(Typography, { children: [_jsx("strong", { children: "Log Instance:" }), "\u00A0 ", clusterLog.name, " "] }) }), _jsxs(AccordionDetails, { children: [clusterLog.log.map((log, i) => (_jsxs(Typography, { children: [_jsxs("span", { children: [_jsx("strong", { children: "Date:\u00A0" }), log.date] }), _jsx("br", {}), _jsxs("span", { children: [_jsx("strong", { children: "Podname:\u00A0" }), log.name] }), _jsx("br", {}), _jsxs("span", { children: [_jsx("strong", { children: "Log:\u00A0" }), log.logs] }), _jsx("br", {}), _jsx("br", {})] }, i * 938))), _jsx(Button, { "aria-label": "Download", style: { margin: "16px" }, variant: "contained", color: "primary", type: "button", onClick: downloadLogHandler, children: "Download" }), _jsx(Button, { style: { margin: "16px" }, variant: "contained", color: "primary", type: "button", onClick: deleteLogHandler, children: "Delete" })] })] }) }));
82 | }
83 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/components/ClusterLog.test.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { afterEach, test } from "vitest";
3 | import { screen, cleanup } from "@testing-library/react";
4 | import userEvent from "@testing-library/user-event";
5 | import { renderWithProviders } from "../../../utils/test-utils";
6 | import ClusterLog from "./ClusterLog";
7 | describe("if cluster log page renders", () => {
8 | const sampleClusterLog = {
9 | name: "log-2024-9-10-11-57-17.json",
10 | log: [
11 | {
12 | date: "September 10, 2024",
13 | logs: "\n> mock-server@1.0.0 start /usr/src/app\n> node server/server.js\n\nServer listening on port: 3000\n",
14 | name: "mock-app-deployment-7fdbd7448d-79mtk",
15 | namespace: "default",
16 | },
17 | {
18 | date: "September 10, 2024",
19 | logs: "\n> mock-server@1.0.0 start /usr/src/app\n> node server/server.js\n\nServer listening on port: 3000\n",
20 | name: "mock-app-deployment-7fdbd7448d-gn7c6",
21 | namespace: "default",
22 | },
23 | ],
24 | };
25 | beforeEach(() => {
26 | renderWithProviders(_jsx(ClusterLog, { clusterLog: sampleClusterLog }));
27 | });
28 | afterEach(() => {
29 | cleanup();
30 | });
31 | test("if a cluster log renders with download button", async () => {
32 | // Simulate expanding the accordion to show its content
33 | const accordionToggle = screen.getByRole("button", {
34 | name: /log instance/i,
35 | });
36 | await userEvent.click(accordionToggle);
37 | // Try to find the "Download" button after expanding the accordion
38 | const downloadLogButton = await screen.findByRole("button", {
39 | name: /Download/i,
40 | });
41 | expect(downloadLogButton).toBeInTheDocument();
42 | });
43 | test("if a cluster log renders with delete buttons", async () => {
44 | // Simulate expanding the accordion to show its content
45 | const accordionToggle = screen.getByRole("button", {
46 | name: /log instance/i,
47 | });
48 | await userEvent.click(accordionToggle);
49 | const deleteLogButton = await screen.findByRole("button", {
50 | name: /Delete/i,
51 | });
52 | expect(deleteLogButton).toBeInTheDocument();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/components/ClusterLog.test.tsx:
--------------------------------------------------------------------------------
1 | import { afterEach, test } from "vitest"
2 | import { screen, cleanup } from "@testing-library/react"
3 | import userEvent from "@testing-library/user-event"
4 | import { renderWithProviders } from "../../../utils/test-utils"
5 | import ClusterLog from "./ClusterLog"
6 |
7 | describe("if cluster log page renders", () => {
8 | const sampleClusterLog = {
9 | name: "log-2024-9-10-11-57-17.json",
10 | log: [
11 | {
12 | date: "September 10, 2024",
13 | logs: "\n> mock-server@1.0.0 start /usr/src/app\n> node server/server.js\n\nServer listening on port: 3000\n",
14 | name: "mock-app-deployment-7fdbd7448d-79mtk",
15 | namespace: "default",
16 | },
17 | {
18 | date: "September 10, 2024",
19 | logs: "\n> mock-server@1.0.0 start /usr/src/app\n> node server/server.js\n\nServer listening on port: 3000\n",
20 | name: "mock-app-deployment-7fdbd7448d-gn7c6",
21 | namespace: "default",
22 | },
23 | ],
24 | }
25 | beforeEach(() => {
26 | renderWithProviders()
27 | })
28 | afterEach(() => {
29 | cleanup()
30 | })
31 |
32 | test("if a cluster log renders with download button", async () => {
33 | // Simulate expanding the accordion to show its content
34 | const accordionToggle = screen.getByRole("button", {
35 | name: /log instance/i,
36 | })
37 | await userEvent.click(accordionToggle)
38 |
39 | // Try to find the "Download" button after expanding the accordion
40 | const downloadLogButton = await screen.findByRole("button", {
41 | name: /Download/i,
42 | })
43 | expect(downloadLogButton).toBeInTheDocument()
44 | })
45 |
46 | test("if a cluster log renders with delete buttons", async () => {
47 | // Simulate expanding the accordion to show its content
48 | const accordionToggle = screen.getByRole("button", {
49 | name: /log instance/i,
50 | })
51 | await userEvent.click(accordionToggle)
52 |
53 | const deleteLogButton = await screen.findByRole("button", {
54 | name: /Delete/i,
55 | })
56 |
57 | expect(deleteLogButton).toBeInTheDocument()
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/components/ClusterLog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import Accordion from "@mui/material/Accordion"
4 | import AccordionSummary from "@mui/material/AccordionSummary"
5 | import AccordionDetails from "@mui/material/AccordionDetails"
6 | import Typography from "@mui/material/Typography"
7 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"
8 | import Button from "@mui/material/Button"
9 |
10 | import type { Log, ClusterLog as ClusterLogType } from "../clusterLogsApiSlice"
11 |
12 | import { useGetClusterLogsQuery } from "../clusterLogsApiSlice"
13 |
14 | interface Props {
15 | clusterLog: ClusterLogType
16 | }
17 |
18 | export default function ClusterLog(props: Props) {
19 | const { clusterLog } = props
20 |
21 | const [expanded, setExpanded] = React.useState(false)
22 |
23 | const { refetch: refetchClusterLogs } = useGetClusterLogsQuery()
24 | //opens and closes the accordion
25 | const handleExpansion = () => {
26 | setExpanded(prevExpanded => !prevExpanded)
27 | }
28 | //requests data from the backend to download the log
29 | const downloadLogHandler = (): void => {
30 | async function sendDownloadLogRequest(): Promise {
31 | try {
32 | const response = await fetch(
33 | `http://localhost:8080/api/getDownloadLogs/${clusterLog.name}`,
34 | )
35 | const blob = await response.blob()
36 | const url = await window.URL.createObjectURL(new Blob([blob]))
37 | const link = await document.createElement("a")
38 |
39 | link.href = url
40 | link.setAttribute("download", clusterLog.name)
41 | document.body.appendChild(link)
42 | link.click()
43 |
44 | if (link.parentNode) {
45 | link.parentNode.removeChild(link)
46 | }
47 | window.URL.revokeObjectURL(url)
48 | } catch (error) {
49 | throw new Error(`Something went wrong: ${(error as Error).message}`)
50 | }
51 | }
52 |
53 | sendDownloadLogRequest()
54 | }
55 | //deletes a log when the user selects delete log
56 | const deleteLogHandler = (): void => {
57 | async function sendDeleteLogRequest() {
58 | try {
59 | await fetch(`http://localhost:8080/api/deleteLogs/${clusterLog.name}`, {
60 | method: "DELETE",
61 | })
62 | setExpanded(prevExpanded => !prevExpanded)
63 | await refetchClusterLogs()
64 | } catch (error) {
65 | throw new Error(`Something went wrong: ${(error as Error).message}`)
66 | }
67 | }
68 | sendDeleteLogRequest()
69 | }
70 |
71 | return (
72 | //each log is placed in an accordion and can expand and shrink.
73 |
74 |
102 | }
104 | aria-controls="panel1-content"
105 | id="panel1-header"
106 | >
107 |
108 | Log Instance:
109 | {clusterLog.name}{" "}
110 |
111 |
112 |
113 |
114 | {clusterLog.log.map((log: Log, i: number) => (
115 |
116 |
117 | Date:
118 | {log.date}
119 |
120 |
121 |
122 | Podname:
123 | {log.name}
124 |
125 |
126 |
127 | Log:
128 | {log.logs}
129 |
130 |
131 |
132 |
133 | ))}
134 |
135 |
145 |
154 |
155 |
156 |
157 | )
158 | }
159 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/containers/ClusterLogContainer.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import { useState, useEffect } from "react";
3 | import { Button, Box } from "@mui/material";
4 | // import Row from "../Row"
5 | // import {ClusterLog} from "../clusterLogsApiSlice"
6 | import { useGetClusterLogsQuery, } from "../clusterLogsApiSlice";
7 | import ClusterLog from "../components/ClusterLog";
8 | export default function LogPage() {
9 | const [dirInfo, setdirInfo] = useState([]);
10 | const [log, setLog] = useState([]);
11 | const [deleted, setDeleted] = useState("");
12 | useEffect(() => {
13 | fetch("http://localhost:8080/api/getLogs")
14 | .then(process => process.json())
15 | .then(data => {
16 | //console.log(data);
17 | setdirInfo(data);
18 | })
19 | .catch(error => {
20 | console.log(error);
21 | });
22 | }, [log, deleted]);
23 | const createLogHandler = () => {
24 | fetch("http://localhost:8080/api/createLogs", {
25 | method: "POST",
26 | })
27 | .then(process => process.json())
28 | .then(data => {
29 | //console.log(data);
30 | setLog(data);
31 | })
32 | .catch(error => {
33 | console.log(error);
34 | });
35 | };
36 | const store = [];
37 | for (let i = dirInfo.length; i > 0; i--) {
38 | if (dirInfo[i] !== (null || undefined)) {
39 | store.push(_jsx(Box, { children: _jsx(ClusterLog, { setDeleted: setDeleted, logName: dirInfo[i] }) }, i * 123));
40 | }
41 | }
42 | const { data: clusterLog, isLoading: clusterLogIsLoading, isError: clusterLogError, refetch: refetchClusterLogs, } = useGetClusterLogsQuery();
43 | console.log("useGetClusterLogsQuery.data: ", clusterLog);
44 | return (_jsxs("div", { style: { position: "absolute", left: "250px", top: "100px" }, children: [_jsx("h1", { style: {
45 | textAlign: "center",
46 | marginLeft: "32px",
47 | marginBottom: "16px",
48 | minWidth: "700px",
49 | }, children: "Logs" }), _jsx(Button, { style: {
50 | display: "flex",
51 | left: "290px",
52 | marginLeft: "32px",
53 | marginBottom: "16px",
54 | }, variant: "contained", color: "primary", type: "button", onClick: createLogHandler, children: "Create a Log" }), store] }));
55 | }
56 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/containers/ClusterLogContainer.test.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { afterEach, test } from "vitest";
3 | import { screen, cleanup } from "@testing-library/react";
4 | import { renderWithProviders } from "../../../utils/test-utils";
5 | import ClusterLogContainer from "../containers/ClusterLogContainer";
6 | afterEach(() => {
7 | cleanup();
8 | });
9 | test("Cluster Log Container should render", () => {
10 | renderWithProviders(_jsx(ClusterLogContainer, {}));
11 | // buttons to load correctly
12 | const createLogButton = screen.getByRole("button", { name: /Create A Log/i });
13 | const deleteLogButton = screen.getByRole("button", {
14 | name: /Delete All Logs/i,
15 | });
16 | // Test for Body text
17 | expect(createLogButton).toBeInTheDocument();
18 | expect(deleteLogButton).toBeInTheDocument();
19 | });
20 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/containers/ClusterLogContainer.test.tsx:
--------------------------------------------------------------------------------
1 | import { afterEach, test } from "vitest"
2 | import { screen, cleanup } from "@testing-library/react"
3 | import { renderWithProviders } from "../../../utils/test-utils"
4 |
5 | import ClusterLogContainer from "../containers/ClusterLogContainer"
6 |
7 | afterEach(() => {
8 | cleanup()
9 | })
10 |
11 | test("Cluster Log Container should render", () => {
12 | renderWithProviders()
13 |
14 | // buttons to load correctly
15 | const createLogButton = screen.getByRole("button", { name: /Create A Log/i })
16 | const deleteLogButton = screen.getByRole("button", {
17 | name: /Delete All Logs/i,
18 | })
19 |
20 | // Test for Body text
21 | expect(createLogButton).toBeInTheDocument()
22 | expect(deleteLogButton).toBeInTheDocument()
23 | })
24 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/containers/ClusterLogContainer.tsx:
--------------------------------------------------------------------------------
1 | // import type React from "react"
2 | import { useState } from "react"
3 | import { Button } from "@mui/material"
4 |
5 | // Alert Dialog imports
6 | import * as React from "react"
7 | import Dialog from "@mui/material/Dialog"
8 | import DialogActions from "@mui/material/DialogActions"
9 | import DialogContent from "@mui/material/DialogContent"
10 | import DialogContentText from "@mui/material/DialogContentText"
11 | import DialogTitle from "@mui/material/DialogTitle"
12 |
13 | import { useGetClusterLogsQuery } from "../clusterLogsApiSlice"
14 |
15 | import ClusterLog from "../components/ClusterLog"
16 |
17 | export default function LogPage() {
18 | const [open, setOpen] = useState(false)
19 | //creates a log in the backend once the create log button is pressed.
20 | const createLogHandler = (): void => {
21 | async function sendCreateLogRequest() {
22 | try {
23 | await fetch("http://localhost:8080/api/createLogs", {
24 | method: "POST",
25 | })
26 | await refetchClusterLogs()
27 | } catch (error) {
28 | throw new Error(`Something went wrong: ${(error as Error).message}`)
29 | }
30 | }
31 |
32 | sendCreateLogRequest()
33 | }
34 | //RTK Query to grab log info and to refetch if desired
35 | const { data: clusterLogs, refetch: refetchClusterLogs } =
36 | useGetClusterLogsQuery()
37 | //handlers for dialogues and for state modifications.
38 | function AlertDialog() {
39 | const handleClickOpen = () => {
40 | setOpen(true)
41 | }
42 |
43 | const handleClose = () => {
44 | setOpen(false)
45 | }
46 |
47 | const handleCloseCancel = () => {
48 | setOpen(false)
49 | }
50 |
51 | const handleCloseConfirm = () => {
52 | setOpen(false)
53 | deleteLogHandler()
54 | }
55 |
56 | return (
57 | // delete all confirmation prompt
58 |
59 |
62 |
84 |
85 | )
86 | }
87 |
88 | const confirmDeleteAll = () => {
89 | setOpen(true)
90 | }
91 | //deletes all the logs if the user accepts the confirmation prompt
92 | const deleteLogHandler = async (): Promise => {
93 | if (!clusterLogs || clusterLogs.length === 0) {
94 | return
95 | }
96 |
97 | try {
98 | await Promise.all(
99 | clusterLogs.map(async (log): Promise => {
100 | try {
101 | await fetch(`http://localhost:8080/api/deleteLogs/${log.name}`, {
102 | method: "DELETE",
103 | })
104 | } catch (error) {
105 | throw new Error(`Something went wrong: ${(error as Error).message}`)
106 | }
107 | }),
108 | )
109 | await refetchClusterLogs()
110 | } catch (error) {
111 | throw new Error(`Something went wrong: ${(error as Error).message}`)
112 | }
113 | }
114 |
115 | return (
116 | // holds, styles, and displays the logs and buttons
117 |
126 |
137 | Logs
138 |
139 |
140 |
155 |
172 |
173 | {clusterLogs?.map((clusterLog, i) => (
174 |
175 | ))}
176 |
186 | {open === true &&
}
187 |
188 |
189 | )
190 | }
191 |
--------------------------------------------------------------------------------
/client/src/features/cluster-log/containers/clusterLogContainer.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | // import type React from "react"
3 | import { useState } from "react";
4 | import { Button } from "@mui/material";
5 | // Alert Dialog imports
6 | import * as React from "react";
7 | import Dialog from "@mui/material/Dialog";
8 | import DialogActions from "@mui/material/DialogActions";
9 | import DialogContent from "@mui/material/DialogContent";
10 | import DialogContentText from "@mui/material/DialogContentText";
11 | import DialogTitle from "@mui/material/DialogTitle";
12 | import { useGetClusterLogsQuery } from "../clusterLogsApiSlice";
13 | import ClusterLog from "../components/ClusterLog";
14 | export default function LogPage() {
15 | const [open, setOpen] = useState(false);
16 | //creates a log in the backend once the create log button is pressed.
17 | const createLogHandler = () => {
18 | async function sendCreateLogRequest() {
19 | try {
20 | await fetch("http://localhost:8080/api/createLogs", {
21 | method: "POST",
22 | });
23 | await refetchClusterLogs();
24 | }
25 | catch (error) {
26 | throw new Error(`Something went wrong: ${error.message}`);
27 | }
28 | }
29 | sendCreateLogRequest();
30 | };
31 | //RTK Query to grab log info and to refetch if desired
32 | const { data: clusterLogs, refetch: refetchClusterLogs } = useGetClusterLogsQuery();
33 | //handlers for dialogues and for state modifications.
34 | function AlertDialog() {
35 | const handleClickOpen = () => {
36 | setOpen(true);
37 | };
38 | const handleClose = () => {
39 | setOpen(false);
40 | };
41 | const handleCloseCancel = () => {
42 | setOpen(false);
43 | };
44 | const handleCloseConfirm = () => {
45 | setOpen(false);
46 | deleteLogHandler();
47 | };
48 | return (
49 | // delete all confirmation prompt
50 | _jsxs(React.Fragment, { children: [_jsx(Button, { variant: "outlined", onClick: handleClickOpen, children: "Delete Logs" }), _jsxs(Dialog, { open: open, onClose: handleClose, "aria-labelledby": "alert-dialog-title", "aria-describedby": "alert-dialog-description", children: [_jsx(DialogTitle, { id: "alert-dialog-title", children: "Are you sure you want to delete all logs?" }), _jsx(DialogContent, { children: _jsx(DialogContentText, { id: "alert-dialog-description", children: "This action is irreversable, please confirm you would like to delete ALL logs." }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: handleCloseCancel, children: "Cancel" }), _jsx(Button, { onClick: handleCloseConfirm, autoFocus: true, children: "Confirm" })] })] })] }));
51 | }
52 | const confirmDeleteAll = () => {
53 | setOpen(true);
54 | };
55 | //deletes all the logs if the user accepts the confirmation prompt
56 | const deleteLogHandler = async () => {
57 | if (!clusterLogs || clusterLogs.length === 0) {
58 | return;
59 | }
60 | try {
61 | await Promise.all(clusterLogs.map(async (log) => {
62 | try {
63 | await fetch(`http://localhost:8080/api/deleteLogs/${log.name}`, {
64 | method: "DELETE",
65 | });
66 | }
67 | catch (error) {
68 | throw new Error(`Something went wrong: ${error.message}`);
69 | }
70 | }));
71 | await refetchClusterLogs();
72 | }
73 | catch (error) {
74 | throw new Error(`Something went wrong: ${error.message}`);
75 | }
76 | };
77 | return (
78 | // holds, styles, and displays the logs and buttons
79 | _jsxs("div", { style: {
80 | position: "relative",
81 | left: "225px",
82 | top: "90px",
83 | width: "100vw",
84 | height: "100vh",
85 | }, children: [_jsx("h1", { style: {
86 | position: "absolute",
87 | textAlign: "center",
88 | marginLeft: "32px",
89 | marginBottom: "16px",
90 | minWidth: "700px",
91 | left: "-50px",
92 | top: "35px",
93 | }, children: "Logs" }), _jsxs("div", { className: "log-button-container", children: [_jsx(Button, { style: {
94 | display: "absolute",
95 | left: "130px",
96 | top: "100px",
97 | marginLeft: "32px",
98 | marginBottom: "16px",
99 | }, variant: "contained", color: "secondary", type: "button", onClick: createLogHandler, children: "Create a Log" }), _jsx(Button, { id: "delete-all-logs-button", style: {
100 | display: "absolute",
101 | left: "190px",
102 | top: "100px",
103 | marginLeft: "32px",
104 | marginBottom: "16px",
105 | }, variant: "contained", color: "error", type: "button", onClick: confirmDeleteAll, disabled: !clusterLogs || clusterLogs.length === 0 ? true : false, children: "Delete all Logs" })] }), clusterLogs?.map((clusterLog, i) => (_jsx(ClusterLog, { clusterLog: clusterLog }, `clusterLog:${i}`))), _jsx("div", { className: "alert-dialog", style: {
106 | display: "flex",
107 | justifyContent: "center",
108 | marginTop: "16px",
109 | }, children: open === true && _jsx(AlertDialog, {}) })] }));
110 | }
111 |
--------------------------------------------------------------------------------
/client/src/features/cluster-view/clusterViewApiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 | import { createSlice } from "@reduxjs/toolkit";
3 | // Define an API service for the cluster view
4 | export const clusterApi = createApi({
5 | reducerPath: "clusterApi",
6 | baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:8080/" }),
7 | endpoints: builder => ({
8 | getKubernetesNodes: builder.query({
9 | query: () => "api/nodes",
10 | }),
11 | getKubernetesPods: builder.query({
12 | query: () => "api/pods",
13 | }),
14 | getKubernetesServices: builder.query({
15 | query: () => "api/services",
16 | }),
17 | }),
18 | });
19 | // Auto-generated hooks for the API queries
20 | export const { useGetKubernetesNodesQuery, useGetKubernetesPodsQuery, useGetKubernetesServicesQuery, } = clusterApi;
21 | const initialState = {
22 | pods: ["pod_1"],
23 | };
24 | export const clusterViewSlice = createSlice({
25 | name: "clusterView",
26 | initialState,
27 | reducers: {},
28 | });
29 | // Selectors for any additional state managed in this slice
30 | export const selectPods = (state) => state.clusterView.pods;
31 | export default clusterViewSlice.reducer;
32 |
--------------------------------------------------------------------------------
/client/src/features/cluster-view/clusterViewApiSlice.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
2 | import { createSlice } from "@reduxjs/toolkit"
3 |
4 | // Define the structure of the Node object
5 | export interface KubernetesNode {
6 | creationTimestamp: string
7 | name: string
8 | labels: { [key: string]: string }
9 | podCIDR: string
10 | addresses: { type: string; address: string }[]
11 | allocatable: { [key: string]: string }
12 | capacity: {
13 | [key: string]: string
14 | }
15 | conditions: { type: string; status: string }[]
16 | }
17 |
18 | export interface KubernetesPod {
19 | name: string
20 | creationTimestamp: string
21 | namespace: string
22 | labels: { [key: string]: string }
23 | nodeName: string
24 | restartPolicy: string
25 | hostIP: string
26 | podIP: string
27 | phase: string
28 | startTime: string
29 | uid: string
30 | conditions: Conditions[]
31 | }
32 |
33 | export interface KubernetesServices {
34 | name: String
35 | namespace: String
36 | labels: { [key: string]: string } | undefined
37 | creationTimestamp: Date | undefined
38 | clusterIP: String | undefined
39 | ports: string[] | undefined
40 | selector: { [key: string]: string } | undefined
41 | type: String | undefined
42 | }
43 |
44 | interface Conditions {
45 | lastProbeTime: string | null
46 | lastTransitionTime: Date
47 | status: Boolean
48 | type: string
49 | }
50 |
51 | // Define an API service for the cluster view
52 | export const clusterApi = createApi({
53 | reducerPath: "clusterApi",
54 | baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:8080/" }),
55 | endpoints: builder => ({
56 | getKubernetesNodes: builder.query({
57 | query: () => "api/nodes",
58 | }),
59 | getKubernetesPods: builder.query({
60 | query: () => "api/pods",
61 | }),
62 | getKubernetesServices: builder.query({
63 | query: () => "api/services",
64 | }),
65 | }),
66 | })
67 |
68 | // Auto-generated hooks for the API queries
69 | export const {
70 | useGetKubernetesNodesQuery,
71 | useGetKubernetesPodsQuery,
72 | useGetKubernetesServicesQuery,
73 | } = clusterApi
74 |
75 | export interface ClusterViewState {
76 | pods: string[]
77 | }
78 |
79 | const initialState: ClusterViewState = {
80 | pods: ["pod_1"],
81 | }
82 |
83 | export const clusterViewSlice = createSlice({
84 | name: "clusterView",
85 | initialState,
86 | reducers: {},
87 | })
88 |
89 | // Selectors for any additional state managed in this slice
90 | export const selectPods = (state: { clusterView: ClusterViewState }) =>
91 | state.clusterView.pods
92 |
93 | export default clusterViewSlice.reducer
94 |
--------------------------------------------------------------------------------
/client/src/features/cluster-view/components/CustomNode.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2 | import { useState, useEffect } from "react";
3 | import { Handle, Position } from "@xyflow/react";
4 | import Popover from "@mui/material/Popover";
5 | import Typography from "@mui/material/Typography";
6 | import styled from "styled-components";
7 | const Cluster = styled.div `
8 | background: white;
9 | border-radius: 50%;
10 | height: 150px;
11 | width: 150px;
12 | border: 5px solid #ad97d0;
13 | box-shadow: 0 0 40px #ad97d0;
14 | color: black;
15 | text-align: center;
16 | align-content: center;
17 | `;
18 | const Node = styled.div `
19 | background: white;
20 | border-radius: 50%;
21 | height: 150px;
22 | width: 150px;
23 | border: 5px solid #ad97d0;
24 | box-shadow: 0 0 40px #ad97d0;
25 | color: black;
26 | text-align: center;
27 | align-content: center;
28 | `;
29 | const Pod = styled.div `
30 | background: white;
31 | border-radius: 50%;
32 | height: 150px;
33 | width: 150px;
34 | color: black;
35 | text-align: center;
36 | align-content: center;
37 | `;
38 | export const KubeNode = ({ data }) => {
39 | // ** set local state **
40 | const [anchorEl, setAnchorEl] = useState(null);
41 | // Define fuctions to open and close popover element
42 | const handleClick = (event) => {
43 | event.preventDefault();
44 | setAnchorEl(event.currentTarget);
45 | };
46 | const handleClose = () => {
47 | setAnchorEl(null);
48 | };
49 | const open = Boolean(anchorEl);
50 | const id = open ? "simple-popover" : undefined;
51 | return (_jsxs(_Fragment, { children: [_jsxs(Node, { onClick: handleClick, children: [_jsx("div", { children: data.name.length > 10 ? `${data.name.slice(0, 20)}...` : data.name }), _jsx(Handle, { type: "target", position: Position.Top, style: { borderRadius: 100, width: 20 } }), _jsx(Handle, { type: "source", position: Position.Bottom, style: { borderRadius: 100, width: 20 } })] }), _jsx(Popover, { id: id, open: open, anchorEl: anchorEl, onClose: handleClose, anchorOrigin: {
52 | vertical: "top",
53 | horizontal: "right",
54 | }, children: _jsxs(Typography, { sx: { p: 2 }, children: [_jsx("strong", { children: "Name:" }), " ", data.name, _jsx("br", {}), _jsx("strong", { children: "Time Created:" }), data["creationTimestamp"] ? data.creationTimestamp : null, _jsx("br", {}), _jsx("strong", { children: "Capacity:" }), _jsx("br", {}), _jsx("strong", { children: _jsx("span", { style: { marginLeft: "20px" }, children: " CPU: " }) }), data.capacity["cpu"].toString(), _jsx("br", {}), _jsx("strong", { children: _jsx("span", { style: { marginLeft: "20px" }, children: " ephemeral-storage: " }) }), data.capacity["ephemeral-storage"], _jsx("br", {}), _jsx("strong", { children: _jsx("span", { style: { marginLeft: "20px" }, children: " hugepages-1Gi: " }) }), data.capacity["hugepages-1Gi"], _jsx("br", {}), _jsx("strong", { children: _jsx("span", { style: { marginLeft: "20px" }, children: " hugepages-2Mi: " }) }), data.capacity["hugepages-2Mi"], _jsx("br", {}), _jsx("strong", { children: _jsx("span", { style: { marginLeft: "20px" }, children: " memory: " }) }), data.capacity["memory"], _jsx("br", {}), _jsx("strong", { children: _jsx("span", { style: { marginLeft: "20px" }, children: " pods: " }) }), data.capacity["pods"]] }) })] }));
55 | };
56 | export const KubePod = ({ data }) => {
57 | // ** set local state **
58 | const [anchorEl, setAnchorEl] = useState(null);
59 | const [status, setStatus] = useState(data.conditions[2].status);
60 | // Monitors changes in pod status and updates state accordingly
61 | useEffect(() => {
62 | setStatus(data.conditions[2].status);
63 | }, [data.conditions]);
64 | // Define fuctions to open and close popover element
65 | const handleClick = (event) => {
66 | event.preventDefault();
67 | setAnchorEl(event.currentTarget);
68 | };
69 | const handleClose = () => {
70 | setAnchorEl(null);
71 | };
72 | const open = Boolean(anchorEl);
73 | const id = open ? "simple-popover" : undefined;
74 | const styles = {
75 | border: `5px solid ${status ? "rgb(46, 226, 88)" : "red"}`,
76 | boxShadow: `0 0 40px ${status ? "rgb(46, 226, 88)" : "red"}`,
77 | };
78 | return (_jsxs(_Fragment, { children: [_jsxs(Pod, { onClick: handleClick, style: styles, children: [_jsx("div", { children: data.name.length > 10 ? `${data.name.slice(0, 20)}...` : data.name }), _jsx(Handle, { type: "target", position: Position.Top, style: { borderRadius: 100, width: 20 } })] }), _jsx(Popover, { id: id, open: open, anchorEl: anchorEl, onClose: handleClose, anchorOrigin: {
79 | vertical: "top",
80 | horizontal: "right",
81 | }, children: _jsxs(Typography, { sx: { p: 2 }, children: [_jsx("strong", { children: "Name:" }), " ", data.name, _jsx("br", {}), _jsx("strong", { children: "Time Created: " }), data.creationTimestamp ? data.creationTimestamp : null, _jsx("br", {}), _jsx("strong", { children: "phase:" }), " ", data.phase, _jsx("br", {}), _jsx("strong", { children: "restartPolicy:" }), " ", data.restartPolicy, _jsx("br", {}), _jsx("strong", { children: "uid:" }), " ", data.uid] }) })] }));
82 | };
83 | export const KubeCluster = ({ data }) => {
84 | return (_jsx(_Fragment, { children: _jsxs(Cluster, { children: [_jsx("div", { children: data.name.length > 10 ? `${data.name.slice(0, 20)}...` : data.name }), _jsx(Handle, { type: "source", position: Position.Bottom, style: { borderRadius: 100, width: 20 } })] }) }));
85 | };
86 |
--------------------------------------------------------------------------------
/client/src/features/cluster-view/components/CustomNode.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react"
2 | import { useState, useEffect } from "react"
3 | import { Handle, Position } from "@xyflow/react"
4 | import Popover from "@mui/material/Popover"
5 | import Typography from "@mui/material/Typography"
6 | import styled from "styled-components"
7 | import type { KubernetesNode, KubernetesPod } from "../clusterViewApiSlice"
8 |
9 | // ****************************
10 | // ** Create Interface's **
11 | // ****************************
12 |
13 | interface ReactFlowNodeData {
14 | data: KubernetesNode
15 | }
16 |
17 | interface ReactFlowPodData {
18 | data: KubernetesPod
19 | }
20 |
21 | interface ReactFlowClusterData {
22 | data: { name: string }
23 | }
24 |
25 | const Cluster = styled.div`
26 | background: white;
27 | border-radius: 50%;
28 | height: 150px;
29 | width: 150px;
30 | border: 5px solid #ad97d0;
31 | box-shadow: 0 0 40px #ad97d0;
32 | color: black;
33 | text-align: center;
34 | align-content: center;
35 | `
36 |
37 | const Node = styled.div`
38 | background: white;
39 | border-radius: 50%;
40 | height: 150px;
41 | width: 150px;
42 | border: 5px solid #ad97d0;
43 | box-shadow: 0 0 40px #ad97d0;
44 | color: black;
45 | text-align: center;
46 | align-content: center;
47 | `
48 |
49 | const Pod = styled.div`
50 | background: white;
51 | border-radius: 50%;
52 | height: 150px;
53 | width: 150px;
54 | color: black;
55 | text-align: center;
56 | align-content: center;
57 | `
58 |
59 | export const KubeNode = ({ data }: ReactFlowNodeData) => {
60 | // ** set local state **
61 | const [anchorEl, setAnchorEl] = useState(null)
62 |
63 | // Define fuctions to open and close popover element
64 | const handleClick = (event: React.MouseEvent) => {
65 | event.preventDefault()
66 | setAnchorEl(event.currentTarget)
67 | }
68 |
69 | const handleClose = () => {
70 | setAnchorEl(null)
71 | }
72 |
73 | const open = Boolean(anchorEl)
74 | const id = open ? "simple-popover" : undefined
75 |
76 | return (
77 | <>
78 |
79 |
80 | {data.name.length > 10 ? `${data.name.slice(0, 20)}...` : data.name}
81 |
82 |
83 |
88 |
89 |
94 |
95 |
96 |
106 |
107 | Name: {data.name}
108 |
109 | Time Created:
110 | {data["creationTimestamp"] ? data.creationTimestamp : null}
111 |
112 | Capacity:
113 |
114 |
115 | CPU:
116 |
117 | {data.capacity["cpu"].toString()}
118 |
119 |
120 | ephemeral-storage:
121 |
122 | {data.capacity["ephemeral-storage"]}
123 |
124 |
125 | hugepages-1Gi:
126 |
127 | {data.capacity["hugepages-1Gi"]}
128 |
129 |
130 | hugepages-2Mi:
131 |
132 | {data.capacity["hugepages-2Mi"]}
133 |
134 |
135 | memory:
136 |
137 | {data.capacity["memory"]}
138 |
139 |
140 | pods:
141 |
142 | {data.capacity["pods"]}
143 |
144 |
145 | >
146 | )
147 | }
148 |
149 | export const KubePod = ({ data }: ReactFlowPodData) => {
150 | // ** set local state **
151 | const [anchorEl, setAnchorEl] = useState(null)
152 | const [status, setStatus] = useState(data.conditions[2].status)
153 |
154 | // Monitors changes in pod status and updates state accordingly
155 | useEffect(() => {
156 | setStatus(data.conditions[2].status)
157 | }, [data.conditions])
158 |
159 | // Define fuctions to open and close popover element
160 | const handleClick = (event: React.MouseEvent) => {
161 | event.preventDefault()
162 | setAnchorEl(event.currentTarget)
163 | }
164 |
165 | const handleClose = () => {
166 | setAnchorEl(null)
167 | }
168 |
169 | const open = Boolean(anchorEl)
170 | const id = open ? "simple-popover" : undefined
171 |
172 | const styles = {
173 | border: `5px solid ${status ? "rgb(46, 226, 88)" : "red"}`,
174 | boxShadow: `0 0 40px ${status ? "rgb(46, 226, 88)" : "red"}`,
175 | }
176 |
177 | return (
178 | <>
179 |
180 |
181 | {data.name.length > 10 ? `${data.name.slice(0, 20)}...` : data.name}
182 |
183 |
184 |
189 |
190 |
200 |
201 | Name: {data.name}
202 |
203 | Time Created:
204 | {data.creationTimestamp ? data.creationTimestamp : null}
205 |
206 | phase: {data.phase}
207 |
208 | restartPolicy: {data.restartPolicy}
209 |
210 | uid: {data.uid}
211 |
212 |
213 | >
214 | )
215 | }
216 |
217 | export const KubeCluster = ({ data }: ReactFlowClusterData) => {
218 | return (
219 | <>
220 |
221 |
222 | {data.name.length > 10 ? `${data.name.slice(0, 20)}...` : data.name}
223 |
224 |
229 |
230 | >
231 | )
232 | }
233 |
--------------------------------------------------------------------------------
/client/src/features/cluster-view/containers/ClusterViewContainer.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import { useState, useEffect, useMemo } from "react";
3 | import "@xyflow/react/dist/style.css";
4 | import { ReactFlow, Background, Controls } from "@xyflow/react";
5 | import { MiniMap } from "@xyflow/react";
6 | import { useGetKubernetesNodesQuery, useGetKubernetesPodsQuery, } from "../clusterViewApiSlice";
7 | import { KubeNode, KubePod, KubeCluster } from "../components/CustomNode";
8 | // *******************
9 | // ** Component **
10 | // *******************
11 | export default function ClusterViewContainer() {
12 | // **** Global State ****
13 | // Hooks into Kubernets Cluster Data via RTK Query
14 | const { data: kubernetesNodes = [] } = useGetKubernetesNodesQuery();
15 | const { data: kubernetesPods = [] } = useGetKubernetesPodsQuery();
16 | // Create object to pass into type property of React Flow Nodes.
17 | // This enables the usage of a React Component to be the structure of a ReactFlow Node.
18 | const nodeTypes = useMemo(() => ({ kubeNode: KubeNode, kubePod: KubePod, kubeCluster: KubeCluster }), []);
19 | // **** Manage Side Effect ****
20 | // Ensure mappedNodes is always up to date when new data is recieved from Kubernetes Cluster
21 | useEffect(() => {
22 | if (kubernetesNodes.length > 0 && kubernetesPods.length > 0) {
23 | // Creates pod arrays for each node
24 | const initializedNodes = initializeNodes(kubernetesNodes);
25 | // Calls node mapping function and stores in a temp variable
26 | const tempMappedNodes = mapPodsToNodes(initializedNodes, kubernetesPods);
27 | // Sets the mappedNodes state to the newly mapped nodes
28 | setMappedNodes(tempMappedNodes);
29 | }
30 | }, [kubernetesNodes, kubernetesPods]);
31 | // **** Local State ****
32 | // Sets state for nodes array ****
33 | const [mappedNodes, setMappedNodes] = useState([]);
34 | // **** Helper Functions ****
35 | // mapPodsToNodes, maps the pods array to the corresponding node
36 | function mapPodsToNodes(nodes, pods) {
37 | const nodeMap = {};
38 | nodes.forEach(node => {
39 | nodeMap[node.name] = node;
40 | });
41 | pods.forEach(pod => {
42 | if (nodeMap[pod.nodeName]) {
43 | nodeMap[pod.nodeName].pods?.push(pod);
44 | }
45 | });
46 | return nodes;
47 | }
48 | // Adds a 'pods' array to each node in the nodes array
49 | // This array stores the corresponding pods for that node
50 | function initializeNodes(nodes) {
51 | return nodes.map(node => ({ name: node.name, data: node, pods: [] }));
52 | }
53 | // **********************************
54 | // ** Renders React Flow Nodes **
55 | // **********************************
56 | const reactFlowNodes = () => {
57 | // Adds Kubernetes Cluster as the first node by default
58 | const reactFlowNodeArray = [
59 | {
60 | id: "Cluster",
61 | position: { x: (mappedNodes.length / 2 + 1) * 750, y: 0 },
62 | data: { name: "Cluster" },
63 | type: "kubeCluster",
64 | draggable: true,
65 | },
66 | ];
67 | // Iterates through the mapped nodes state array
68 | mappedNodes.forEach((node, index) => {
69 | // Adds the node to the output array first
70 | const startingXPos = (index + 1) * 1000;
71 | reactFlowNodeArray.push({
72 | id: node.name,
73 | position: { x: startingXPos, y: 300 },
74 | data: { ...node.data },
75 | type: "kubeNode",
76 | draggable: true,
77 | });
78 | const podsPerRow = 3;
79 | let podValueY = 600;
80 | let podCurrentIndex = 0;
81 | // Conditional wrapper to start iteration of pods array for current node
82 | while (podCurrentIndex < node.pods.length) {
83 | const startX = startingXPos - 200;
84 | // Iterates through the pods array for this node
85 | for (let i = 0; i < podsPerRow && podCurrentIndex < node.pods.length; i++) {
86 | // Adds pod to reactFlowNodeArray
87 | reactFlowNodeArray.push({
88 | id: node.pods[podCurrentIndex].uid.toString(),
89 | position: { x: startX + i * 200, y: podValueY },
90 | data: { ...node.pods[podCurrentIndex] },
91 | type: "kubePod",
92 | draggable: true,
93 | });
94 | podCurrentIndex++;
95 | }
96 | // Increments variables for new row
97 | podValueY += 200;
98 | }
99 | });
100 | return reactFlowNodeArray;
101 | };
102 | // **********************************
103 | // ** Renders React Flow Edges **
104 | // **********************************
105 | const reactFlowEdges = () => {
106 | const reactFlowEdgeArray = [];
107 | // Adds React Flow Edges connections between nodes and the cluster
108 | mappedNodes.forEach(node => {
109 | reactFlowEdgeArray.push({
110 | id: `Cluster-${node.name}`,
111 | source: "Cluster",
112 | target: `${node.name}`,
113 | animated: true,
114 | });
115 | });
116 | // Adds React Flow Edges connections between pods and nodes
117 | kubernetesPods.forEach(pod => {
118 | reactFlowEdgeArray.push({
119 | id: `${pod.nodeName}-${pod.name}`,
120 | source: `${pod.nodeName}`,
121 | target: `${pod.uid}`,
122 | animated: true,
123 | });
124 | });
125 | return reactFlowEdgeArray;
126 | };
127 | const nodes = reactFlowNodes();
128 | const edges = reactFlowEdges();
129 | // **** ClusterViewContainer Function Return ****
130 | return (_jsx("div", { id: "clusterview-container", className: "container", children: _jsx("div", { style: { width: "100vw", height: "100vh" }, children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, children: [_jsx(Background, {}), _jsx(Controls, {}), _jsx(MiniMap, { style: { backgroundColor: "gray" } })] }) }) }));
131 | }
132 |
--------------------------------------------------------------------------------
/client/src/features/cluster-view/containers/ClusterViewContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useMemo } from "react"
2 | import "@xyflow/react/dist/style.css"
3 | import { ReactFlow, Background, Controls } from "@xyflow/react"
4 | import type { Node, Edge } from "@xyflow/react"
5 | import { MiniMap } from "@xyflow/react"
6 | import {
7 | useGetKubernetesNodesQuery,
8 | useGetKubernetesPodsQuery,
9 | } from "../clusterViewApiSlice"
10 | import { KubeNode, KubePod, KubeCluster } from "../components/CustomNode"
11 | import type { KubernetesNode, KubernetesPod } from "../clusterViewApiSlice"
12 |
13 | // ****************************
14 | // ** Create Interface's **
15 | // ****************************
16 |
17 | interface MiddlewareNodes {
18 | name: string
19 | data: KubernetesNode
20 | pods: KubernetesPod[]
21 | }
22 |
23 | // *******************
24 | // ** Component **
25 | // *******************
26 |
27 | export default function ClusterViewContainer() {
28 | // **** Global State ****
29 | // Hooks into Kubernets Cluster Data via RTK Query
30 | const { data: kubernetesNodes = [] } = useGetKubernetesNodesQuery()
31 |
32 | const { data: kubernetesPods = [] } = useGetKubernetesPodsQuery()
33 |
34 | // Create object to pass into type property of React Flow Nodes.
35 | // This enables the usage of a React Component to be the structure of a ReactFlow Node.
36 | const nodeTypes = useMemo(
37 | () => ({ kubeNode: KubeNode, kubePod: KubePod, kubeCluster: KubeCluster }),
38 | [],
39 | )
40 |
41 | // **** Manage Side Effect ****
42 | // Ensure mappedNodes is always up to date when new data is recieved from Kubernetes Cluster
43 | useEffect(() => {
44 | if (kubernetesNodes.length > 0 && kubernetesPods.length > 0) {
45 | // Creates pod arrays for each node
46 | const initializedNodes = initializeNodes(kubernetesNodes)
47 | // Calls node mapping function and stores in a temp variable
48 | const tempMappedNodes = mapPodsToNodes(initializedNodes, kubernetesPods)
49 | // Sets the mappedNodes state to the newly mapped nodes
50 | setMappedNodes(tempMappedNodes)
51 | }
52 | }, [kubernetesNodes, kubernetesPods])
53 |
54 | // **** Local State ****
55 | // Sets state for nodes array ****
56 | const [mappedNodes, setMappedNodes] = useState([])
57 |
58 | // **** Helper Functions ****
59 | // mapPodsToNodes, maps the pods array to the corresponding node
60 | function mapPodsToNodes(
61 | nodes: MiddlewareNodes[],
62 | pods: KubernetesPod[],
63 | ): MiddlewareNodes[] {
64 | const nodeMap: { [key: string]: MiddlewareNodes } = {}
65 |
66 | nodes.forEach(node => {
67 | nodeMap[node.name] = node
68 | })
69 |
70 | pods.forEach(pod => {
71 | if (nodeMap[pod.nodeName]) {
72 | nodeMap[pod.nodeName].pods?.push(pod)
73 | }
74 | })
75 |
76 | return nodes
77 | }
78 |
79 | // Adds a 'pods' array to each node in the nodes array
80 | // This array stores the corresponding pods for that node
81 | function initializeNodes(nodes: KubernetesNode[]): MiddlewareNodes[] {
82 | return nodes.map(node => ({ name: node.name, data: node, pods: [] }))
83 | }
84 |
85 | // **********************************
86 | // ** Renders React Flow Nodes **
87 | // **********************************
88 | const reactFlowNodes = (): Node[] => {
89 | // Adds Kubernetes Cluster as the first node by default
90 | const reactFlowNodeArray: Node[] = [
91 | {
92 | id: "Cluster",
93 | position: { x: (mappedNodes.length / 2 + 1) * 750, y: 0 },
94 | data: { name: "Cluster" },
95 | type: "kubeCluster",
96 | draggable: true,
97 | },
98 | ]
99 |
100 | // Iterates through the mapped nodes state array
101 | mappedNodes.forEach((node, index) => {
102 | // Adds the node to the output array first
103 | const startingXPos = (index + 1) * 1000
104 | reactFlowNodeArray.push({
105 | id: node.name,
106 | position: { x: startingXPos, y: 300 },
107 | data: { ...node.data },
108 | type: "kubeNode",
109 | draggable: true,
110 | })
111 |
112 | const podsPerRow = 3
113 | let podValueY = 600
114 | let podCurrentIndex = 0
115 |
116 | // Conditional wrapper to start iteration of pods array for current node
117 | while (podCurrentIndex < node.pods.length) {
118 | const startX = startingXPos - 200
119 |
120 | // Iterates through the pods array for this node
121 | for (
122 | let i = 0;
123 | i < podsPerRow && podCurrentIndex < node.pods.length;
124 | i++
125 | ) {
126 | // Adds pod to reactFlowNodeArray
127 | reactFlowNodeArray.push({
128 | id: node.pods[podCurrentIndex].uid.toString(),
129 | position: { x: startX + i * 200, y: podValueY },
130 | data: { ...node.pods[podCurrentIndex] },
131 | type: "kubePod",
132 | draggable: true,
133 | })
134 | podCurrentIndex++
135 | }
136 |
137 | // Increments variables for new row
138 | podValueY += 200
139 | }
140 | })
141 | return reactFlowNodeArray
142 | }
143 |
144 | // **********************************
145 | // ** Renders React Flow Edges **
146 | // **********************************
147 |
148 | const reactFlowEdges = (): Edge[] => {
149 | const reactFlowEdgeArray: Edge[] = []
150 |
151 | // Adds React Flow Edges connections between nodes and the cluster
152 | mappedNodes.forEach(node => {
153 | reactFlowEdgeArray.push({
154 | id: `Cluster-${node.name}`,
155 | source: "Cluster",
156 | target: `${node.name}`,
157 | animated: true,
158 | })
159 | })
160 |
161 | // Adds React Flow Edges connections between pods and nodes
162 | kubernetesPods.forEach(pod => {
163 | reactFlowEdgeArray.push({
164 | id: `${pod.nodeName}-${pod.name}`,
165 | source: `${pod.nodeName}`,
166 | target: `${pod.uid}`,
167 | animated: true,
168 | })
169 | })
170 | return reactFlowEdgeArray
171 | }
172 |
173 | const nodes = reactFlowNodes()
174 | const edges = reactFlowEdges()
175 |
176 | // **** ClusterViewContainer Function Return ****
177 | return (
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | )
188 | }
189 |
--------------------------------------------------------------------------------
/client/src/features/grafana-dashboard/Grafana.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | body {
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | min-height: 100vh;
12 | }
13 |
14 | .wrapper {
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | align-items: center;
19 | border: 1px solid rgb(229, 209, 255);
20 | padding: 20px;
21 | gap: 20px;
22 | width: 100%;
23 | max-width: 600px;
24 | margin: 0 auto;
25 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.241);
26 | }
27 |
28 | .wrapper form {
29 | display: flex;
30 | flex-direction: column;
31 | width: 100%;
32 | }
33 |
34 | .wrapper h2 {
35 | margin-top: 15px;
36 | font-size: 1.8rem;
37 | margin-bottom: 20px;
38 | text-align: center;
39 | }
40 |
41 | .wrapper input[type="text"] {
42 | width: 100%;
43 | padding: 12px 16px;
44 | border-radius: 12px;
45 | /* border: 1px solid #ccc; */
46 | font-size: 1rem;
47 | outline: none;
48 | transition: border-color 0.3s ease;
49 | }
50 |
51 | .wrapper input[type="text"]:focus {
52 | border-color: #6200ea;
53 | box-shadow: 0 0 8px rgba(98, 0, 234, 0.2);
54 | }
55 |
56 | .wrapper button {
57 | padding: 12px 24px;
58 | background-color: #7f00ff;
59 | color: white;
60 | border: none;
61 | border-radius: 12px;
62 | font-size: 1rem;
63 | cursor: pointer;
64 | transition:
65 | background-color 0.3s ease,
66 | transform 0.3s ease;
67 | width: 100%;
68 | margin-top: 20px;
69 | }
70 |
71 | .wrapper button:hover {
72 | background-color: #4a00b4;
73 | }
74 |
75 | .wrapper button:active {
76 | background-color: #3a008c;
77 | transform: translateY(2px);
78 | }
79 |
80 | @media (max-width: 768px) {
81 | .wrapper {
82 | width: 90%;
83 | padding: 15px;
84 | }
85 |
86 | .wrapper h2 {
87 | font-size: 1.5rem;
88 | }
89 |
90 | .wrapper button {
91 | font-size: 0.9rem;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/client/src/features/grafana-dashboard/GrafanaDashboardApiSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | const initialState = {
3 | src: ''
4 | };
5 | const iframeSlice = createSlice({
6 | name: 'iframe',
7 | initialState,
8 | reducers: {
9 | setIframeSrc: (state, action) => {
10 | state.src = action.payload;
11 | },
12 | },
13 | });
14 | export const { setIframeSrc } = iframeSlice.actions;
15 | export default iframeSlice.reducer;
16 |
--------------------------------------------------------------------------------
/client/src/features/grafana-dashboard/GrafanaDashboardApiSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import type { PayloadAction } from '@reduxjs/toolkit'
3 |
4 | // Define the initial state for the iframe URL
5 | interface IframeState {
6 | src: string;
7 | }
8 |
9 | const initialState: IframeState = {
10 | src: ''
11 | };
12 |
13 | const iframeSlice = createSlice({
14 | name: 'iframe',
15 | initialState,
16 | reducers: {
17 | setIframeSrc: (state, action: PayloadAction) => {
18 | state.src = action.payload;
19 | },
20 | },
21 | });
22 |
23 | export const { setIframeSrc } = iframeSlice.actions;
24 | export default iframeSlice.reducer;
--------------------------------------------------------------------------------
/client/src/features/grafana-dashboard/GrafanaViewContainer.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2 | import { useState } from "react";
3 | import { setIframeSrc } from "./GrafanaDashboardApiSlice";
4 | import { useAppDispatch, useAppSelector } from "../../app/hooks";
5 | import "./Grafana.css";
6 | import { TextField, Button } from "@mui/material";
7 | export default function GrafanaViewContainer() {
8 | const iframeURL = useAppSelector((state) => state.iframe.src);
9 | const Form = () => {
10 | const [inputValue, setInputValue] = useState("");
11 | const dispatch = useAppDispatch();
12 | const handleInputChange = (e) => {
13 | setInputValue(e.target.value);
14 | };
15 | const handleSubmit = (e) => {
16 | e.preventDefault();
17 | dispatch(setIframeSrc(inputValue));
18 | };
19 | return (_jsxs("div", { className: "wrapper", style: { position: "relative", left: "-45px", top: "-75px" }, children: [_jsx("h2", { children: "Connect Your Grafana Dashboard" }), _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextField, { label: "Grafana URL", color: "primary", variant: "outlined", placeholder: "http://your-grafana-instance/d/your-dashboard-id", focused: true, value: inputValue, onChange: handleInputChange }), _jsx(Button, { type: "button", children: "Connect" })] })] }));
20 | };
21 | const Dashboard = () => {
22 | return (_jsx("iframe", { title: "Grafana Dashboard", src: iframeURL, style: {
23 | width: "100vw",
24 | height: "100vh",
25 | border: "none",
26 | position: "relative",
27 | marginTop: "30px",
28 | } }));
29 | };
30 | return _jsx(_Fragment, { children: iframeURL !== "" ? _jsx(Dashboard, {}) : _jsx(Form, {}) });
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/features/grafana-dashboard/GrafanaViewContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { setIframeSrc } from "./GrafanaDashboardApiSlice"
3 | import { useAppDispatch, useAppSelector } from "../../app/hooks"
4 | import type React from "react"
5 | import "./Grafana.css"
6 | import type { RootState } from "../../app/store"
7 | import { TextField, Button } from "@mui/material"
8 |
9 | export default function GrafanaViewContainer() {
10 | const iframeURL = useAppSelector((state: RootState) => state.iframe.src)
11 |
12 | const Form = () => {
13 | const [inputValue, setInputValue] = useState("")
14 | const dispatch = useAppDispatch()
15 |
16 | const handleInputChange = (e: React.ChangeEvent) => {
17 | setInputValue(e.target.value)
18 | }
19 |
20 | const handleSubmit = (e: React.FormEvent) => {
21 | e.preventDefault()
22 | dispatch(setIframeSrc(inputValue))
23 | }
24 |
25 | return (
26 |
30 |
Connect Your Grafana Dashboard
31 |
43 |
44 | )
45 | }
46 |
47 | const Dashboard = () => {
48 | return (
49 |
60 | )
61 | }
62 |
63 | return <>{iframeURL !== "" ? : }>
64 | }
65 |
--------------------------------------------------------------------------------
/client/src/features/info-drawer/InfoPopover.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import * as React from "react";
3 | import { Popover, Typography, Button } from "@mui/material";
4 | export default function BasicPopover() {
5 | const [anchorEl, setAnchorEl] = React.useState(null);
6 | const handleClick = (event) => {
7 | setAnchorEl(event.currentTarget);
8 | };
9 | const handleClose = () => {
10 | setAnchorEl(null);
11 | };
12 | const open = Boolean(anchorEl);
13 | const id = open ? "simple-popover" : undefined;
14 | return (_jsxs("div", { children: [_jsx(Button, { "aria-describedby": id, variant: "contained", onClick: handleClick, children: "Open Popover" }), _jsx(Popover, { id: id, open: open, anchorEl: anchorEl, onClose: handleClose, anchorOrigin: {
15 | vertical: "bottom",
16 | horizontal: "left",
17 | }, children: _jsx(Typography, { sx: { p: 2 }, children: "The content of the Popover." }) })] }));
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/features/info-drawer/InfoPopover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {Popover, Typography, Button } from "@mui/material"
3 |
4 | export default function BasicPopover() {
5 | const [anchorEl, setAnchorEl] = React.useState(null)
6 |
7 | const handleClick = (event: React.MouseEvent) => {
8 | setAnchorEl(event.currentTarget)
9 | }
10 |
11 | const handleClose = () => {
12 | setAnchorEl(null)
13 | }
14 |
15 | const open = Boolean(anchorEl)
16 | const id = open ? "simple-popover" : undefined
17 |
18 | return (
19 |
20 |
23 |
33 | The content of the Popover.
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/LandingPage.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import { AppBar, Toolbar, Typography, Button, Container, Grid, Box, } from "@mui/material";
3 | import logoSVG from "../../public/logo.svg";
4 | import logoPNG from "../../public/logo.png";
5 | import "./landingpage.css";
6 | import MeetTheTeam from "./components/MeetTheTeam";
7 | import ReadMe from "./components/ReadMe";
8 | export default function LandingPage() {
9 | return (_jsxs(Box, { sx: { minWidth: "750px" }, children: [_jsx(AppBar, { position: "sticky", sx: {
10 | background: "linear-gradient(to bottom, white 70%, rgba(255, 255, 255, 0))",
11 | transition: "background-color 0.5s ease",
12 | }, color: "transparent", elevation: 0, style: { width: "100vw" }, children: _jsxs(Toolbar, { sx: { justifyContent: "space-between" }, children: [_jsx("img", { src: logoSVG, alt: "App logo", style: { width: "50px", marginRight: "15px" } }), _jsx(Typography, { variant: "h6", component: "div", sx: { flexGrow: 1 }, children: "K8STATE" }), _jsx(Button, { color: "inherit", onClick: () => document
13 | .getElementById("team-section")
14 | ?.scrollIntoView({ behavior: "smooth" }), children: "Meet The Team" }), _jsx(Button, { color: "inherit", onClick: () => document
15 | .getElementById("readme-section")
16 | ?.scrollIntoView({ behavior: "smooth" }), children: "Setting Up" })] }) }), _jsx(Container, { className: "landingpage", maxWidth: "lg", sx: { pt: 6 }, children: _jsxs(Grid, { container: true, spacing: 4, alignItems: "center", justifyContent: "center", direction: "column", textAlign: "center", children: [_jsx(Grid, { item: true, xs: 12, children: _jsx("img", { className: "logo-main App-logo-float", src: logoPNG, alt: "App logo", style: {
17 | width: "100%",
18 | maxWidth: "300px",
19 | marginBottom: "20px",
20 | paddingTop: "15vh",
21 | } }) }), _jsxs(Grid, { item: true, xs: 12, children: [_jsxs(Typography, { variant: "h1", component: "h1", gutterBottom: true, children: ["K", _jsx("span", { style: { color: "#ad97d0" }, children: "8" }), "STATE"] }), _jsx(Typography, { variant: "h4", component: "p", gutterBottom: true, children: "Revolutionize Kubernetes Management" }), _jsx(Typography, { variant: "subtitle1", component: "p", gutterBottom: true, children: "Experience cluster visualization that moves as fast as you do." }), _jsx(Button, { "aria-label": "Get Started", variant: "contained", color: "primary", size: "large", sx: { marginRight: "10px" }, href: "clusterui", children: "Get Started" }), _jsx(Button, { "aria-label": "Visit GitHub", variant: "outlined", color: "primary", size: "large", href: "https://github.com/oslabs-beta/k8state", children: "Visit GitHub" })] })] }) }), _jsx("section", { id: "readme-section", children: _jsx(ReadMe, {}) }), _jsx("section", { id: "team-section", children: _jsx(MeetTheTeam, {}) }), _jsx("footer", { className: "footer", children: _jsxs(Container, { maxWidth: "lg", children: [_jsx(Typography, { variant: "body2", color: "#ad97d0", align: "center", children: "Developed For Engineers by Engineers" }), _jsx(Typography, { variant: "body2", color: "textSecondary", align: "center", children: "Released under the MIT License." }), _jsx(Typography, { variant: "body2", color: "textSecondary", align: "center", children: "Copyright \u00A9 2024 K8STATE Contributors" })] }) })] }));
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/LandingPage.test.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { afterEach, test } from "vitest";
3 | import { screen, cleanup } from "@testing-library/react";
4 | import LandingPage from "./LandingPage";
5 | import { renderWithProviders } from "../../utils/test-utils";
6 | afterEach(() => {
7 | cleanup();
8 | });
9 | test("Landing Page should render", async () => {
10 | renderWithProviders(_jsx(LandingPage, {}));
11 | // Check if Body rendered correctly with proper text
12 | expect(screen.getByText(/Revolutionize Kubernetes Management/i)).toBeInTheDocument();
13 | expect(screen.getByText(/Experience cluster visualization that moves as fast as you do./i)).toBeInTheDocument();
14 | // Check if Footer rendered correctly with proper text
15 | expect(screen.getByText(/Developed For Engineers by Engineers/i)).toBeInTheDocument();
16 | expect(screen.getByText(/Released under the MIT License./i)).toBeInTheDocument();
17 | expect(screen.getByText(/Copyright © 2024 K8STATE Contributors/i)).toBeInTheDocument();
18 | });
19 | test("Landing Page should have correct link to Get Started click", async () => {
20 | // Render the LandingPage component
21 | renderWithProviders(_jsx(LandingPage, {}));
22 | // Find the "Get Started" link by its aria-label
23 | const getStartedLink = screen.getByLabelText("Get Started");
24 | // Check if the href attribute is correct
25 | expect(getStartedLink).toHaveAttribute("href", "clusterui");
26 | // Check if the link is still present
27 | expect(getStartedLink).toBeInTheDocument();
28 | });
29 | test("Landing Page should correct link to GitHub", async () => {
30 | // Render the LandingPage component
31 | renderWithProviders(_jsx(LandingPage, {}));
32 | // Find the "Get Started" link by its aria-label
33 | const getStartedLink = screen.getByLabelText("Visit GitHub");
34 | // Check if the href attribute is correct
35 | expect(getStartedLink).toHaveAttribute("href", "https://github.com/oslabs-beta/k8state");
36 | expect(getStartedLink).toBeInTheDocument();
37 | });
38 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/LandingPage.test.tsx:
--------------------------------------------------------------------------------
1 | import { afterEach, test } from "vitest"
2 | import { screen, cleanup } from "@testing-library/react"
3 | import LandingPage from "./LandingPage"
4 | import { renderWithProviders } from "../../utils/test-utils"
5 |
6 | afterEach(() => {
7 | cleanup()
8 | })
9 |
10 | test("Landing Page should render", async () => {
11 | renderWithProviders()
12 |
13 | // Check if Body rendered correctly with proper text
14 | expect(
15 | screen.getByText(/Revolutionize Kubernetes Management/i),
16 | ).toBeInTheDocument()
17 |
18 | expect(
19 | screen.getByText(
20 | /Experience cluster visualization that moves as fast as you do./i,
21 | ),
22 | ).toBeInTheDocument()
23 |
24 | // Check if Footer rendered correctly with proper text
25 | expect(
26 | screen.getByText(/Developed For Engineers by Engineers/i),
27 | ).toBeInTheDocument()
28 | expect(
29 | screen.getByText(/Released under the MIT License./i),
30 | ).toBeInTheDocument()
31 | expect(
32 | screen.getByText(/Copyright © 2024 K8STATE Contributors/i),
33 | ).toBeInTheDocument()
34 | })
35 |
36 | test("Landing Page should have correct link to Get Started click", async () => {
37 | // Render the LandingPage component
38 | renderWithProviders()
39 |
40 | // Find the "Get Started" link by its aria-label
41 | const getStartedLink = screen.getByLabelText("Get Started")
42 |
43 | // Check if the href attribute is correct
44 | expect(getStartedLink).toHaveAttribute("href", "clusterui")
45 |
46 | // Check if the link is still present
47 | expect(getStartedLink).toBeInTheDocument()
48 | })
49 |
50 | test("Landing Page should correct link to GitHub", async () => {
51 | // Render the LandingPage component
52 | renderWithProviders()
53 |
54 | // Find the "Get Started" link by its aria-label
55 | const getStartedLink = screen.getByLabelText("Visit GitHub")
56 |
57 | // Check if the href attribute is correct
58 | expect(getStartedLink).toHaveAttribute(
59 | "href",
60 | "https://github.com/oslabs-beta/k8state",
61 | )
62 |
63 | expect(getStartedLink).toBeInTheDocument()
64 | })
65 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AppBar,
3 | Toolbar,
4 | Typography,
5 | Button,
6 | Container,
7 | Grid,
8 | Box,
9 | } from "@mui/material"
10 | import logoSVG from "../../public/logo.svg"
11 | import logoPNG from "../../public/logo.png"
12 | import "./landingpage.css"
13 | import MeetTheTeam from "./components/MeetTheTeam"
14 | import ReadMe from "./components/ReadMe"
15 |
16 | export default function LandingPage() {
17 | return (
18 |
19 |
30 |
31 |
36 |
37 | K8STATE
38 |
39 |
49 |
59 |
60 |
61 | {/* Main Content */}
62 |
63 |
71 |
72 |
83 |
84 |
85 |
86 | K8STATE
87 |
88 |
89 | Revolutionize Kubernetes Management
90 |
91 |
92 | Experience cluster visualization that moves as fast as you do.
93 |
94 |
104 |
113 |
114 |
115 |
116 |
117 |
120 |
121 |
124 |
125 | {/* Footer */}
126 |
139 |
140 | )
141 | }
142 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/components/MeetTheTeam.css:
--------------------------------------------------------------------------------
1 | .team-section {
2 | padding: 50px 0;
3 | margin-left: 20px;
4 | margin-right: 20px;
5 | text-align: center;
6 | }
7 |
8 | .team-title {
9 | font-size: 2rem;
10 | font-weight: 700;
11 | margin-bottom: 50px;
12 | }
13 |
14 | .team-grid {
15 | display: grid;
16 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
17 | gap: 30px;
18 | justify-items: center;
19 | align-items: start;
20 | }
21 |
22 | .team-card {
23 | background-color: #ffffff;
24 | border-radius: 10px;
25 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
26 | padding: 20px;
27 | max-width: 300px;
28 | text-align: center;
29 | transition: transform 0.3s ease, box-shadow 0.3s ease;
30 | }
31 |
32 | .team-card:hover {
33 | transform: translateY(-5px);
34 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
35 | }
36 |
37 | .team-image {
38 | border-radius: 50%;
39 | width: 150px;
40 | height: 150px;
41 | object-fit: cover;
42 | margin-bottom: 20px;
43 | }
44 |
45 | .team-name {
46 | font-size: 1.5rem;
47 | font-weight: 600;
48 | color: #333;
49 | margin-bottom: 10px;
50 | }
51 |
52 | .team-role {
53 | font-size: 1.2rem;
54 | font-weight: 500;
55 | color: #666;
56 | margin-bottom: 10px;
57 | }
58 |
59 | .team-links {
60 | display: flex;
61 | justify-content: center;
62 | gap: 15px;
63 | }
64 |
65 | .team-links a {
66 | color: #0073b1;
67 | font-weight: 600;
68 | text-decoration: none;
69 | transition: color 0.3s ease;
70 | }
71 |
72 | .team-links a:hover {
73 | color: #005582;
74 | text-decoration: underline;
75 | }
76 |
77 | @media (max-width: 768px) {
78 | .team-title {
79 | font-size: 2rem;
80 | }
81 |
82 | .team-grid {
83 | grid-template-columns: 1fr;
84 | }
85 |
86 | .team-card {
87 | max-width: 90%;
88 | }
89 | }
--------------------------------------------------------------------------------
/client/src/features/landing-page/components/MeetTheTeam.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import AlexPFP from '../../../public/AlexPFP.jpeg';
3 | import VincentPFP from '../../../public/VincentPFP.png';
4 | import MichaelPFP from '../../../public/MichaelPFP.jpg';
5 | import JonathanPFP from '../../../public/JonathanPFP.jpeg';
6 | import './MeetTheTeam.css';
7 | export default function MeetTheTeam() {
8 | const teamMembers = [
9 | {
10 | name: 'Jonathan Wu',
11 | role: 'Full Stack Developer',
12 | github: 'https://github.com/Jon-Wu1',
13 | linkedin: 'https://www.linkedin.com/in/jonathan-wu1/',
14 | img: JonathanPFP
15 | },
16 | {
17 | name: 'Vincent Collis',
18 | role: 'Full Stack Developer',
19 | github: 'https://github.com/vincentcollis',
20 | linkedin: 'https://www.linkedin.com/in/vincentcollis/',
21 | img: VincentPFP
22 | },
23 | {
24 | name: 'Michael Chen',
25 | role: 'Full Stack Developer',
26 | github: 'https://github.com/mochamochaccino',
27 | linkedin: 'https://www.linkedin.com/in/michael-chen-345b4b1aa/',
28 | img: MichaelPFP
29 | },
30 | {
31 | name: 'Alex Greenberg',
32 | role: 'Full Stack Developer',
33 | github: 'https://github.com/AlexG0718',
34 | linkedin: 'https://www.linkedin.com/in/alex-greenberg-1530a812a/',
35 | img: AlexPFP
36 | }
37 | ];
38 | return (_jsxs("section", { className: "team-section", children: [_jsx("h2", { className: "team-title", children: "Meet the Team" }), _jsx("div", { className: "team-grid", children: teamMembers.map((member) => (_jsxs("div", { className: "team-card", children: [_jsx("img", { src: `${member.img}`, alt: `${member.name} Profile`, className: "team-image" }), _jsx("h3", { className: "team-name", children: member.name }), _jsx("p", { className: "team-role", children: member.role }), _jsxs("div", { className: "team-links", children: [_jsx("a", { href: member.github, target: "_blank", rel: "noopener noreferrer", children: "GitHub" }), _jsx("a", { href: member.linkedin, target: "_blank", rel: "noopener noreferrer", children: "LinkedIn" })] })] }, member.name))) })] }));
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/components/MeetTheTeam.tsx:
--------------------------------------------------------------------------------
1 | import AlexPFP from '../../../public/AlexPFP.jpeg';
2 | import VincentPFP from '../../../public/VincentPFP.png';
3 | import MichaelPFP from '../../../public/MichaelPFP.jpg';
4 | import JonathanPFP from '../../../public/JonathanPFP.jpeg';
5 |
6 | import './MeetTheTeam.css';
7 |
8 | export default function MeetTheTeam () {
9 |
10 | interface TeamMember {
11 | name: string;
12 | role: string;
13 | github: string;
14 | linkedin: string;
15 | img: string;
16 | }
17 |
18 | const teamMembers: TeamMember[] = [
19 | {
20 | name: 'Jonathan Wu',
21 | role: 'Full Stack Developer',
22 | github: 'https://github.com/Jon-Wu1',
23 | linkedin: 'https://www.linkedin.com/in/jonathan-wu1/',
24 | img: JonathanPFP
25 | },
26 | {
27 | name: 'Vincent Collis',
28 | role: 'Full Stack Developer',
29 | github: 'https://github.com/vincentcollis',
30 | linkedin: 'https://www.linkedin.com/in/vincentcollis/',
31 | img: VincentPFP
32 | },
33 | {
34 | name: 'Michael Chen',
35 | role: 'Full Stack Developer',
36 | github: 'https://github.com/mochamochaccino',
37 | linkedin: 'https://www.linkedin.com/in/michael-chen-345b4b1aa/',
38 | img: MichaelPFP
39 | },
40 | {
41 | name: 'Alex Greenberg',
42 | role: 'Full Stack Developer',
43 | github: 'https://github.com/AlexG0718',
44 | linkedin: 'https://www.linkedin.com/in/alex-greenberg-1530a812a/',
45 | img: AlexPFP
46 | }
47 | ]
48 |
49 | return (
50 |
51 |
52 | Meet the Team
53 |
54 |
55 | {teamMembers.map((member) => (
56 |
57 |
58 |

63 |
64 |
{member.name}
65 |
{member.role}
66 |
67 |
75 |
76 | ))}
77 |
78 |
79 | )
80 | }
--------------------------------------------------------------------------------
/client/src/features/landing-page/components/ReadMe.css:
--------------------------------------------------------------------------------
1 | .readme-section {
2 | padding: 50px 0;
3 | background-color: #f9f9f9;
4 | text-align: center;
5 | }
6 |
7 | .readme-title {
8 | font-size: 2.2rem;
9 | font-weight: 700;
10 | color: #333;
11 | margin-bottom: 30px;
12 | }
13 |
14 | .instructions {
15 | max-width: 800px;
16 | margin: 0 auto;
17 | margin-top: 15px;
18 | text-align: left;
19 | font-size: 1.1rem;
20 | color: #555;
21 | line-height: 1.6;
22 | }
23 |
24 | .instructions pre {
25 | background-color: #f0f0f0;
26 | padding: 10px;
27 | border-radius: 5px;
28 | color: #333;
29 | }
30 |
31 | .instructions code {
32 | font-family: 'Courier New', Courier, monospace;
33 | font-size: 1rem;
34 | background-color: #e0e0e0;
35 | padding: 2px 5px;
36 | border-radius: 3px;
37 | }
38 |
39 | .instructions ol {
40 | padding-left: 20px;
41 | }
42 |
43 | .instructions a {
44 | color: #0073b1;
45 | }
46 |
47 | .instructions ul {
48 | margin-left: 40px;
49 | }
50 |
51 | .instructions ul li {
52 | list-style-type: disc;
53 | }
--------------------------------------------------------------------------------
/client/src/features/landing-page/components/ReadMe.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import './ReadMe.css';
3 | export default function ReadMe() {
4 | return (_jsxs("section", { className: "readme-section", children: [_jsx("h2", { className: "readme-title", children: "How to Run the Application" }), _jsx("h3", { children: "## If this application is already running locally, you can skip this section. ##" }), _jsxs("div", { className: "instructions", children: [_jsx("p", { children: "Follow these steps to clone and run the K8State application on your local machine:" }), _jsxs("ol", { children: [_jsx("li", { children: _jsx("strong", { children: "Clone the repository:" }) }), _jsx("pre", { children: _jsx("code", { children: "git clone https://github.com/oslabs-beta/k8state.git" }) }), _jsx("li", { children: _jsx("strong", { children: "Navigate to the client directory:" }) }), _jsx("pre", { children: _jsx("code", { children: "cd k8state" }) }), _jsx("pre", { children: _jsx("code", { children: "cd client" }) }), _jsx("li", { children: _jsx("strong", { children: "Install dependencies:" }) }), _jsx("pre", { children: _jsx("code", { children: "npm install" }) }), _jsx("li", { children: _jsx("strong", { children: "Start the client:" }) }), _jsx("pre", { children: _jsx("code", { children: "npm run dev" }) }), _jsx("li", { children: _jsx("strong", { children: "Navigate to the server directory:" }) }), _jsx("pre", { children: _jsx("code", { children: "cd .." }) }), _jsx("pre", { children: _jsx("code", { children: "cd server" }) }), _jsx("li", { children: _jsx("strong", { children: "Install dependencies:" }) }), _jsx("pre", { children: _jsx("code", { children: "npm install" }) }), _jsx("li", { children: _jsx("strong", { children: "Start the development server:" }) }), _jsx("pre", { children: _jsx("code", { children: "npm run server" }) }), _jsx("li", { children: _jsx("strong", { children: "Access the app:" }) }), _jsxs("p", { children: ["Once the server is running, open ", _jsx("a", { href: "http://localhost:3000", target: "_blank", rel: "noopener noreferrer", children: "http://localhost:3000" }), " in your browser to view the application."] })] })] })] }));
5 | }
6 | ;
7 |
--------------------------------------------------------------------------------
/client/src/features/landing-page/components/ReadMe.tsx:
--------------------------------------------------------------------------------
1 | import './ReadMe.css'
2 |
3 | export default function ReadMe () {
4 |
5 | return (
6 |
7 |
8 | How to Run the Application
9 | ## If this application is already running locally, you can skip this section. ##
10 |
11 |
12 | Follow these steps to clone and run the K8State application on your local machine:
13 |
14 |
15 | - Clone the repository:
16 | git clone https://github.com/oslabs-beta/k8state.git
17 |
18 | - Navigate to the client directory:
19 | cd k8state
20 | cd client
21 |
22 | - Install dependencies:
23 | npm install
24 |
25 | - Start the client:
26 | npm run dev
27 |
28 | - Navigate to the server directory:
29 | cd ..
30 | cd server
31 |
32 | - Install dependencies:
33 | npm install
34 |
35 | - Start the development server:
36 | npm run server
37 |
38 | - Access the app:
39 | Once the server is running, open http://localhost:3000 in your browser to view the application.
40 |
41 |
42 |
43 |
44 | );
45 | };
--------------------------------------------------------------------------------
/client/src/features/landing-page/landingpage.css:
--------------------------------------------------------------------------------
1 | .landingpage {
2 | display: flex;
3 | min-height: calc(100vh - 64px); /* Adjusting for AppBar height */
4 | align-items: center;
5 | }
6 |
7 | .logo-main {
8 | filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.2))
9 | drop-shadow(0 30px 80px rgba(93, 52, 200, 0.2))
10 | drop-shadow(0 60px 20px rgba(128, 90, 213, 0.2));
11 | }
12 |
13 | @keyframes App-logo-float {
14 | 0% {
15 | transform: translateY(0);
16 | }
17 | 50% {
18 | transform: translateY(10px);
19 | }
20 | 100% {
21 | transform: translateY(0px);
22 | }
23 | }
24 |
25 | .footer {
26 | background: #f8f8f8;
27 | padding: 20px 0;
28 | border-top: 1px solid #ddd;
29 | text-align: center;
30 | margin-top: auto;
31 | width: 100vw;
32 | }
33 |
34 | footer p {
35 | font-size: 0.9rem;
36 | color: #666;
37 | }
38 |
39 | @media (prefers-reduced-motion: no-preference) {
40 | .logo-main {
41 | animation: App-logo-float infinite 3s ease-in-out;
42 | }
43 | }
--------------------------------------------------------------------------------
/client/src/features/settings/settings.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2 | import Box from "@mui/material/Box";
3 | import TextField from "@mui/material/TextField";
4 | import Stack from "@mui/material/Stack";
5 | import Button from "@mui/material/Button";
6 | import Alert from "@mui/material/Alert";
7 | import { useState } from "react";
8 | const Settings = () => {
9 | const [inputError, setInputError] = useState(false);
10 | const [envAddress, setEnvAddress] = useState(null);
11 | const [envKey, setEnvKey] = useState(null);
12 | const [envAlertMessage, setEnvAlertMessage] = useState(null);
13 | const [envAlert, setEnvAlert] = useState(false);
14 | const handleEnvSubmit = async (event) => {
15 | event.preventDefault();
16 | if (inputError === true || envAlertMessage === "Success!")
17 | return;
18 | try {
19 | const response = await fetch("http://localhost:8080/api/checkAPI", {
20 | method: "POST",
21 | headers: {
22 | "Content-Type": "application/json",
23 | },
24 | body: JSON.stringify({
25 | address: envAddress,
26 | key: envKey,
27 | }),
28 | });
29 | const data = await response.json();
30 | if (data.message === "ok") {
31 | setEnvAlertMessage("Success!");
32 | setEnvAlert(true);
33 | setTimeout(() => setEnvAlert(false), 5000);
34 | setTimeout(() => setEnvAlertMessage(null), 5000);
35 | }
36 | else {
37 | setEnvAlertMessage("Invalid Address or Key");
38 | setEnvAlert(true);
39 | setInputError(true);
40 | setTimeout(() => setEnvAlert(false), 5000);
41 | setTimeout(() => setInputError(false), 5000);
42 | setTimeout(() => setEnvAlertMessage(null), 5000);
43 | }
44 | }
45 | catch (error) {
46 | throw new Error(`Something went wrong: ${error.message}`);
47 | }
48 | };
49 | return (_jsxs(Box, { component: "form", sx: { "& > :not(style)": { m: 1, width: "25ch" } }, noValidate: true, autoComplete: "off", style: {
50 | position: "relative",
51 | width: "max-content",
52 | height: "max-content",
53 | }, children: [_jsx("h1", { style: {
54 | width: "max-content",
55 | position: "relative",
56 | alignContent: "center",
57 | top: "-170px",
58 | left: "-45px",
59 | }, children: "Change Cluster Address and Key" }), _jsxs("div", { style: { position: "relative" }, children: [_jsx(TextField, { "aria-label": "Address", label: "Address", color: envAlertMessage === "Success!" ? "success" : "primary", helperText: envAddress?.includes("www.")
60 | ? "Do not include 'www' before Cluster URL"
61 | : null, error: inputError ||
62 | (envAddress?.includes("www.") && !envAddress?.includes("www.com")), placeholder: "clusterurl.com:00000", onChange: (e) => setEnvAddress(e.target.value), focused: true, style: {
63 | position: "absolute",
64 | top: "-150px",
65 | left: "45px",
66 | width: "300px",
67 | } }), _jsx(TextField, { label: "Key", color: envAlertMessage === "Success!" ? "success" : "primary", error: inputError, placeholder: "yJhbGciOiJSUzI1NiIsImtpZCI6ImhzU...", onChange: (e) => setEnvKey(e.target.value), focused: true, style: {
68 | position: "absolute",
69 | top: "-60px",
70 | left: "45px",
71 | width: "300px",
72 | } }), _jsx("div", { style: {
73 | position: "relative",
74 | top: "10px",
75 | left: "150px",
76 | }, children: _jsx(Stack, { direction: "row", spacing: 2, children: _jsx(Button, { variant: inputError === true ? "outlined" : "contained", color: inputError === true
77 | ? "error"
78 | : envAlertMessage === "Success!"
79 | ? "success"
80 | : "secondary", disabled: envAddress?.includes("www.") && !envAddress?.includes("www.com")
81 | ? true
82 | : false, onClick: handleEnvSubmit, style: { marginTop: "16px" }, children: "Submit" }) }) })] }), _jsx("div", { style: {
83 | position: "absolute",
84 | left: "90px",
85 | top: "145px",
86 | marginTop: "16px",
87 | }, children: _jsx(Stack, { sx: { width: "100%" }, spacing: 2, children: envAlert && (_jsx(Alert, { severity: inputError === true ? "error" : "success", children: envAlertMessage })) }) })] }));
88 | };
89 | export default Settings;
90 |
--------------------------------------------------------------------------------
/client/src/features/settings/settings.test.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { afterEach, test } from "vitest";
3 | import { screen, cleanup } from "@testing-library/react";
4 | import { renderWithProviders } from "../../utils/test-utils";
5 | import Settings from "./settings";
6 | describe("if setting page renders", () => {
7 | beforeEach(() => {
8 | renderWithProviders(_jsx(Settings, {}));
9 | });
10 | afterEach(() => {
11 | cleanup();
12 | });
13 | test("test if submit button renders", () => {
14 | const submitButton = screen.getByRole("button", { name: /Submit/i });
15 | expect(submitButton).toBeInTheDocument();
16 | });
17 | test("test if address input box renders", () => {
18 | const addressInputBox = screen.getByRole("textbox", { name: /address/i });
19 | expect(addressInputBox).toBeInTheDocument();
20 | });
21 | test("test if key input box renders", () => {
22 | const addressInputBox = screen.getByRole("textbox", { name: /key/i });
23 | expect(addressInputBox).toBeInTheDocument();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/client/src/features/settings/settings.test.tsx:
--------------------------------------------------------------------------------
1 | import { afterEach, test } from "vitest"
2 | import { screen, cleanup } from "@testing-library/react"
3 | import userEvent from "@testing-library/user-event"
4 | import { renderWithProviders } from "../../utils/test-utils"
5 |
6 | import Settings from "./settings"
7 |
8 | describe("if setting page renders", () => {
9 | beforeEach(() => {
10 | renderWithProviders()
11 | })
12 | afterEach(() => {
13 | cleanup()
14 | })
15 |
16 | test("test if submit button renders", () => {
17 | const submitButton = screen.getByRole("button", { name: /Submit/i })
18 | expect(submitButton).toBeInTheDocument()
19 | })
20 |
21 | test("test if address input box renders", () => {
22 | const addressInputBox = screen.getByRole("textbox", { name: /address/i })
23 | expect(addressInputBox).toBeInTheDocument()
24 | })
25 |
26 | test("test if key input box renders", () => {
27 | const addressInputBox = screen.getByRole("textbox", { name: /key/i })
28 | expect(addressInputBox).toBeInTheDocument()
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/client/src/features/settings/settings.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react"
2 | import Box from "@mui/material/Box"
3 | import TextField from "@mui/material/TextField"
4 | import Stack from "@mui/material/Stack"
5 | import Button from "@mui/material/Button"
6 | import Alert from "@mui/material/Alert"
7 | import { useState } from "react"
8 |
9 | const Settings = () => {
10 | const [inputError, setInputError] = useState(false)
11 | const [envAddress, setEnvAddress] = useState(null)
12 | const [envKey, setEnvKey] = useState(null)
13 | const [envAlertMessage, setEnvAlertMessage] = useState(null)
14 | const [envAlert, setEnvAlert] = useState(false)
15 |
16 | const handleEnvSubmit = async (event: React.MouseEvent) => {
17 | event.preventDefault()
18 | if (inputError === true || envAlertMessage === "Success!") return
19 |
20 | try {
21 | const response = await fetch("http://localhost:8080/api/checkAPI", {
22 | method: "POST",
23 | headers: {
24 | "Content-Type": "application/json",
25 | },
26 | body: JSON.stringify({
27 | address: envAddress,
28 | key: envKey,
29 | }),
30 | })
31 |
32 | const data = await response.json()
33 |
34 | if (data.message === "ok") {
35 | setEnvAlertMessage("Success!")
36 | setEnvAlert(true)
37 | setTimeout(() => setEnvAlert(false), 5000)
38 | setTimeout(() => setEnvAlertMessage(null), 5000)
39 | } else {
40 | setEnvAlertMessage("Invalid Address or Key")
41 | setEnvAlert(true)
42 | setInputError(true)
43 | setTimeout(() => setEnvAlert(false), 5000)
44 | setTimeout(() => setInputError(false), 5000)
45 | setTimeout(() => setEnvAlertMessage(null), 5000)
46 | }
47 | } catch (error) {
48 | throw new Error(`Something went wrong: ${(error as Error).message}`)
49 | }
50 | }
51 |
52 | return (
53 | :not(style)": { m: 1, width: "25ch" } }}
56 | noValidate
57 | autoComplete="off"
58 | style={{
59 | position: "relative",
60 | width: "max-content",
61 | height: "max-content",
62 | }}
63 | >
64 |
73 | Change Cluster Address and Key
74 |
75 |
76 |
) =>
91 | setEnvAddress(e.target.value)
92 | }
93 | focused
94 | style={{
95 | position: "absolute",
96 | top: "-150px",
97 | left: "45px",
98 | width: "300px",
99 | }}
100 | />
101 | ) =>
107 | setEnvKey(e.target.value)
108 | }
109 | focused
110 | style={{
111 | position: "absolute",
112 | top: "-60px",
113 | left: "45px",
114 | width: "300px",
115 | }}
116 | />
117 |
124 |
125 |
144 |
145 |
146 |
147 |
155 |
156 | {envAlert && (
157 |
158 | {envAlertMessage}
159 |
160 | )}
161 |
162 |
163 |
164 | )
165 | }
166 |
167 | export default Settings
168 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/main.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import React from "react";
3 | import { createRoot } from "react-dom/client";
4 | import { Provider } from "react-redux";
5 | import App from "./App";
6 | import LandingPage from "./features/landing-page/LandingPage";
7 | import { store } from "./app/store";
8 | import { createBrowserRouter, RouterProvider } from "react-router-dom";
9 | import "./index.css";
10 | import CaptivePortal from "./features/captive-portal/containers/CaptivePortal";
11 | import ClusterLogContainer from "./features/cluster-log/containers/ClusterLogContainer";
12 | import Settings from "./features/settings/settings";
13 | import "@fontsource/roboto/300.css";
14 | import "@fontsource/roboto/400.css";
15 | import "@fontsource/roboto/500.css";
16 | import "@fontsource/roboto/700.css";
17 | import ProtectedRoute from "./features/captive-portal/ProtectedRoute";
18 | import { createTheme, ThemeProvider, alpha, getContrastRatio, } from "@mui/material/styles";
19 | const violetBase = "#7F00FF";
20 | const violetMain = alpha(violetBase, 0.7);
21 | const theme = createTheme({
22 | palette: {
23 | violet: {
24 | main: violetMain,
25 | light: alpha(violetBase, 0.5),
26 | dark: alpha(violetBase, 0.9),
27 | contrastText: getContrastRatio(violetMain, "#fff") > 4.5 ? "#fff" : "#111",
28 | },
29 | },
30 | typography: {
31 | fontFamily: '"Roboto", sans-serif',
32 | h1: {
33 | fontFamily: '"Oswald", sans-serif',
34 | fontWeight: 900,
35 | },
36 | },
37 | });
38 | const router = createBrowserRouter([
39 | {
40 | path: "/",
41 | element: _jsx(LandingPage, {}),
42 | },
43 | {
44 | path: "/clusterui",
45 | element: _jsx(ProtectedRoute, { element: _jsx(App, {}) }),
46 | },
47 | {
48 | path: "/portal",
49 | element: _jsx(CaptivePortal, {}),
50 | },
51 | {
52 | path: "/logs",
53 | element: _jsx(ClusterLogContainer, {}),
54 | },
55 | {
56 | path: "/settings",
57 | element: _jsx(Settings, {}),
58 | },
59 | ]);
60 | const container = document.getElementById("root");
61 | if (container) {
62 | const root = createRoot(container);
63 | root.render(_jsx(React.StrictMode, { children: _jsx(Provider, { store: store, children: _jsx(ThemeProvider, { theme: theme, children: _jsx(RouterProvider, { router: router }) }) }) }));
64 | }
65 | else {
66 | throw new Error("Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.");
67 | }
68 |
--------------------------------------------------------------------------------
/client/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { createRoot } from "react-dom/client"
3 | import { Provider } from "react-redux"
4 | import App from "./App"
5 | import LandingPage from "./features/landing-page/LandingPage"
6 | import { store } from "./app/store"
7 | import { createBrowserRouter, RouterProvider } from "react-router-dom"
8 | import "./index.css"
9 | import CaptivePortal from "./features/captive-portal/containers/CaptivePortal"
10 | import ClusterLogContainer from "./features/cluster-log/containers/ClusterLogContainer"
11 | import Settings from "./features/settings/settings"
12 | import "@fontsource/roboto/300.css"
13 | import "@fontsource/roboto/400.css"
14 | import "@fontsource/roboto/500.css"
15 | import "@fontsource/roboto/700.css"
16 | import ProtectedRoute from "./features/captive-portal/ProtectedRoute"
17 |
18 | import {
19 | createTheme,
20 | ThemeProvider,
21 | alpha,
22 | getContrastRatio,
23 | } from "@mui/material/styles"
24 |
25 | // Augment the palette to include a violet color
26 | declare module "@mui/material/styles" {
27 | interface Palette {
28 | violet: Palette["primary"]
29 | }
30 |
31 | interface PaletteOptions {
32 | violet?: PaletteOptions["primary"]
33 | }
34 | }
35 |
36 | // Update the Button's color options to include a violet option
37 | declare module "@mui/material/Button" {
38 | interface ButtonPropsColorOverrides {
39 | violet: true
40 | }
41 | }
42 |
43 | const violetBase = "#7F00FF"
44 | const violetMain = alpha(violetBase, 0.7)
45 |
46 | const theme = createTheme({
47 | palette: {
48 | violet: {
49 | main: violetMain,
50 | light: alpha(violetBase, 0.5),
51 | dark: alpha(violetBase, 0.9),
52 | contrastText:
53 | getContrastRatio(violetMain, "#fff") > 4.5 ? "#fff" : "#111",
54 | },
55 | },
56 | typography: {
57 | fontFamily: '"Roboto", sans-serif',
58 | h1: {
59 | fontFamily: '"Oswald", sans-serif',
60 | fontWeight: 900,
61 | },
62 | },
63 | })
64 |
65 | const router = createBrowserRouter([
66 | {
67 | path: "/",
68 | element: ,
69 | },
70 | {
71 | path: "/clusterui",
72 | element: } />,
73 | },
74 | {
75 | path: "/portal",
76 | element: ,
77 | },
78 | {
79 | path: "/logs",
80 | element: ,
81 | },
82 | {
83 | path: "/settings",
84 | element: ,
85 | },
86 | ])
87 |
88 | const container = document.getElementById("root")
89 |
90 | if (container) {
91 | const root = createRoot(container)
92 |
93 | root.render(
94 |
95 |
96 |
97 |
98 |
99 |
100 | ,
101 | )
102 | } else {
103 | throw new Error(
104 | "Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.",
105 | )
106 | }
107 |
--------------------------------------------------------------------------------
/client/src/public/AlexPFP.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/k8state/2cc59e952e86a7ce93abc8b43c27d9783d9c5001/client/src/public/AlexPFP.jpeg
--------------------------------------------------------------------------------
/client/src/public/JonathanPFP.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/k8state/2cc59e952e86a7ce93abc8b43c27d9783d9c5001/client/src/public/JonathanPFP.jpeg
--------------------------------------------------------------------------------
/client/src/public/MichaelPFP.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/k8state/2cc59e952e86a7ce93abc8b43c27d9783d9c5001/client/src/public/MichaelPFP.jpg
--------------------------------------------------------------------------------
/client/src/public/VincentPFP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/k8state/2cc59e952e86a7ce93abc8b43c27d9783d9c5001/client/src/public/VincentPFP.png
--------------------------------------------------------------------------------
/client/src/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/k8state/2cc59e952e86a7ce93abc8b43c27d9783d9c5001/client/src/public/logo.png
--------------------------------------------------------------------------------
/client/src/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/client/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/vitest";
2 | import "@testing-library/jest-dom";
3 | // This mock will replace the real ResizeObserver with an empty mock that has the same interface but doesn’t do anything. This should prevent the error from occurring during testing.
4 | class ResizeObserver {
5 | observe() { }
6 | unobserve() { }
7 | disconnect() { }
8 | }
9 | global.ResizeObserver = ResizeObserver;
10 |
--------------------------------------------------------------------------------
/client/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/vitest"
2 | import "@testing-library/jest-dom"
3 |
4 | // This mock will replace the real ResizeObserver with an empty mock that has the same interface but doesn’t do anything. This should prevent the error from occurring during testing.
5 | class ResizeObserver {
6 | observe() {}
7 | unobserve() {}
8 | disconnect() {}
9 | }
10 | global.ResizeObserver = ResizeObserver
11 |
--------------------------------------------------------------------------------
/client/src/utils/test-utils.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import { Provider } from "react-redux";
5 | import { makeStore } from "../app/store";
6 | /**
7 | * Renders the given React element with Redux Provider and custom store.
8 | * This function is useful for testing components that are connected to the Redux store.
9 | *
10 | * @param ui - The React component or element to render.
11 | * @param extendedRenderOptions - Optional configuration options for rendering. This includes `preloadedState` for initial Redux state and `store` for a specific Redux store instance. Any additional properties are passed to React Testing Library's render function.
12 | * @returns An object containing the Redux store used in the render, User event API for simulating user interactions in tests, and all of React Testing Library's query functions for testing the component.
13 | */
14 | export const renderWithProviders = (ui, extendedRenderOptions = {}) => {
15 | const { preloadedState = {},
16 | // Automatically create a store instance if no store was passed in
17 | store = makeStore(preloadedState), ...renderOptions } = extendedRenderOptions;
18 | const Wrapper = ({ children }) => (_jsx(Provider, { store: store, children: children }));
19 | // Return an object with the store and all of RTL's query functions
20 | return {
21 | store,
22 | user: userEvent.setup(),
23 | ...render(ui, { wrapper: Wrapper, ...renderOptions }),
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/utils/test-utils.tsx:
--------------------------------------------------------------------------------
1 | import type { RenderOptions } from "@testing-library/react"
2 | import { render } from "@testing-library/react"
3 | import userEvent from "@testing-library/user-event"
4 | import type { PropsWithChildren, ReactElement } from "react"
5 | import { Provider } from "react-redux"
6 | import type { AppStore, RootState } from "../app/store"
7 | import { makeStore } from "../app/store"
8 |
9 | /**
10 | * This type extends the default options for
11 | * React Testing Library's render function. It allows for
12 | * additional configuration such as specifying an initial Redux state and
13 | * a custom store instance.
14 | */
15 | interface ExtendedRenderOptions extends Omit {
16 | /**
17 | * Defines a specific portion or the entire initial state for the Redux store.
18 | * This is particularly useful for initializing the state in a
19 | * controlled manner during testing, allowing components to be rendered
20 | * with predetermined state conditions.
21 | */
22 | preloadedState?: Partial
23 |
24 | /**
25 | * Allows the use of a specific Redux store instance instead of a
26 | * default or global store. This flexibility is beneficial when
27 | * testing components with unique store requirements or when isolating
28 | * tests from a global store state. The custom store should be configured
29 | * to match the structure and middleware of the store used by the application.
30 | *
31 | * @default makeStore(preloadedState)
32 | */
33 | store?: AppStore
34 | }
35 |
36 | /**
37 | * Renders the given React element with Redux Provider and custom store.
38 | * This function is useful for testing components that are connected to the Redux store.
39 | *
40 | * @param ui - The React component or element to render.
41 | * @param extendedRenderOptions - Optional configuration options for rendering. This includes `preloadedState` for initial Redux state and `store` for a specific Redux store instance. Any additional properties are passed to React Testing Library's render function.
42 | * @returns An object containing the Redux store used in the render, User event API for simulating user interactions in tests, and all of React Testing Library's query functions for testing the component.
43 | */
44 | export const renderWithProviders = (
45 | ui: ReactElement,
46 | extendedRenderOptions: ExtendedRenderOptions = {},
47 | ) => {
48 | const {
49 | preloadedState = {},
50 | // Automatically create a store instance if no store was passed in
51 | store = makeStore(preloadedState),
52 | ...renderOptions
53 | } = extendedRenderOptions
54 |
55 | const Wrapper = ({ children }: PropsWithChildren) => (
56 | {children}
57 | )
58 |
59 | // Return an object with the store and all of RTL's query functions
60 | return {
61 | store,
62 | user: userEvent.setup(),
63 | ...render(ui, { wrapper: Wrapper, ...renderOptions }),
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "module": "ESNext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": false,
16 | "jsx": "react-jsx",
17 | "types": ["vitest/globals", "jest"]
18 | },
19 | "references": [{ "path": "./tsconfig.node.json" }]
20 | }
21 |
--------------------------------------------------------------------------------
/client/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/client/vite.config.d.ts:
--------------------------------------------------------------------------------
1 | // declare const _default: import("vite").UserConfig
2 | // export default _default
3 |
4 | import type { UserConfig } from "vite"
5 |
6 | declare const config: UserConfig
7 | export default config
8 |
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 | import react from "@vitejs/plugin-react";
3 | // https://vitejs.dev/config/
4 | export default defineConfig({
5 | plugins: [react()],
6 | server: {
7 | port: 3000,
8 | open: true,
9 | proxy: {
10 | // with options: http://localhost:3000/api/-> http://localhost:8080/api/
11 | "/api": {
12 | target: "http://localhost:8080/api/",
13 | changeOrigin: true,
14 | rewrite: path => path.replace(/^\/api/, ""),
15 | },
16 | },
17 | },
18 | preview: {
19 | port: 8080,
20 | },
21 | test: {
22 | globals: true,
23 | environment: "jsdom",
24 | setupFiles: "src/setupTests",
25 | mockReset: true,
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config"
2 | import react from "@vitejs/plugin-react"
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 3000,
9 | open: true,
10 | proxy: {
11 | "/api": {
12 | target: "http://localhost:8080/api/",
13 | changeOrigin: true,
14 | rewrite: path => path.replace(/^\/api/, ""),
15 | },
16 | },
17 | },
18 | preview: {
19 | port: 8080,
20 | },
21 | test: {
22 | globals: true,
23 | environment: "jsdom",
24 | setupFiles: "src/setupTests",
25 | mockReset: true,
26 | },
27 | })
28 |
--------------------------------------------------------------------------------
/k8state.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/k8state/2cc59e952e86a7ce93abc8b43c27d9783d9c5001/k8state.png
--------------------------------------------------------------------------------
/server/controllers/generalController.js:
--------------------------------------------------------------------------------
1 | import generalService from '../services/generalService.js';
2 | import kubernetesService from '../services/kubernetesService.js';
3 | import fs from 'fs';
4 | import path from 'path';
5 | const generalController = {
6 | //middleware function to check if the env file exists
7 | checkEnv: (_req, res, next) => {
8 | try {
9 | const check = generalService.checkEnv();
10 | if (check === 'exist') {
11 | res.locals.env = {
12 | address: process.env.KUBERNETES_SERVER,
13 | key: process.env.KUBERNETES_TOKEN,
14 | };
15 | }
16 | else {
17 | res.locals.env = check;
18 | }
19 | next();
20 | }
21 | catch (error) {
22 | res.status(500).json({ message: 'error checking env ' });
23 | throw new Error(`Something went wrong: ${error.message}`);
24 | }
25 | },
26 | //middleware function to write logs
27 | writeLog: async (_req, res, next) => {
28 | generalService.checkLogs();
29 | const pods = res.locals.podData;
30 | const podNames = [];
31 | for (let element of pods) {
32 | podNames.push({
33 | name: element.name,
34 | namespace: element.namespace,
35 | });
36 | }
37 | const logs = await kubernetesService.getLogs(podNames);
38 | generalService.writeLogs(logs);
39 | res.locals.logs = logs;
40 | next();
41 | },
42 | //middleware function to provide a file for download to the frontend
43 | getDownloadSpecificLog: (req, res, next) => {
44 | const logDir = path.resolve('../logs/') + '/' + req.params.log;
45 | res.download(logDir, (err) => {
46 | if (err) {
47 | throw new Error(`Something went wrong: ${err.message}`);
48 | }
49 | else {
50 | next();
51 | }
52 | });
53 | },
54 | //middleware function to grab the logs in the logs folder
55 | getLogs: (_req, res, next) => {
56 | generalService.checkLogs();
57 | const result = generalService.getDirLogs();
58 | const logHolder = result.map((element, _index) => {
59 | const logDir = path.resolve('../logs/') + '/' + element;
60 | return {
61 | name: element,
62 | log: JSON.parse(fs.readFileSync(logDir, 'utf-8')),
63 | };
64 | });
65 | res.locals.dirLogs = logHolder;
66 | next();
67 | },
68 | //middleware function to delete a specific log in the logs folder
69 | deleteSpecificLog: (req, res, next) => {
70 | const logDir = path.resolve('../logs/') + '/' + req.params.log;
71 | try {
72 | fs.unlinkSync(logDir);
73 | res.locals.deletedLog = req.params.log;
74 | }
75 | catch (error) {
76 | res.locals.deletedLog = 'failed to delete';
77 | throw new Error(`Something went wrong: ${error.message}`);
78 | }
79 | next();
80 | },
81 | };
82 | export default generalController;
83 |
--------------------------------------------------------------------------------
/server/controllers/generalController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import generalService from '../services/generalService.js';
3 | import kubernetesService from '../services/kubernetesService.js';
4 | import fs from 'fs';
5 | import path from 'path';
6 |
7 | const generalController = {
8 | //middleware function to check if the env file exists
9 | checkEnv: (_req: Request, res: Response, next: NextFunction) => {
10 | interface addresskey {
11 | address: string;
12 | key: string;
13 | }
14 | try {
15 | const check = generalService.checkEnv();
16 | if (check === 'exist') {
17 | res.locals.env = {
18 | address: process.env.KUBERNETES_SERVER,
19 | key: process.env.KUBERNETES_TOKEN,
20 | } as addresskey;
21 | } else {
22 | res.locals.env = check;
23 | }
24 | next();
25 | } catch (error) {
26 | res.status(500).json({ message: 'error checking env ' });
27 | throw new Error(`Something went wrong: ${(error as Error).message}`);
28 | }
29 | },
30 | //middleware function to write logs
31 | writeLog: async (_req: Request, res: Response, next: NextFunction) => {
32 | interface info {
33 | name: string;
34 | namespace: string;
35 | }
36 | generalService.checkLogs();
37 | const pods = res.locals.podData;
38 | const podNames: info[] = [];
39 | for (let element of pods) {
40 | podNames.push({
41 | name: element.name,
42 | namespace: element.namespace,
43 | } as info);
44 | }
45 | const logs = await kubernetesService.getLogs(podNames);
46 | generalService.writeLogs(logs);
47 | res.locals.logs = logs;
48 | next();
49 | },
50 | //middleware function to provide a file for download to the frontend
51 | getDownloadSpecificLog: (req: Request, res: Response, next: NextFunction) => {
52 | const logDir: string = path.resolve('../logs/') + '/' + req.params.log;
53 | res.download(logDir, (err) => {
54 | if (err) {
55 | throw new Error(`Something went wrong: ${(err as Error).message}`);
56 | } else {
57 | next();
58 | }
59 | });
60 | },
61 | //middleware function to grab the logs in the logs folder
62 | getLogs: (_req: Request, res: Response, next: NextFunction) => {
63 | generalService.checkLogs();
64 | const result: string[] = generalService.getDirLogs();
65 | const logHolder = result.map((element, _index) => {
66 | const logDir: string = path.resolve('../logs/') + '/' + element;
67 | return {
68 | name: element,
69 | log: JSON.parse(fs.readFileSync(logDir, 'utf-8')),
70 | };
71 | });
72 | res.locals.dirLogs = logHolder;
73 | next();
74 | },
75 | //middleware function to delete a specific log in the logs folder
76 | deleteSpecificLog: (req: Request, res: Response, next: NextFunction) => {
77 | const logDir: string = path.resolve('../logs/') + '/' + req.params.log;
78 | try {
79 | fs.unlinkSync(logDir);
80 | res.locals.deletedLog = req.params.log;
81 | } catch (error) {
82 | res.locals.deletedLog = 'failed to delete';
83 | throw new Error(`Something went wrong: ${(error as Error).message}`);
84 | }
85 | next();
86 | },
87 | };
88 |
89 | export default generalController;
90 |
--------------------------------------------------------------------------------
/server/controllers/kubernetesController.js:
--------------------------------------------------------------------------------
1 | import kubernetesService from '../services/kubernetesService.js';
2 | import generalService from '../services/generalService.js';
3 | // ***** Controller Object *****
4 | const kubernetesController = {
5 | // Middleware function to get all pods from the cluster
6 | getPods: async (_req, res, next) => {
7 | try {
8 | const allPods = await kubernetesService.getPodsFromCluster();
9 | const returnedPods = [];
10 | for (const pod of allPods) {
11 | const newPod = {
12 | name: pod.metadata?.name || 'Unknown Pod Name',
13 | creationTimestamp: pod.metadata?.creationTimestamp || undefined,
14 | namespace: pod.metadata?.namespace || 'Unknown namespce',
15 | labels: pod.metadata?.labels || undefined,
16 | nodeName: pod.spec?.nodeName,
17 | restartPolicy: pod.spec?.restartPolicy || 'Unknown restart policy',
18 | hostIP: pod.status?.hostIP || 'Unknown host IP',
19 | podIP: pod.status?.podIP || 'Unknown pod IP',
20 | phase: pod.status?.phase || 'Unknown phase',
21 | conditions: pod.status?.conditions || undefined,
22 | startTime: pod.status?.startTime || undefined,
23 | uid: pod.metadata?.uid || undefined,
24 | };
25 | returnedPods.push(newPod);
26 | }
27 | res.locals.podData = returnedPods;
28 | next();
29 | }
30 | catch (error) {
31 | res.status(500).json({ message: 'Error fetching pods from cluster' });
32 | throw new Error(`Something went wrong: ${error.message}`);
33 | }
34 | },
35 | // Middleware function to get details on a single pod from the cluster
36 | getPodDetails: async (req, res, next) => {
37 | const { podName, namespace } = req.params;
38 | try {
39 | const pod = await kubernetesService.getPodDetailsFromCluster(podName, namespace);
40 | const newPod = {
41 | name: pod.metadata?.name || 'Unknown Pod Name',
42 | creationTimestamp: pod.metadata?.creationTimestamp || undefined,
43 | namespace: pod.metadata?.namespace || 'Unknown namespce',
44 | labels: pod.metadata?.labels || undefined,
45 | nodeName: pod.spec?.nodeName,
46 | restartPolicy: pod.spec?.restartPolicy || 'Unknown restart policy',
47 | hostIP: pod.status?.hostIP || 'Unknown host IP',
48 | podIP: pod.status?.podIP || 'Unknown pod IP',
49 | phase: pod.status?.phase || 'Unknown phase',
50 | conditions: pod.status?.conditions || undefined,
51 | startTime: pod.status?.startTime || undefined,
52 | };
53 | res.locals.pod = newPod;
54 | next();
55 | }
56 | catch (error) {
57 | throw new Error(`Error occurred while fetching pod data for pod: ${podName} in namespace: ${namespace}`);
58 | }
59 | },
60 | // Middleware function to get all nodes from the cluster
61 | getNodes: async (_req, res, next) => {
62 | try {
63 | const allNodes = await kubernetesService.getNodesFromCluster();
64 | const returnedNodes = [];
65 | for (const node of allNodes) {
66 | const newNode = {
67 | creationTimestamp: node.metadata?.creationTimestamp,
68 | name: node.metadata?.name,
69 | namespace: node.metadata?.namespace,
70 | labels: node.metadata?.labels,
71 | podCIDR: node.spec?.podCIDR,
72 | addresses: node.status?.addresses,
73 | allocatable: node.status?.allocatable,
74 | capacity: node.status?.capacity,
75 | conditions: node.status?.conditions,
76 | };
77 | returnedNodes.push(newNode);
78 | }
79 | res.locals.nodeData = returnedNodes;
80 | next();
81 | }
82 | catch (error) {
83 | res.status(500).json({ message: 'Error fetching services from cluster' });
84 | throw new Error(`Something went wrong: ${error.message}`);
85 | }
86 | },
87 | // Middleware function to get all services from the cluster
88 | getServices: async (_req, res, next) => {
89 | try {
90 | const allServices = await kubernetesService.getServicesFromCluster();
91 | const returnedServices = [];
92 | for (const services of allServices) {
93 | const newService = {
94 | name: services.metadata?.name,
95 | namespace: services.metadata?.namespace,
96 | labels: services.metadata?.labels,
97 | creationTimestamp: services.metadata?.creationTimestamp,
98 | clusterIP: services.spec?.clusterIP,
99 | ports: services.spec?.ports,
100 | selector: services.spec?.selector,
101 | type: services.spec?.type,
102 | };
103 | returnedServices.push(newService);
104 | }
105 | res.locals.serviceData = returnedServices;
106 | next();
107 | }
108 | catch (error) {
109 | res.status(500).json({ message: 'Error fetching nodes from cluster' });
110 | throw new Error(`Something went wrong: ${error.message}`);
111 | }
112 | },
113 | //middleware function to check if the user provided key and address are valid
114 | checkAPI: async (req, res, next) => {
115 | const key = req.body.key;
116 | const address = req.body.address;
117 | let cleanAddress = address;
118 | if (cleanAddress) {
119 | cleanAddress = address.replace(/https?:\/\//, '');
120 | try {
121 | const check = await kubernetesService.checkAPI(key, cleanAddress);
122 | if (check === 'ok') {
123 | generalService.writeEnv(key, cleanAddress);
124 | next();
125 | }
126 | else if (check === 'invalidkey') {
127 | res.status(403).json({ message: 'invalid_key' });
128 | }
129 | else {
130 | res.status(500).json({ message: 'unable to connect to cluster' });
131 | }
132 | }
133 | catch (error) {
134 | res.status(500).json({ message: 'error checking API ' });
135 | throw new Error(`Something went wrong: ${error.message}`);
136 | }
137 | }
138 | else {
139 | res.status(500).json({ message: 'no address given' });
140 | }
141 | },
142 | };
143 | // Exports the controller object for use as middleware
144 | export default kubernetesController;
145 |
--------------------------------------------------------------------------------
/server/controllers/kubernetesController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import * as k8s from '@kubernetes/client-node';
3 | import kubernetesService from '../services/kubernetesService.js';
4 | import generalService from '../services/generalService.js';
5 |
6 | // ***** Define Interfaces *****
7 | interface ReturnedPod {
8 | name: string;
9 | creationTimestamp: Date | undefined;
10 | namespace: String;
11 | labels: { [key: string]: string } | undefined;
12 | nodeName: String | undefined;
13 | restartPolicy: String;
14 | hostIP: String;
15 | podIP: String;
16 | phase: string | undefined;
17 | conditions: k8s.V1PodCondition[] | undefined;
18 | startTime: Date | undefined;
19 | uid: String | undefined;
20 | }
21 |
22 | interface ReturnedNode {
23 | name: String | undefined;
24 | namespace: String | undefined;
25 | creationTimestamp: Date | undefined;
26 | podCIDR: String | undefined;
27 | addresses: k8s.V1NodeAddress[] | undefined;
28 | allocatable: { [key: string]: string } | undefined;
29 | capacity: { [key: string]: string } | undefined;
30 | conditions: k8s.V1NodeCondition[] | undefined;
31 | labels: { [key: string]: string } | undefined;
32 | }
33 |
34 | interface ReturnedServices {
35 | name: String | undefined;
36 | namespace: String | undefined;
37 | labels: { [key: string]: string } | undefined;
38 | creationTimestamp: Date | undefined;
39 | clusterIP: String | undefined;
40 | ports: k8s.V1ServicePort[] | undefined;
41 | selector: { [key: string]: string } | undefined;
42 | type: String | undefined;
43 | }
44 |
45 | // ***** Controller Object *****
46 | const kubernetesController = {
47 | // Middleware function to get all pods from the cluster
48 | getPods: async (_req: Request, res: Response, next: NextFunction) => {
49 | try {
50 | const allPods = await kubernetesService.getPodsFromCluster();
51 | const returnedPods: ReturnedPod[] = [];
52 | for (const pod of allPods) {
53 | const newPod: ReturnedPod = {
54 | name: pod.metadata?.name || 'Unknown Pod Name',
55 | creationTimestamp: pod.metadata?.creationTimestamp || undefined,
56 | namespace: pod.metadata?.namespace || 'Unknown namespce',
57 | labels: pod.metadata?.labels || undefined,
58 | nodeName: pod.spec?.nodeName,
59 | restartPolicy: pod.spec?.restartPolicy || 'Unknown restart policy',
60 | hostIP: pod.status?.hostIP || 'Unknown host IP',
61 | podIP: pod.status?.podIP || 'Unknown pod IP',
62 | phase: pod.status?.phase || 'Unknown phase',
63 | conditions: pod.status?.conditions || undefined,
64 | startTime: pod.status?.startTime || undefined,
65 | uid: pod.metadata?.uid || undefined,
66 | };
67 | returnedPods.push(newPod);
68 | }
69 | res.locals.podData = returnedPods;
70 | next();
71 | } catch (error) {
72 | res.status(500).json({ message: 'Error fetching pods from cluster' });
73 | throw new Error(`Something went wrong: ${(error as Error).message}`);
74 | }
75 | },
76 |
77 | // Middleware function to get details on a single pod from the cluster
78 | getPodDetails: async (req: Request, res: Response, next: NextFunction) => {
79 | const { podName, namespace } = req.params;
80 | interface ReturnedPod {
81 | name: string;
82 | creationTimestamp: Date | undefined;
83 | namespace: String;
84 | labels: { [key: string]: string } | undefined;
85 | nodeName: String | undefined;
86 | restartPolicy: String;
87 | hostIP: String;
88 | podIP: String;
89 | phase: string | undefined;
90 | conditions: k8s.V1PodCondition[] | undefined;
91 | startTime: Date | undefined;
92 | }
93 | try {
94 | const pod = await kubernetesService.getPodDetailsFromCluster(
95 | podName,
96 | namespace
97 | );
98 | const newPod: ReturnedPod = {
99 | name: pod.metadata?.name || 'Unknown Pod Name',
100 | creationTimestamp: pod.metadata?.creationTimestamp || undefined,
101 | namespace: pod.metadata?.namespace || 'Unknown namespce',
102 | labels: pod.metadata?.labels || undefined,
103 | nodeName: pod.spec?.nodeName,
104 | restartPolicy: pod.spec?.restartPolicy || 'Unknown restart policy',
105 | hostIP: pod.status?.hostIP || 'Unknown host IP',
106 | podIP: pod.status?.podIP || 'Unknown pod IP',
107 | phase: pod.status?.phase || 'Unknown phase',
108 | conditions: pod.status?.conditions || undefined,
109 | startTime: pod.status?.startTime || undefined,
110 | };
111 | res.locals.pod = newPod;
112 | next();
113 | } catch (error) {
114 | throw new Error(
115 | `Error occurred while fetching pod data for pod: ${podName} in namespace: ${namespace}`
116 | );
117 | }
118 | },
119 |
120 | // Middleware function to get all nodes from the cluster
121 | getNodes: async (_req: Request, res: Response, next: NextFunction) => {
122 | try {
123 | const allNodes = await kubernetesService.getNodesFromCluster();
124 | const returnedNodes: ReturnedNode[] = [];
125 | for (const node of allNodes) {
126 | const newNode: ReturnedNode = {
127 | creationTimestamp: node.metadata?.creationTimestamp,
128 | name: node.metadata?.name,
129 | namespace: node.metadata?.namespace,
130 | labels: node.metadata?.labels,
131 | podCIDR: node.spec?.podCIDR,
132 | addresses: node.status?.addresses,
133 | allocatable: node.status?.allocatable,
134 | capacity: node.status?.capacity,
135 | conditions: node.status?.conditions,
136 | };
137 | returnedNodes.push(newNode);
138 | }
139 | res.locals.nodeData = returnedNodes;
140 | next();
141 | } catch (error) {
142 | res.status(500).json({ message: 'Error fetching services from cluster' });
143 | throw new Error(`Something went wrong: ${(error as Error).message}`);
144 | }
145 | },
146 |
147 | // Middleware function to get all services from the cluster
148 | getServices: async (_req: Request, res: Response, next: NextFunction) => {
149 | try {
150 | const allServices = await kubernetesService.getServicesFromCluster();
151 | const returnedServices: ReturnedServices[] = [];
152 |
153 | for (const services of allServices) {
154 | const newService: ReturnedServices = {
155 | name: services.metadata?.name,
156 | namespace: services.metadata?.namespace,
157 | labels: services.metadata?.labels,
158 | creationTimestamp: services.metadata?.creationTimestamp,
159 | clusterIP: services.spec?.clusterIP,
160 | ports: services.spec?.ports,
161 | selector: services.spec?.selector,
162 | type: services.spec?.type,
163 | };
164 | returnedServices.push(newService);
165 | }
166 | res.locals.serviceData = returnedServices;
167 | next();
168 | } catch (error) {
169 | res.status(500).json({ message: 'Error fetching nodes from cluster' });
170 | throw new Error(`Something went wrong: ${(error as Error).message}`);
171 | }
172 | },
173 |
174 | //middleware function to check if the user provided key and address are valid
175 | checkAPI: async (req: Request, res: Response, next: NextFunction) => {
176 | const key: string = req.body.key;
177 | const address: string = req.body.address;
178 | let cleanAddress: string = address;
179 | if (cleanAddress) {
180 | cleanAddress = address.replace(/https?:\/\//, '');
181 |
182 | try {
183 | const check = await kubernetesService.checkAPI(key, cleanAddress);
184 | if (check === 'ok') {
185 | generalService.writeEnv(key, cleanAddress);
186 | next();
187 | } else if (check === 'invalidkey') {
188 | res.status(403).json({ message: 'invalid_key' });
189 | } else {
190 | res.status(500).json({ message: 'unable to connect to cluster' });
191 | }
192 | } catch (error) {
193 | res.status(500).json({ message: 'error checking API ' });
194 | throw new Error(`Something went wrong: ${(error as Error).message}`);
195 | }
196 | } else {
197 | res.status(500).json({ message: 'no address given' });
198 | }
199 | },
200 | };
201 |
202 | // Exports the controller object for use as middleware
203 | export default kubernetesController;
204 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "main": "server.js",
6 | "scripts": {
7 | "server": "concurrently \"tsc --watch\" \"nodemon server.js\"",
8 | "build": "tsc",
9 | "watch": "tsc --watch",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "description": "",
15 | "dependencies": {
16 | "@kubernetes/client-node": "^0.21.0",
17 | "axios": "^1.7.4",
18 | "concurrently": "^8.2.2",
19 | "cors": "^2.8.5",
20 | "dotenv": "^16.4.5",
21 | "express": "^4.19.2",
22 | "node": "^22.6.0",
23 | "ts-node": "^10.9.2",
24 | "typescript": "^5.5.4",
25 | "vite": "^5.4.1"
26 | },
27 | "devDependencies": {
28 | "@types/cors": "^2.8.17",
29 | "@types/express": "^4.17.21",
30 | "nodemon": "^3.1.4"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/routes/kubernetesRouter.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import kubernetesController from '../controllers/kubernetesController.js';
3 | import generalController from '../controllers/generalController.js';
4 | const kubernetesRouter = Router();
5 | // Route to get all pods in the cluster
6 | kubernetesRouter.get('/pods', kubernetesController.getPods, (_req, res) => {
7 | res.status(200).json(res.locals.podData);
8 | });
9 | // Route to get all details of a specific pod
10 | kubernetesRouter.get('/pods/:namespace/:podName', kubernetesController.getPodDetails, (_req, res) => {
11 | res.status(200).json(res.locals.pod);
12 | });
13 | // Route to get all services in the cluster
14 | kubernetesRouter.get('/services', kubernetesController.getServices, (_req, res) => {
15 | res.status(200).json(res.locals.serviceData);
16 | });
17 | // Route to get all nodes in the cluster
18 | kubernetesRouter.get('/nodes', kubernetesController.getNodes, (_req, res) => {
19 | res.status(200).json(res.locals.nodeData);
20 | });
21 | //Route to check if the API information is correct
22 | kubernetesRouter.post('/checkAPI', kubernetesController.checkAPI, (_req, res) => {
23 | res.status(200).json({ message: 'ok' });
24 | });
25 | //Route to check if the environment file exists and if it has information
26 | kubernetesRouter.get('/checkENV', generalController.checkEnv, (_req, res) => {
27 | res.status(200).json(res.locals.env);
28 | });
29 | //Route to create logs
30 | kubernetesRouter.post('/createLogs', kubernetesController.getPods, generalController.writeLog, (_req, res) => {
31 | res.status(200).json(res.locals.logs);
32 | });
33 | //Route to get logs
34 | kubernetesRouter.get('/getLogs', generalController.getLogs, (_req, res) => {
35 | res.status(200).json(res.locals.dirLogs);
36 | });
37 | //Route to download a log
38 | kubernetesRouter.get('/getDownloadLogs/:log', generalController.getDownloadSpecificLog, (_req, res) => {
39 | res.status(200);
40 | });
41 | //Route to delete a log
42 | kubernetesRouter.delete('/deleteLogs/:log', generalController.deleteSpecificLog, (_req, res) => {
43 | res.status(200).json(res.locals.deletedLog);
44 | });
45 | export default kubernetesRouter;
46 |
--------------------------------------------------------------------------------
/server/routes/kubernetesRouter.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import kubernetesController from '../controllers/kubernetesController.js';
3 | import generalController from '../controllers/generalController.js';
4 | const kubernetesRouter = Router();
5 |
6 | // Route to get all pods in the cluster
7 | kubernetesRouter.get('/pods', kubernetesController.getPods, (_req, res) => {
8 | res.status(200).json(res.locals.podData);
9 | });
10 |
11 | // Route to get all details of a specific pod
12 | kubernetesRouter.get(
13 | '/pods/:namespace/:podName',
14 | kubernetesController.getPodDetails,
15 | (_req, res) => {
16 | res.status(200).json(res.locals.pod);
17 | }
18 | );
19 |
20 | // Route to get all services in the cluster
21 | kubernetesRouter.get(
22 | '/services',
23 | kubernetesController.getServices,
24 | (_req, res) => {
25 | res.status(200).json(res.locals.serviceData);
26 | }
27 | );
28 |
29 | // Route to get all nodes in the cluster
30 | kubernetesRouter.get('/nodes', kubernetesController.getNodes, (_req, res) => {
31 | res.status(200).json(res.locals.nodeData);
32 | });
33 | //Route to check if the API information is correct
34 | kubernetesRouter.post(
35 | '/checkAPI',
36 | kubernetesController.checkAPI,
37 | (_req, res) => {
38 | res.status(200).json({ message: 'ok' });
39 | }
40 | );
41 | //Route to check if the environment file exists and if it has information
42 | kubernetesRouter.get('/checkENV', generalController.checkEnv, (_req, res) => {
43 | res.status(200).json(res.locals.env);
44 | });
45 | //Route to create logs
46 | kubernetesRouter.post(
47 | '/createLogs',
48 | kubernetesController.getPods,
49 | generalController.writeLog,
50 | (_req, res) => {
51 | res.status(200).json(res.locals.logs);
52 | }
53 | );
54 | //Route to get logs
55 | kubernetesRouter.get('/getLogs', generalController.getLogs, (_req, res) => {
56 | res.status(200).json(res.locals.dirLogs);
57 | });
58 | //Route to download a log
59 | kubernetesRouter.get(
60 | '/getDownloadLogs/:log',
61 | generalController.getDownloadSpecificLog,
62 | (_req, res) => {
63 | res.status(200);
64 | }
65 | );
66 | //Route to delete a log
67 | kubernetesRouter.delete(
68 | '/deleteLogs/:log',
69 | generalController.deleteSpecificLog,
70 | (_req, res) => {
71 | res.status(200).json(res.locals.deletedLog);
72 | }
73 | );
74 | export default kubernetesRouter;
75 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import kubernetesRouter from './routes/kubernetesRouter.js';
3 | import cors from 'cors';
4 | const app = express();
5 | const PORT = 8080;
6 | app.use(express.json());
7 | app.use(cors());
8 | // Kubernetes API Router Handler
9 | app.use('/api', kubernetesRouter);
10 | // Standard 404 Route Handler
11 | app.use('/', (_req, res) => {
12 | res.status(404).send('Error page not found!');
13 | });
14 | // Express Global Error Handler
15 | app.use((err, _req, res, _next) => {
16 | console.log(err);
17 | res.status(500).json(err);
18 | });
19 | // Starts the app on the given port
20 | app.listen(PORT, () => {
21 | console.log(`Server is running on http://localhost:${PORT}`);
22 | });
23 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from 'express';
2 | import kubernetesRouter from './routes/kubernetesRouter.js';
3 | import cors from 'cors';
4 |
5 | const app = express();
6 | const PORT = 8080;
7 |
8 | app.use(express.json());
9 | app.use(cors());
10 |
11 | // Kubernetes API Router Handler
12 | app.use('/api', kubernetesRouter);
13 |
14 | // Standard 404 Route Handler
15 | app.use('/', (_req, res) => {
16 | res.status(404).send('Error page not found!');
17 | });
18 |
19 | // Express Global Error Handler
20 | app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
21 | console.log(err);
22 | res.status(500).json(err);
23 | });
24 |
25 | // Starts the app on the given port
26 | app.listen(PORT, () => {
27 | console.log(`Server is running on http://localhost:${PORT}`);
28 | });
29 |
--------------------------------------------------------------------------------
/server/services/generalService.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | const generalService = {
4 | // Function checks if the .env file exists
5 | checkEnv: () => {
6 | if (!process.env.KUBERNETES_SERVER || !process.env.KUBERNETES_TOKEN) {
7 | const envPath = path.resolve('./.env');
8 | if (!fs.existsSync(envPath)) {
9 | const defaultEnv = 'KUBERNETES_SERVER=\n' + 'KUBERNETES_TOKEN=';
10 | fs.writeFileSync(envPath, defaultEnv.trim());
11 | return 'init';
12 | }
13 | else {
14 | return 'noVar';
15 | }
16 | }
17 | else {
18 | return 'exist';
19 | }
20 | },
21 | //Function creates an .env file if it does not exist
22 | writeEnv: (key, address) => {
23 | const envPath = path.resolve('./.env');
24 | const fileEnv = 'KUBERNETES_SERVER=https://' + address + '\n' + 'KUBERNETES_TOKEN=' + key;
25 | fs.writeFileSync(envPath, fileEnv, 'utf-8');
26 | process.env.KUBERNETES_SERVER = 'https://' + address;
27 | process.env.KUBERNETES_TOKEN = key;
28 | },
29 | //Function checks if the logs folder exists
30 | checkLogs: () => {
31 | const logFolder = path.resolve('../logs/');
32 | fs.access(logFolder, (err) => {
33 | if (err) {
34 | fs.mkdir(logFolder, (err) => {
35 | if (err) {
36 | throw new Error(`Something went wrong: ${err.message}`);
37 | }
38 | });
39 | }
40 | });
41 | },
42 | //Function creates a new log in JSON
43 | writeLogs: (input) => {
44 | const time = new Date();
45 | const year = time.getFullYear();
46 | const month = time.getMonth() + 1;
47 | const day = time.getDate();
48 | const hours = time.getHours();
49 | const minutes = time.getMinutes();
50 | const seconds = time.getSeconds();
51 | const logFile = path.resolve(`../logs/log-${year}-${month}-${day}-${hours}-${minutes}-${seconds}.json`);
52 | if (!fs.existsSync(logFile)) {
53 | fs.writeFileSync(logFile, JSON.stringify(input, null, 2));
54 | }
55 | else {
56 | fs.writeFileSync(logFile, JSON.stringify(input, null, 2));
57 | }
58 | },
59 | //Function gets the logs in the logs directory
60 | getDirLogs: () => {
61 | const logDir = path.resolve('../logs/');
62 | const filesInDir = fs.readdirSync(logDir);
63 | return filesInDir;
64 | },
65 | };
66 | export default generalService;
67 |
--------------------------------------------------------------------------------
/server/services/generalService.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | interface data {
5 | name: string;
6 | namespace: string;
7 | logs: string;
8 | }
9 |
10 | const generalService = {
11 | // Function checks if the .env file exists
12 | checkEnv: (): string | undefined => {
13 | if (!process.env.KUBERNETES_SERVER || !process.env.KUBERNETES_TOKEN) {
14 | const envPath = path.resolve('./.env');
15 | if (!fs.existsSync(envPath)) {
16 | const defaultEnv = 'KUBERNETES_SERVER=\n' + 'KUBERNETES_TOKEN=';
17 | fs.writeFileSync(envPath, defaultEnv.trim());
18 | return 'init';
19 | } else {
20 | return 'noVar';
21 | }
22 | } else {
23 | return 'exist';
24 | }
25 | },
26 | //Function creates an .env file if it does not exist
27 | writeEnv: (key: string, address: string) => {
28 | const envPath = path.resolve('./.env');
29 | const fileEnv =
30 | 'KUBERNETES_SERVER=https://' + address + '\n' + 'KUBERNETES_TOKEN=' + key;
31 | fs.writeFileSync(envPath, fileEnv, 'utf-8');
32 | process.env.KUBERNETES_SERVER = 'https://' + address;
33 | process.env.KUBERNETES_TOKEN = key;
34 | },
35 | //Function checks if the logs folder exists
36 | checkLogs: () => {
37 | const logFolder = path.resolve('../logs/');
38 | fs.access(logFolder, (err) => {
39 | if (err) {
40 | fs.mkdir(logFolder, (err) => {
41 | if (err) {
42 | throw new Error(`Something went wrong: ${(err as Error).message}`);
43 | }
44 | });
45 | }
46 | });
47 | },
48 | //Function creates a new log in JSON
49 | writeLogs: (input: data[] | undefined) => {
50 | const time = new Date();
51 | const year = time.getFullYear();
52 | const month = time.getMonth() + 1;
53 | const day = time.getDate();
54 | const hours = time.getHours();
55 | const minutes = time.getMinutes();
56 | const seconds = time.getSeconds();
57 | const logFile = path.resolve(
58 | `../logs/log-${year}-${month}-${day}-${hours}-${minutes}-${seconds}.json`
59 | );
60 | if (!fs.existsSync(logFile)) {
61 | fs.writeFileSync(logFile, JSON.stringify(input, null, 2));
62 | } else {
63 | fs.writeFileSync(logFile, JSON.stringify(input, null, 2));
64 | }
65 | },
66 | //Function gets the logs in the logs directory
67 | getDirLogs: (): string[] => {
68 | const logDir: string = path.resolve('../logs/');
69 | const filesInDir: string[] = fs.readdirSync(logDir);
70 | return filesInDir;
71 | },
72 | };
73 |
74 | export default generalService;
75 |
--------------------------------------------------------------------------------
/server/services/kubernetesService.js:
--------------------------------------------------------------------------------
1 | // Lines 2 - 33 are basic kubernetes API setup
2 | import * as k8s from '@kubernetes/client-node';
3 | import dotenv from 'dotenv';
4 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
5 | dotenv.config();
6 | // Defines helper functions that will connect middleware to the Kubernetes API Client functions
7 | const kubernetesService = {
8 | createClient: () => {
9 | // Creates the config file that the server will be using to communicate with the cluster
10 | const kc = new k8s.KubeConfig();
11 | kc.loadFromOptions({
12 | clusters: [
13 | {
14 | name: 'main-cluster',
15 | server: `${process.env.KUBERNETES_SERVER}`,
16 | skipTLSVerify: true,
17 | },
18 | ],
19 | users: [
20 | {
21 | name: 'main-user',
22 | token: `${process.env.KUBERNETES_TOKEN}`,
23 | },
24 | ],
25 | contexts: [
26 | {
27 | name: 'main-context',
28 | cluster: 'main-cluster',
29 | user: 'main-user',
30 | },
31 | ],
32 | currentContext: 'main-context',
33 | });
34 | // Creates an instance of a Kubernetes API Client to interact with the Kubernetes API
35 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
36 | return k8sApi;
37 | },
38 | // Function that gets all pods from the cluster
39 | getPodsFromCluster: async () => {
40 | const k8sApi = kubernetesService.createClient();
41 | try {
42 | const res = await k8sApi.listPodForAllNamespaces();
43 | return res.body.items;
44 | }
45 | catch (error) {
46 | throw new Error(`Error fetching all pod details from the cluster.`);
47 | }
48 | },
49 | // Function that gets a specific pod's details from the cluster
50 | getPodDetailsFromCluster: async (podName, namespace) => {
51 | const k8sApi = kubernetesService.createClient();
52 | try {
53 | const res = await k8sApi.readNamespacedPod(podName, namespace);
54 | return res.body;
55 | }
56 | catch (error) {
57 | throw new Error(`Error fetching pod details from the cluster for pod name: ${podName} in namespace: ${namespace}.`);
58 | }
59 | },
60 | // Function that gets all services from the cluster
61 | getServicesFromCluster: async () => {
62 | const k8sApi = kubernetesService.createClient();
63 | try {
64 | const res = await k8sApi.listServiceForAllNamespaces();
65 | return res.body.items;
66 | }
67 | catch (error) {
68 | throw new Error(`Error fetching all service data from the cluster.`);
69 | }
70 | },
71 | // Function that gets all nodes from the cluster
72 | getNodesFromCluster: async () => {
73 | const k8sApi = kubernetesService.createClient();
74 | try {
75 | const res = await k8sApi.listNode();
76 | return res.body.items;
77 | }
78 | catch (error) {
79 | throw new Error(`Error fetching all node data from the cluster.`);
80 | }
81 | },
82 | // Function that checks if the API is reachable and the authorization works
83 | checkAPI: async (key, address) => {
84 | try {
85 | const test = await fetch('https://' + address + '/api/v1/nodes', {
86 | method: 'GET',
87 | headers: {
88 | authorization: 'Bearer ' + key,
89 | },
90 | });
91 | if (test.status !== 200) {
92 | return 'invalidkey';
93 | }
94 | else {
95 | return 'ok';
96 | }
97 | }
98 | catch (error) {
99 | if (error instanceof Error) {
100 | return error;
101 | }
102 | }
103 | },
104 | // Function gets the logs from the logs folder and formats them
105 | getLogs: async (input) => {
106 | const k8sApi = kubernetesService.createClient();
107 | try {
108 | const date = new Date();
109 | const formatter = new Intl.DateTimeFormat('en-US', {
110 | year: 'numeric',
111 | month: 'long',
112 | day: 'numeric',
113 | });
114 | const formattedDate = formatter.format(date);
115 | const logs = [];
116 | for (let i = 0; i < input.length; i++) {
117 | if (input[i].namespace !== 'kube-system' &&
118 | input[i].namespace !== 'monitoring') {
119 | const result = await k8sApi.readNamespacedPodLog(input[i].name, input[i].namespace);
120 | logs.push({
121 | name: input[i].name,
122 | namespace: input[i].namespace,
123 | logs: result.body,
124 | date: formattedDate,
125 | });
126 | }
127 | }
128 | return logs;
129 | }
130 | catch (error) {
131 | throw new Error(`Something went wrong: ${error.message}`);
132 | }
133 | },
134 | };
135 | // Exports service object for use as helper functions
136 | export default kubernetesService;
137 |
--------------------------------------------------------------------------------
/server/services/kubernetesService.ts:
--------------------------------------------------------------------------------
1 | // Lines 2 - 33 are basic kubernetes API setup
2 | import * as k8s from '@kubernetes/client-node';
3 | import dotenv from 'dotenv';
4 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
5 | dotenv.config();
6 |
7 | interface info {
8 | name: string;
9 | namespace: string;
10 | }
11 | interface data {
12 | name: string;
13 | namespace: string;
14 | logs: string;
15 | }
16 | // Defines helper functions that will connect middleware to the Kubernetes API Client functions
17 | const kubernetesService = {
18 | createClient: (): k8s.CoreV1Api => {
19 | // Creates the config file that the server will be using to communicate with the cluster
20 | const kc = new k8s.KubeConfig();
21 | kc.loadFromOptions({
22 | clusters: [
23 | {
24 | name: 'main-cluster',
25 | server: `${process.env.KUBERNETES_SERVER}`,
26 | skipTLSVerify: true,
27 | },
28 | ],
29 | users: [
30 | {
31 | name: 'main-user',
32 | token: `${process.env.KUBERNETES_TOKEN}`,
33 | },
34 | ],
35 | contexts: [
36 | {
37 | name: 'main-context',
38 | cluster: 'main-cluster',
39 | user: 'main-user',
40 | },
41 | ],
42 | currentContext: 'main-context',
43 | });
44 |
45 | // Creates an instance of a Kubernetes API Client to interact with the Kubernetes API
46 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
47 | return k8sApi;
48 | },
49 |
50 | // Function that gets all pods from the cluster
51 | getPodsFromCluster: async (): Promise => {
52 | const k8sApi = kubernetesService.createClient();
53 | try {
54 | const res = await k8sApi.listPodForAllNamespaces();
55 | return res.body.items;
56 | } catch (error) {
57 | throw new Error(`Error fetching all pod details from the cluster.`);
58 | }
59 | },
60 |
61 | // Function that gets a specific pod's details from the cluster
62 | getPodDetailsFromCluster: async (
63 | podName: string,
64 | namespace: string
65 | ): Promise => {
66 | const k8sApi = kubernetesService.createClient();
67 | try {
68 | const res = await k8sApi.readNamespacedPod(podName, namespace);
69 | return res.body;
70 | } catch (error) {
71 | throw new Error(
72 | `Error fetching pod details from the cluster for pod name: ${podName} in namespace: ${namespace}.`
73 | );
74 | }
75 | },
76 |
77 | // Function that gets all services from the cluster
78 | getServicesFromCluster: async (): Promise => {
79 | const k8sApi = kubernetesService.createClient();
80 | try {
81 | const res = await k8sApi.listServiceForAllNamespaces();
82 | return res.body.items;
83 | } catch (error) {
84 | throw new Error(`Error fetching all service data from the cluster.`);
85 | }
86 | },
87 |
88 | // Function that gets all nodes from the cluster
89 | getNodesFromCluster: async (): Promise => {
90 | const k8sApi = kubernetesService.createClient();
91 | try {
92 | const res = await k8sApi.listNode();
93 | return res.body.items;
94 | } catch (error) {
95 | throw new Error(`Error fetching all node data from the cluster.`);
96 | }
97 | },
98 | // Function that checks if the API is reachable and the authorization works
99 | checkAPI: async (
100 | key: string,
101 | address: string
102 | ): Promise => {
103 | try {
104 | const test = await fetch('https://' + address + '/api/v1/nodes', {
105 | method: 'GET',
106 | headers: {
107 | authorization: 'Bearer ' + key,
108 | },
109 | });
110 |
111 | if (test.status !== 200) {
112 | return 'invalidkey';
113 | } else {
114 | return 'ok';
115 | }
116 | } catch (error) {
117 | if (error instanceof Error) {
118 | return error;
119 | }
120 | }
121 | },
122 | // Function gets the logs from the logs folder and formats them
123 | getLogs: async (input: info[]): Promise => {
124 | const k8sApi = kubernetesService.createClient();
125 | try {
126 | const date = new Date();
127 | const formatter = new Intl.DateTimeFormat('en-US', {
128 | year: 'numeric',
129 | month: 'long',
130 | day: 'numeric',
131 | });
132 | const formattedDate = formatter.format(date);
133 | const logs: data[] = [];
134 | for (let i = 0; i < input.length; i++) {
135 | if (
136 | input[i].namespace !== 'kube-system' &&
137 | input[i].namespace !== 'monitoring'
138 | ) {
139 | const result = await k8sApi.readNamespacedPodLog(
140 | input[i].name,
141 | input[i].namespace
142 | );
143 | logs.push({
144 | name: input[i].name,
145 | namespace: input[i].namespace,
146 | logs: result.body,
147 | date: formattedDate,
148 | } as data);
149 | }
150 | }
151 |
152 | return logs;
153 | } catch (error) {
154 | throw new Error(`Something went wrong: ${(error as Error).message}`);
155 | }
156 | },
157 | };
158 |
159 | // Exports service object for use as helper functions
160 | export default kubernetesService;
161 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": false,
14 |
15 | /* Interop Constraints */
16 | "esModuleInterop": true, // added by Vince Aug 23
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": ["./"]
25 | }
26 |
--------------------------------------------------------------------------------
/server/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | export default defineConfig({
3 | server: {
4 | port: 8080, // Server runs on localhost:8080
5 | },
6 | });
7 |
--------------------------------------------------------------------------------
/server/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | server: {
5 | port: 8080, // Server runs on localhost:8080
6 | },
7 | });
8 |
--------------------------------------------------------------------------------