├── .gitignore
├── LICENSE
├── README.md
├── __mocks__
├── fileMock.js
└── styleMocks.js
├── __tests__
├── Events.test.tsx
├── playwright.e2e.ts
├── supertest.js
└── utils.test.ts
├── client
├── Types.ts
├── assets
│ ├── All-Services.png
│ ├── Forward-Success.png
│ ├── Home-to-Analysis.gif
│ ├── Landing-to-Home.gif
│ ├── analysis-icon.png
│ ├── close-menu-icon.png
│ ├── dashboard-icon.png
│ ├── favicon.icns
│ ├── favicon.ico
│ ├── logo-hat.png
│ ├── logo.png
│ ├── ns-icon.png
│ ├── open-menu-icon.png
│ └── stylesheets
│ │ ├── Sofia-Pro-Soft.otf
│ │ └── style.scss
├── components
│ ├── AnalysisPage.tsx
│ ├── App.tsx
│ ├── ChartGrid.tsx
│ ├── ClusterChart.tsx
│ ├── ClusterChartCard.tsx
│ ├── Events.tsx
│ ├── Graph.tsx
│ ├── HomePage.tsx
│ ├── LandingPage.tsx
│ ├── LogCard.tsx
│ ├── Modal.tsx
│ ├── Sidebar.tsx
│ └── Tooltip.tsx
├── global.d.ts
├── index.html
└── index.tsx
├── electron
├── main.ts
├── metricsData
│ └── formatMatrix.ts
├── preload.ts
└── utils.ts
├── jest.config.js
├── package.json
├── playwright.config.ts
├── tsconfig.json
├── webpack.config.js
└── webpack.production.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # PromQL queries
107 | QUERIES.TXT
108 |
109 | test.js
110 |
111 | package-lock.json
112 |
113 | # playwright testing reports
114 | __tests__/playwright_reports
115 |
116 | # do not include /build
117 | build
118 | # do not include .DS_STORE
119 | .DS_STORE
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # Palaemon :fried_shrimp: :fried_shrimp:
7 |
8 | - An Electron based developer tool for Kubernetes cluster monitoring and error analysis
9 | - Palaemon is a Greek, child sea-god who came to aid sailors in distress. He was often depicted as riding a dolphin. Also, a genus of [shrimp](https://en.wikipedia.org/wiki/Palaemon_(crustacean)).
10 |
11 | # Running the Electron App
12 | Prerequisites:
13 | - [ ] kubectl installed
14 | - [ ] Prometheus installed
15 | - [ ] Prometheus port-forwarded to `localhost:9090`
16 |
17 | ## Launching in dev mode with Hot-Module Reload (HMR)
18 | If this is the first time launching the app then run the following commands:
19 | ```
20 | npm install
21 | npm run build
22 | ```
23 | This will build your initial `dist` folder.
24 |
25 |
26 | Then on a different terminal run
27 | ```
28 | npm run electronmon
29 | ```
30 |
31 | The build command for webpack will run webpack with the --watch flag to watch for any changes in the files, and rebuild the dist folder when any files are changed. Electronmon is watching the dist folder for any changes and will either refresh or relaunch the electron app when it detects any of the dist folder files have been changed.
32 |
33 |
34 | ## After the initial build, you can now run the following command
35 | ```
36 | npm start
37 | ```
38 | This which will first delete the old `dist` folder from your app, and concurrently launch the webpack to build and electronmon to wait for the new `dist` folder to be built.
39 |
40 | If the build process was interrupted through `CTRL + C` or other means, then you may receive the following error:
41 | ```
42 | Error: ENOENT: no such file or directory, stat 'dist'
43 | ```
44 | In this case, just run the command below, and wait for webpack to finish building. After webpack has finished, you can start the app normally.
45 | ```
46 | npm run build
47 | ```
48 | ## Launching in production mode:
49 | The command below will build and bundle files into /dist folder for production and open the electron app based on the bundled files from /dist.
50 | ```
51 | npm start:production
52 | ```
53 |
54 | # Using Palaemon
55 | 1. On a successful startup, you will be greeted with a landing page allowing you to select a namespace to be analyzed.
56 | 2. After selecting your namespace, you will be moved to the home page
57 | - The home page is where you can find a list of all events, alerts, and OOMKills
58 | - Events are limited to occurences within the last hour due to a kubectl limitation.
59 | - OOMKill events are **not** limited to the last hour.
60 | 3. The lefthand side will show you all of your pods within the namespace.
61 | - The color of the squares represent the memory usage vs memory limit of the pod, with green pods using less memory than their requested amount.
62 | - Black pods indicate that no data was available for that pod.
63 | 4. When the OOMKill type is seleceted, an `Analyze` button will appear, where you can click that to be taken to the `Analysis` page.
64 | - Here is where you can find all of the information on your node and other pods at the time of death, along with pertinent events.
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ## Kubernetes Requirements
78 | Palaemon utilizes the kubernetes command line tool (kubectl) to gather data on your nodes and clusters. Kubectl can be downloaded and installed [here](https://kubernetes.io/docs/tasks/tools/).
79 |
80 | Minikube can also be used to test out Palaemon. More can be found on their official documentation [here](https://minikube.sigs.k8s.io/docs/start/).
81 |
82 | Palaemon has not yet been tested with Amazon Elastic Kubernetes Service (EKS) or Microsoft Azure Kubernetes Service (AKS), but should be compatible provided that kubectl can access all cluster and node information.
83 | ## How to Connect to Google Kubernetes Engine
84 |
85 | 1. Install gcloud CLI on your local machine from [here](https://cloud.google.com/sdk/docs/install)
86 | - If you have having problems with the gcloud CLI, try using the command below in your home directory, and make sure to update your $PATH in the process.
87 | ```
88 | curl https://sdk.cloud.google.com | bash
89 | ```
90 | - If you are still having issues, trying restarting your terminal.
91 | 2. Initialize the gcloud CLI following the steps [here](https://cloud.google.com/sdk/docs/initializing)
92 | 3. Connect your gcloud CLI to your GKE cluster [here](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl)
93 | - Make sure to get the credentials of your cluster (see below) or you will not be able to connect to your cluster
94 | ```
95 | gcloud container clusters get-credentials [CLUSTER_NAME]
96 | ```
97 |
98 | # Prometheus Requirements
99 | Palaemon utilizes Prometheus to scrape for data on your pods. Therefore, please ensure you have Prometheus installed on your node.
100 |
101 | Cloud hosting platforms such as GCP and AWS already have managed Prometheus services. If you are new to Prometheus, we recommend installing the Prometheus monitoring package, Prometheus Operator. This includes a full monitoring stack: Prometheus, Alert Manager, Node Exporter, Grafana, and Kube State Metrics. Prometheus Operator can be installed onto your cluster via Helm at the link [here](https://artifacthub.io/packages/helm/choerodon/prometheus-operator).
102 |
103 | Once you have Prometheus installed into your Kubernetes cluster, follow the steps below to port-forward your Prometheus operator service.
104 |
105 | ## Connecting Prometheus to Palaemon
106 |
107 | Make sure a Prometheus pod is installed onto your node/cluster, and forward its port (default 9090) to your localhost through the command below.
108 | ```
109 | kubectl port-forward -n [NAMESPACE] service/[PROMETHEUS] 9090
110 | ```
111 |
112 | - The -n flag indicates the namespace that the pod is assigned to.
113 | - A list of all available services can be found through typing the command below in the terminal.
114 | ```
115 | kubectl get services -A
116 | ```
117 | Find the service with a 9090/TCP Port assigned, and forward that service to your local 9090.
118 |
119 | 
120 |
121 | In the example above, the command would be :
122 | ```
123 | kubectl port-forward -n monitoring service/operator-kube-prometheus-s-prometheus 9090
124 | ```
125 |
126 | Once you have port-forwarded the service, you should now be able to access the Palaemon application. If you are still getting an error with port 9090 being closed, double-check that you are exposing the correct service: It should be listening on port 9090 (see picture below).
127 |
128 | - A more detailed set of instructions can be found on Google's Documentations [here](https://cloud.google.com/stackdriver/docs/managed-prometheus/query)
129 |
130 | 
131 |
132 | ### NOTE: While you *can* forward Prometheus to a local port that is **not** 9090, Palaemon is not yet setup to handle any other port besides 9090.
133 |
134 |
135 | ## How to Run Tests
136 | ### Unit and Integration tests using Jest
137 | The jest testing suite will start with the command below. The `--watch` flag is enabled, which allows for immediate retests upon save. The jest config in `jest.config.js` is set up to only look for and run test files within the `__test__` folder and with file names that include ".test." in them, such as "Events.test.tsx".
138 | ```
139 | npm run test:watch
140 | ```
141 | Units tests are set up using Jest testing suite and react-testing-library to test react components in the front end.
142 |
143 | ### End-to-End testing with Playwright Test Runner
144 | `npm run test:e2e` will execute the playwright test runner and run any test files in the `__test__` folder with the name format ".e2e." in them, such as "playwright.e2e.ts".
145 |
146 | There are settings to enable HTML report and video, snapshot, trace recordings that can be configured in the `playwright.config.ts` file is fo desired.
147 |
148 |
149 | ## Features
150 |
151 | * Realtime Pod memory usage, sorted by namespaces
152 | * Connect to locally hosted or cloud hosted Kubernetes clusters and Prometheus monitoring tools
153 | * Filter by namespaces for meaningful reports
154 | * Monitoring for health and resource usage per pod and nodes
155 | * Records and curates reports based on recent out-of-memory kill (OOMKill) errors
156 | * Displays event and alert logs
157 | * Visualize health of pods and nodes by namespace in a Kubernetes cluster
158 | * Execute customized queries based on user-defined time intervals
159 | * Documentation available on the official website
160 |
161 | ## Planned Features
162 |
163 | 1. Provide custom alerts for OOMKill events with specific termination reasons such as “Limit Overcommit” or “Container Limit Reached”
164 | 2. Allow for early, graceful termination of pods
165 | 3. Automatic reconfiguration of YAML files to adjust memory limits and requests
166 |
167 | ## Built With
168 |
169 |
170 | - [Electron](https://www.electronjs.org/)
171 | - [TypeScript](https://www.typescriptlang.org/)
172 | - [React](https://reactjs.org/)
173 | - [React Router](https://reactrouter.com/)
174 | - [Jest](https://jestjs.io/)
175 | - [Playwright](https://playwright.dev/)
176 | - [Node](https://nodejs.org/)
177 | - [Prometheus](https://prometheus.io/)
178 | - [Chart.js](https://www.chartjs.org/)
179 |
180 |
181 | ## The Team
182 | - Si Young Mah [Github](https://github.com/siyoungmah) [LinkedIn](https://www.linkedin.com/in/siyoungmah/)
183 | - Patrick Hu [Github](https://github.com/pathu91) [LinkedIn](https://www.linkedin.com/in/patrickhu91/)
184 | - Thang Thai [Github](https://github.com/thang-thai) [LinkedIn](https://www.linkedin.com/in/thang-thai/)
185 | - Raivyno Lenny Sutrisno [Github](https://github.com/FrozenStove) [LinkedIn](https://www.linkedin.com/in/raivyno-sutrisno/)
186 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
--------------------------------------------------------------------------------
/__mocks__/styleMocks.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
--------------------------------------------------------------------------------
/__tests__/Events.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3 | import '@testing-library/jest-dom/extend-expect';
4 |
5 | import Events from '../client/components/Events';
6 | import { EventProps, EventObject, oomObject } from '../client/Types';
7 |
8 | beforeAll(() => {
9 | window.api = {
10 | getNodes: jest.fn(),
11 | getDeployments: jest.fn(),
12 | getServices: jest.fn(),
13 | getPods: jest.fn(),
14 | getLogs: jest.fn(),
15 | getEvents: jest.fn().mockReturnValue(Promise.resolve(mockEvents)),
16 | getNamespaces: jest.fn(),
17 | getMemoryUsageByPods: jest.fn(),
18 | getAlerts: jest.fn().mockReturnValue(Promise.resolve(mockAlerts)),
19 | getAllInfo: jest.fn(),
20 | getOOMKills: jest.fn().mockReturnValue(Promise.resolve(mockOOMKills)),
21 | getUsage: jest.fn(),
22 | getAnalysis: jest.fn(),
23 | };
24 | });
25 |
26 | beforeEach(() => {
27 | render(
28 | {}}
30 | analyzedPod={[]}
31 | setAnalyzedData={() => {}}
32 | setShowGraphs={() => {}}
33 | clusterChartData={[]}
34 | />
35 | );
36 | });
37 |
38 | const mockEvents = [
39 | {
40 | namespace: 'mock_event_namespace',
41 | lastSeen: 'mock_event_lastseen',
42 | severity: 'mock_event_severity',
43 | reason: 'mock_event_reason',
44 | message: 'mock_event_message',
45 | object: 'mock_event_object',
46 | },
47 | ];
48 |
49 | const mockAlerts = [
50 | {
51 | group: 'mock_alerts_group',
52 | state: 'mock_alerts_state',
53 | name: 'mock_alerts_name',
54 | severity: 'mock_alerts_severity',
55 | description: 'mock_alerts_description',
56 | summary: 'mock_alerts_group',
57 | alerts: 'mock_alerts_alerts',
58 | },
59 | ];
60 |
61 | // a sample mocked oomObject
62 | const mockOomObject: oomObject = {
63 | namespace: 'mock_namespace',
64 | node: 'mock_node',
65 | podName: 'mock_podname',
66 | laststate: 'mock_state',
67 | restartcount: '4',
68 | reason: 'Fatal testing in jest mocks',
69 | exitcode: 'MOCK_EXITCODE',
70 | started: 'mock_begining',
71 | finished: 'mock_ending',
72 | ready: 'mock_ready',
73 | limits: {
74 | limitCpu: 'mock_cpu_limit',
75 | limitMemory: 'mock_memory_limit',
76 | },
77 | requests: {
78 | limitCpu: 'mock_cpu_request',
79 | limitMemory: 'mock_memory_request',
80 | },
81 | };
82 |
83 | const mockOOMKills = [mockOomObject];
84 |
85 | const setStateMock = jest.fn();
86 | const useStateMock: any = (useState: any) => [useState, setStateMock];
87 | jest.spyOn(React, 'useState').mockImplementation(useStateMock);
88 |
89 | describe(' ', () => {
90 | test('should render without crashing', () => {});
91 | test('should render necessary components', () => {});
92 | test('should render logs cards based on select options', () => {});
93 | test('should render loading icon only when fetching new data', () => {});
94 |
95 | describe('The log type selector', () => {
96 | test('should have Events as default log type upon render', () => {
97 | expect(
98 | screen.getByRole('combobox', { name: 'log-type' })
99 | ).toHaveDisplayValue('Events');
100 | expect(
101 | screen.getByRole('combobox', { name: 'log-type' })
102 | ).not.toHaveDisplayValue('Alerts');
103 | expect(
104 | screen.getByRole('combobox', { name: 'log-type' })
105 | ).not.toHaveDisplayValue('OOMKills');
106 | });
107 |
108 | test('should change displayed value based on user input', () => {
109 | // change the selection to 'alerts'
110 | fireEvent.change(screen.getByRole('combobox', { name: 'log-type' }), {
111 | target: { value: 'alerts' },
112 | });
113 |
114 | // the screen should correctly display the newly chosen option
115 | expect(
116 | screen.getByRole('combobox', { name: 'log-type' })
117 | ).toHaveDisplayValue('Alerts');
118 | expect(
119 | screen.getByRole('combobox', { name: 'log-type' })
120 | ).not.toHaveDisplayValue('Events');
121 | expect(
122 | screen.getByRole('combobox', { name: 'log-type' })
123 | ).not.toHaveDisplayValue('OOMKills');
124 |
125 | // change the selection to 'OOMKills'
126 | fireEvent.change(screen.getByRole('combobox', { name: 'log-type' }), {
127 | target: { value: 'oomkills' },
128 | });
129 |
130 | // the screen should correctly display the newly chosen option
131 | expect(
132 | screen.getByRole('combobox', { name: 'log-type' })
133 | ).toHaveDisplayValue('OOMKills');
134 | expect(
135 | screen.getByRole('combobox', { name: 'log-type' })
136 | ).not.toHaveDisplayValue('Events');
137 | expect(
138 | screen.getByRole('combobox', { name: 'log-type' })
139 | ).not.toHaveDisplayValue('Alerts');
140 | });
141 |
142 | test('should filter log card components when the select option has changed', () => {});
143 |
144 | test('should capitalize select option values', () => {});
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/__tests__/playwright.e2e.ts:
--------------------------------------------------------------------------------
1 | import { ElectronApplication, Page, _electron as electron } from 'playwright'
2 | import { expect, test } from '@playwright/test';
3 |
4 | let electronApp: ElectronApplication;
5 | let page: Page;
6 |
7 | // Test basic launching of the app
8 | test.describe('01. Launch app', async () => {
9 | test.beforeEach(async () => {
10 | // open the app
11 | electronApp = await electron.launch({ args: ['dist/electron/main.js'] });
12 | electronApp.on('window', async (page) => {
13 | const filename = page.url()?.split('/').pop();
14 | console.log(`Window opened: ${filename}`);
15 |
16 | // capture errors
17 | page.on('pageerror', (err) => console.log('page has errors: ', err));
18 |
19 | // capture console messages
20 | page.on('console', (log) => console.log(log.text()));
21 | });
22 | page = await electronApp.firstWindow();
23 | });
24 |
25 | test.afterEach(async () => {
26 | // close the app
27 | await electronApp.close();
28 | });
29 |
30 | test.describe('Rendering landing page:', () => {
31 | test('renders the first page', async () => {
32 | // page = await electronApp.firstWindow();
33 |
34 | await page.waitForSelector('h1');
35 | const text = await page.$eval('h1', (el) => el.textContent);
36 | expect(text).toBe('PALAEMON');
37 |
38 | // const title = await page.title();
39 | // expect(title).toBe('Palaemon');
40 | await expect(page).toHaveTitle('Palaemon');
41 | });
42 |
43 | test('sidebar for navigation exists', async () => {
44 | await page.waitForSelector('#sidebar');
45 | expect(await page.$('#sidebar')).toBeTruthy();
46 | });
47 |
48 | test('on app load, hash routing is initialized as an empty string', async () => {
49 | // HashRouter keeps track of routing by using #
50 | // when the app loads, the default hash is empty
51 | const hash = await page.evaluate(() => window.location.hash);
52 | expect(hash).toBe('');
53 | });
54 | });
55 |
56 | test.describe('Static routing and navigation around the app:', () => {
57 |
58 | test('clicking on hat logo routes back to landing page', async () => {
59 | await page.click('#sidebar #logo');
60 |
61 | expect(await page.$('#landing-container')).toBeTruthy();
62 | const hash = await page.evaluate(() => window.location.hash);
63 | expect(hash).toBe('#/');
64 | });
65 |
66 | test('clicking on logo text PALAEMON routes back to landing page', async () => {
67 | await page.click('#company-name');
68 |
69 | expect(await page.$('#landing-container')).toBeTruthy();
70 | const hash = await page.evaluate(() => window.location.hash);
71 | expect(hash).toBe('#/');
72 | });
73 |
74 | test('clicking on "Namespace" on sidebar routes back to landing page', async () => {
75 | await page.click('#link-namespace');
76 | await page.waitForSelector('#landing-container');
77 |
78 | expect(await page.$('#landing-container')).toBeTruthy();
79 | const hash = await page.evaluate(() => window.location.hash);
80 | expect(hash).toBe('#/');
81 | });
82 |
83 | test('clicking on "Dashboard" on sidebar routes to homepage', async () => {
84 | await page.click('#link-dashboard');
85 |
86 | // wait for react router to load
87 | await page.waitForSelector('#contents');
88 |
89 | const hash = await page.evaluate(() => window.location.hash);
90 | expect(hash).toBe('#/home');
91 |
92 | //landing container from previous page should no longer exist
93 | expect(await page.$('#landing-container')).toBeNull();
94 | expect(await page.$('#contents')).toBeTruthy();
95 | // form should no longer exist on the page
96 | });
97 |
98 | test('clicking on "Analysis" on sidebar routes to analysis page', async () => {
99 | await page.click('#link-analysis');
100 |
101 | // wait for react router to load
102 | await page.waitForSelector('#analysis-container');
103 |
104 | const hash = await page.evaluate(() => window.location.hash);
105 | expect(hash).toBe('#/analysis');
106 |
107 | //landing container from previous page should no longer exist
108 | expect(await page.$('#landing-container')).toBeNull();
109 | expect(await page.$('#analysis-container')).toBeTruthy();
110 | // form should no longer exist on the page
111 | });
112 | });
113 | });
114 |
115 | // Testing Namespace page
116 | test.describe('02. Namespace page ', async () => {
117 | test.beforeEach(async () => {
118 | // open the app
119 | electronApp = await electron.launch({ args: ['dist/electron/main.js'] });
120 | electronApp.on('window', async (page) => {
121 | const filename = page.url()?.split('/').pop();
122 | console.log(`Window opened: ${filename}`);
123 |
124 | // capture errors
125 | page.on('pageerror', (err) => console.log('page has errors: ', err));
126 |
127 | // capture console messages
128 | page.on('console', (log) => console.log(log.text()));
129 | });
130 | page = await electronApp.firstWindow();
131 | });
132 |
133 | test.afterEach(async () => {
134 | // close the app
135 | await electronApp.close();
136 | });
137 |
138 | test('Select menu should be populated based on available namespaces:', async () => {
139 |
140 | });
141 | });
142 |
143 |
--------------------------------------------------------------------------------
/__tests__/supertest.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 |
3 | const server = "http://localhost:9090";
4 |
--------------------------------------------------------------------------------
/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | const globalAny:any = global;
2 |
3 | beforeAll(() => { });
4 | beforeEach(() => { });
5 | afterAll(() => { });
6 | afterEach(() => { });
7 |
8 | // import { enableFetchMocks } from 'jest-fetch-mock'
9 | // enableFetchMocks()
10 |
11 | // import fetchMock from "jest-fetch-mock"
12 |
13 | // import fetch from 'node-fetch';
14 |
15 | import { SvgInfoObj } from '../client/Types';
16 | import { setStartAndEndTime, fetchMem } from '../electron/utils'
17 |
18 | // jest.mock('node-fetch');
19 |
20 | // let mockedFetch: jest.Mocked;
21 |
22 |
23 | describe('setStartAnEndTime', () => {
24 | test('returns and object with keys startTime and endTime', () => {
25 | const result = setStartAndEndTime();
26 | expect(typeof result).toBe('object');
27 | expect(result).toHaveProperty('startTime');
28 | expect(result).toHaveProperty('endTime');
29 | });
30 |
31 | test('sets startTime to an hour before now, and endTime to now', () => {
32 | const now = new Date();
33 | const endTime = now.toISOString();
34 | now.setHours(now.getHours() - 24);
35 | const startTime = now.toISOString();
36 | const result = setStartAndEndTime();
37 |
38 | expect(result.startTime).toBe(startTime);
39 | expect(result.endTime).toBe(endTime);
40 | });
41 |
42 | test('Lenny is testing here', () => {
43 | expect('Lenny').toBe('Lenny');
44 | });
45 |
46 | });
47 |
48 | describe('fetchMem should return the memory usage of a pod', () => {
49 | // beforeAll(() => {
50 | const mockObj = {
51 | metadata: {
52 | annotations: { 'cluster-autoscaler.kubernetes.io/safe-to-evict': 'true' },
53 | creationTimestamp: '2022 - 09 - 08T17: 08: 33.000Z',
54 | generateName: 'mockPodname',
55 | labels: {
56 | app: 'prometheus-node-exporter',
57 | chart: 'prometheus-node-exporter-3.3.1',
58 | 'controller-revision-hash': '7987bcc957',
59 | heritage: 'Helm',
60 | jobLabel: 'node-exporter',
61 | 'pod-template-generation': '1',
62 | release: 'stack'
63 | },
64 | name: 'mockPodname',
65 | namespace: 'mockNamepsace',
66 | },
67 | spec: {
68 | nodeName: 'mockNodeName'
69 | },
70 | }
71 | // limitData.data.result[0].metric.resource
72 | const mockRequest = {
73 | status: 'success',
74 | data: {
75 | resultType: 'matrix',
76 | result: [ // we use this line
77 | {
78 | metric: {
79 | __name__: 'kube_pod_container_resource_requests',
80 | container: 'alertmanager',
81 | endpoint: 'http',
82 | instance: '172.17.0.2:8080',
83 | job: 'kube-state-metrics',
84 | namespace: 'default',
85 | node: 'minikube',
86 | pod: 'alertmanager-stack-kube-prometheus-stac-alertmanager-0',
87 | resource: 'request', // we use this line
88 | service: 'stack-kube-state-metrics',
89 | uid: '863f4c04-580e-4b7f-923b-f1dd52fd1c07',
90 | unit: 'byte'
91 | },
92 | values: [[1663624864.753, '52428800']] // this string of numbers is what we need
93 | }
94 | ]
95 | },
96 | }
97 | const mockLimit = {
98 | status: 'success',
99 | data: {
100 | resultType: 'matrix',
101 | result: [ // we use this line
102 | {
103 | metric: {
104 | __name__: 'kube_pod_container_resource_requests',
105 | container: 'alertmanager',
106 | endpoint: 'http',
107 | instance: '172.17.0.2:8080',
108 | job: 'kube-state-metrics',
109 | namespace: 'default',
110 | node: 'minikube',
111 | pod: 'alertmanager-stack-kube-prometheus-stac-alertmanager-0',
112 | resource: 'memory', // we use this line
113 | service: 'stack-kube-state-metrics',
114 | uid: '863f4c04-580e-4b7f-923b-f1dd52fd1c07',
115 | unit: 'byte'
116 | },
117 | values: [[1663624864.753, '102428800']] // this string of numbers is what we need
118 | }
119 | ]
120 | },
121 | };
122 |
123 |
124 | const jsonLimObj = {
125 | json: jest.fn().mockReturnValue(Promise.resolve(mockLimit))
126 | }
127 | const jsonReqObj = {
128 | json: jest.fn().mockReturnValue(Promise.resolve(mockRequest))
129 | }
130 |
131 | let returnValue: any;
132 | let returnValueOnce: any;
133 |
134 |
135 |
136 |
137 | // jest.mock('node-fetch', ()=>jest.fn().mockReturnValue(Promise.resolve(jsonReqObj)).mockReturnValueOnce(Promise.resolve(jsonLimObj)))
138 |
139 | afterEach(() => {
140 | // console.log('afterEach')
141 | jest.resetModules()
142 | })
143 |
144 | beforeEach(() => {
145 | // console.log('beforeEach')
146 | jest.mock('node-fetch', ()=>jest.fn()
147 | .mockReturnValue(Promise.resolve(returnValue))
148 | .mockReturnValueOnce(Promise.resolve(returnValueOnce))
149 | )
150 | })
151 |
152 | test('It should return a default object with port 9090 closed', async () => {
153 |
154 | const defaultData = {
155 | name: '',
156 | usage: 1,
157 | resource: '',
158 | limit: 1,
159 | request: 1,
160 | parent: '',
161 | namespace: '',
162 | }
163 |
164 | const pod1: SvgInfoObj = await fetchMem(mockObj)
165 | // console.log('mypod9090', pod1)
166 | expect(pod1).toStrictEqual(defaultData)
167 | })
168 |
169 |
170 | test('It should accept an object with the correct information, or return an error', async () => {
171 | // THIS TEST NEEDS TO BE FINISHED ON THE UTILS.TS SIDE, IT IS SET TO FAIL IF UNCOMMENTED
172 |
173 | const defaultData = {
174 | name: '',
175 | usage: 1,
176 | resource: '',
177 | limit: 1,
178 | request: 1,
179 | parent: '',
180 | namespace: '',
181 | }
182 |
183 | // const pod1: SvgInfoObj = await fetchMem('string')
184 | // console.log('mypod9090', pod1)
185 | // expect(pod1).toStrictEqual(defaultData)
186 | })
187 |
188 | test('It returns memory usage and data about pods', async () => {
189 |
190 | returnValue = jsonReqObj;
191 | returnValueOnce = jsonLimObj;
192 |
193 | const pod: SvgInfoObj = await fetchMem(mockObj)
194 |
195 | // console.log('mypodTrue', pod)
196 | expect(pod).toHaveProperty('name');
197 | expect(pod.request).toBe(52.4288);
198 | expect(pod.limit).toBe(102.4288);
199 | expect(pod).toBeInstanceOf(SvgInfoObj);
200 | })
201 | // test('', () => {
202 |
203 | // })
204 |
205 | })
206 |
207 | // test other functions that live in then
--------------------------------------------------------------------------------
/client/Types.ts:
--------------------------------------------------------------------------------
1 | //--------------------------------Types for Home Page----------------------------------------------------
2 |
3 | export type SvgInfo = {
4 | // for properties that dont exist in pod, node, cluster or deployment give it a 0 for num or '' for string
5 | name: string; // name of pod, node, or cluster, or deployment
6 | usage: number; // pods: memory used in bytes -- node: "memory requested" field from k8 (kibibytes)
7 | resource: string; // default should be memory. if a pod tracks cpu it will overwrite
8 | unit?: string; // either megabytes or milicores
9 | request: number; // pods: request memory in bytes -- node: 0 (node does not have same type of "request" memory as pods)
10 | limit: number; // pods: limit memory in bytes -- node: "memory allocatable" field from k8 (kibibytes)
11 | parent: string; // pod: node name -- node: cluster name
12 | namespace: string; // the namespace of a pod, or n/a for node
13 | };
14 |
15 | export class SvgInfoObj implements SvgInfo {
16 | constructor() {
17 | // set default values for each prop
18 | // number defaults are set to 1 (instead of) to avoid divide by 0 issues
19 | this.name = '';
20 | this.usage = 1;
21 | this.resource = 'memory';
22 | this.request = 1;
23 | this.limit = 1;
24 | this.parent = '';
25 | this.namespace = '';
26 | }
27 |
28 | name: string; // name of pod, node, or cluster, or deployment
29 | usage: number; // pods: memory used in bytes -- node: "memory requested" field from k8 (kibibytes)
30 | resource: string; // default should be memory. if a pod tracks cpu it will overwrite
31 | unit?: string; // either megabytes or milicores
32 | request: number; // pods: request memory in bytes -- node: 0 (node does not have same type of "request" memory as pods)
33 | limit: number; // pods: limit memory in bytes -- node: "memory allocatable" field from k8 (kibibytes)
34 | parent: string; // pod: node name -- node: cluster name
35 | namespace: string; // the namespace of a pod, or n/a for node
36 | }
37 |
38 | export type ClusterAllInfo = {
39 | Nodes: SvgInfo[];
40 | Pods: SvgInfo[];
41 | };
42 |
43 | export interface ModalProps extends SvgInfo {
44 | position: { left: string; top: string };
45 | close: () => void;
46 | }
47 | //--------------------------------Types for Cluster Chart----------------------------------------------------
48 |
49 | export interface ClusterChartProps extends ClusterAllInfo {
50 | close: () => void;
51 | click: (e: any, input: SvgInfo) => void;
52 | }
53 |
54 | export type ClusterChartCardProps = {
55 | title: string; // Cluster, or Pod, or Node, or Deployment
56 | data: SvgInfo[];
57 | click: (e: any, input: SvgInfo) => void;
58 | close: () => void;
59 | };
60 |
61 | //--------------------------------Types for the right side and alerts/events ----------------------------------------------------
62 |
63 | export type EventProps = {
64 | setAnalyzedPod: (input: any) => void;
65 | analyzedPod: any[];
66 | clusterChartData: any[];
67 | setAnalyzedData: (input: any) => void;
68 | setShowGraphs: (input: any) => void;
69 | };
70 |
71 | export type AnalyzeCount = any[];
72 |
73 | export interface LogCardProps {
74 | eventObj?: EventObject;
75 | alertObj?: AlertObject;
76 | oomObj?: oomObject;
77 | logType: string;
78 | analyzedPod: oomObject;
79 | setAnalyzedPod: (input: any) => void;
80 | clusterChartData: any;
81 | setAnalyzedData: (input: any) => void;
82 | setShowGraphs: (input: any) => void;
83 | setLoading?: (input: any) => void;
84 | }
85 |
86 | export type EventObject = {
87 | namespace: string;
88 | lastSeen: string;
89 | severity: string;
90 | reason: string;
91 | message: string;
92 | object: string;
93 | };
94 |
95 | export type AlertObject = {
96 | group: string;
97 | state: string;
98 | name: string;
99 | severity: string;
100 | description: string;
101 | summary: string;
102 | alerts: string;
103 | };
104 |
105 | export type oomObject = {
106 | namespace: string;
107 | node: string;
108 | podName: string;
109 | laststate: string;
110 | restartcount: string;
111 | reason: string;
112 | exitcode: string;
113 | started: string;
114 | finished: string;
115 | ready: string;
116 | limits: LimOrReq;
117 | requests: LimOrReq;
118 | };
119 |
120 | export type LimOrReq = {
121 | limitCpu: string;
122 | limitMemory: string;
123 | };
124 |
125 | //--------------------------------Types for Graphs----------------------------------------------------
126 | export type GraphData = {
127 | [podName: string]: {
128 | times: string[];
129 | values: number[];
130 | };
131 | }[];
132 |
133 | export type ChartGraphData = {
134 | nodeMem: GraphData;
135 | nodeCPU: GraphData;
136 | podMem: GraphData;
137 | podCPU: GraphData;
138 | netRead: GraphData;
139 | netWrite: GraphData;
140 | };
141 |
142 | export type GraphableData = {
143 | label: string;
144 | backgroundColor: string;
145 | borderColor: string;
146 | data: number[];
147 | };
148 |
149 | //--------------------------------Types for Analysis Page----------------------------------------------------
150 |
151 | export type AnalysisPage = {
152 | analyzedPod: any[];
153 | analyzedData: any[];
154 | setAnalyzedPod: (input: any) => void;
155 | setAnalyzedData: (input: any) => void;
156 | showGraphs: boolean;
157 | setShowGraphs: (input: boolean) => void;
158 | };
159 |
160 | export type AnalysisPageProps = {
161 | analyzedPod: any[];
162 | analyzedData: any[];
163 | setAnalyzedPod: (input: any) => void;
164 | clusterChartData: any;
165 | setAnalyzedData: (input: any) => void;
166 | showGraphs: boolean;
167 | setShowGraphs: (input: boolean) => void;
168 | };
169 |
170 | export type TooltipProps = {
171 | position: { left: string; top: string };
172 | };
173 |
174 | export type SidebarProps = {
175 | menuOpen: string;
176 | setMenuOpen: (input: boolean) => void;
177 | };
--------------------------------------------------------------------------------
/client/assets/All-Services.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/All-Services.png
--------------------------------------------------------------------------------
/client/assets/Forward-Success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/Forward-Success.png
--------------------------------------------------------------------------------
/client/assets/Home-to-Analysis.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/Home-to-Analysis.gif
--------------------------------------------------------------------------------
/client/assets/Landing-to-Home.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/Landing-to-Home.gif
--------------------------------------------------------------------------------
/client/assets/analysis-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/analysis-icon.png
--------------------------------------------------------------------------------
/client/assets/close-menu-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/close-menu-icon.png
--------------------------------------------------------------------------------
/client/assets/dashboard-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/dashboard-icon.png
--------------------------------------------------------------------------------
/client/assets/favicon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/favicon.icns
--------------------------------------------------------------------------------
/client/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/favicon.ico
--------------------------------------------------------------------------------
/client/assets/logo-hat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/logo-hat.png
--------------------------------------------------------------------------------
/client/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/logo.png
--------------------------------------------------------------------------------
/client/assets/ns-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/ns-icon.png
--------------------------------------------------------------------------------
/client/assets/open-menu-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/open-menu-icon.png
--------------------------------------------------------------------------------
/client/assets/stylesheets/Sofia-Pro-Soft.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Palaemon/5786859f3f543a3303f379a8dad0ca981f48c885/client/assets/stylesheets/Sofia-Pro-Soft.otf
--------------------------------------------------------------------------------
/client/assets/stylesheets/style.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Sofia Pro Soft';
3 | src: url('./Sofia-Pro-Soft.otf') format('opentype');
4 | }
5 |
6 | $ugly-sewage-green: #00695c;
7 | $p-light-color: #439889;
8 | $p-dark-color: #003d33;
9 |
10 | $font-color: #002a25;
11 |
12 | $secondary-color: #b2dfdb;
13 | $tertiary-color: #d1ece9;
14 | $s-light-color: #e5ffff;
15 | $s-dark-color: #82ada9;
16 |
17 | $sidebar-width: 240px;
18 | $sidebar-width-closed: 80px;
19 |
20 | $header-height: 56px;
21 |
22 | $grey: #ddd;
23 |
24 | html {
25 | height: 100vh;
26 | // prevents the window from over-scrolling and bouncing beyond the 100% screen size
27 | overflow: hidden;
28 | }
29 |
30 | body,
31 | #root,
32 | #sidebar,
33 | #contents,
34 | #landing-container,
35 | #analysis-container {
36 | height: 100%;
37 | }
38 |
39 | body {
40 | margin: 0px;
41 | padding: 0px;
42 | box-sizing: border-box;
43 | }
44 |
45 | * {
46 | // border: 1px black solid;
47 | font-family: Arial, Helvetica, sans-serif;
48 | }
49 |
50 | #logo {
51 | width: 180px;
52 | margin: 8px;
53 | transition: all 0.3s;
54 | }
55 |
56 | #logo-menu-closed {
57 | width: 60px;
58 | margin: 8px;
59 | }
60 |
61 | #root {
62 | height: 100vh;
63 | padding: 0px;
64 | display: flex;
65 | }
66 |
67 | #sidebar {
68 | width: $sidebar-width;
69 | background-color: #004941;
70 | display: flex;
71 | flex-direction: column;
72 | align-items: center;
73 | position: relative;
74 | transition: all 0.3s;
75 | padding: 0.5rem;
76 |
77 | a {
78 | & > img {
79 | margin-top: 50px;
80 | }
81 |
82 | // logo-hat load as "focus-visible" with a yellow outline on app load
83 | // this is to disable that :(
84 | &:focus-visible {
85 | outline: none !important;
86 | }
87 | }
88 |
89 | ul {
90 | list-style-type: none;
91 | padding: 0;
92 |
93 | li {
94 | &,
95 | * {
96 | text-decoration: none;
97 | margin: 0.6rem 0;
98 | font-size: 1.2rem;
99 | color: whitesmoke;
100 | }
101 | }
102 | }
103 |
104 | #close-menu-icon,
105 | #open-menu-icon {
106 | width: 1.2rem;
107 | position: absolute;
108 | top: 0.4rem;
109 | right: 0.4rem;
110 | cursor: pointer;
111 | }
112 |
113 | .sidebar-page-container {
114 | display: flex;
115 | align-items: center;
116 | gap: 1rem;
117 | transition: all 0.3s;
118 |
119 | .sidebar-icon {
120 | width: 2.4rem;
121 | color: white;
122 | transition: all 0.3s;
123 | }
124 |
125 | .sidebar-icon-closed {
126 | @extend .sidebar-icon;
127 | width: 2rem;
128 | transition: all 0.3s;
129 | }
130 |
131 | .sidebar-names {
132 | transition: all 1s;
133 | }
134 | }
135 | }
136 |
137 | #sidebar-closed {
138 | @extend #sidebar;
139 | width: $sidebar-width-closed;
140 | transition: all 0.3s;
141 | }
142 |
143 | #page {
144 | flex-grow: 1;
145 | display: flex;
146 | flex-direction: column;
147 | background-color: $secondary-color;
148 | height: 100vh;
149 | }
150 |
151 | #header {
152 | display: flex;
153 | justify-content: flex-end;
154 | background-color: $ugly-sewage-green;
155 |
156 | img {
157 | display: block;
158 | }
159 |
160 | a {
161 | text-decoration: none;
162 | }
163 |
164 | h1 {
165 | display: block;
166 | height: max-content;
167 | color: whitesmoke;
168 | padding-right: 12px;
169 | margin: 12px;
170 | font-size: 1.7rem;
171 | font-family: "Sofia Pro Soft";
172 | }
173 | }
174 |
175 | #contents {
176 | flex-grow: 1;
177 | display: flex;
178 | justify-content: space-between;
179 | background-color: #b2dfdb;
180 | // transition: all 0.3s;
181 |
182 | & #left-side,
183 | #right-side {
184 | height: 100%;
185 | width: calc((100vw - $sidebar-width) / 2);
186 | transition: all 0.3s;
187 | }
188 |
189 | & #left-side-closed,
190 | #right-side-closed {
191 | height: 100%;
192 | width: calc((100vw - $sidebar-width-closed) / 2);
193 | transition: all 0.3s;
194 | }
195 | }
196 |
197 | #left-side {
198 | display: flex;
199 | flex-direction: column;
200 | justify-content: flex-start;
201 | }
202 |
203 | #tagline {
204 | display: block;
205 | margin: 0;
206 | text-align: right;
207 | background-color: $ugly-sewage-green;
208 | font-weight: 100;
209 | font-size: 1rem;
210 | font-style: italic;
211 | color: whitesmoke;
212 | padding-right: 12px;
213 | }
214 |
215 | footer {
216 | font-size: 0.75rem;
217 | text-align: center;
218 | color: $ugly-sewage-green;
219 | padding: 4px 0;
220 | }
221 |
222 | // left side styling
223 | .cluster-chart-card {
224 | background-color: white;
225 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
226 | border-radius: 10px;
227 | margin: 1rem 2rem;
228 | padding: 0.6rem;
229 | display: flex;
230 | align-items: center;
231 | flex-wrap: wrap;
232 | font-weight: bold;
233 | color: $font-color;
234 |
235 | .cluster-chart-names {
236 | display: inline-block;
237 | width: 100px;
238 | }
239 |
240 | svg {
241 | margin: 0px 5px;
242 | // border: black 2px solid;
243 | }
244 |
245 | svg:hover {
246 | filter: drop-shadow(2px 2px 3px $font-color);
247 | }
248 |
249 | .health-bar {
250 | cursor: pointer;
251 | }
252 | }
253 |
254 | .cluster-btns {
255 | display: flex;
256 | gap: 0.6rem;
257 | margin: 1rem 2rem;
258 |
259 | .select-mem,
260 | .select-cpu {
261 | font-size: 0.7rem;
262 | border-radius: 10px;
263 | border: none;
264 | background-color: $ugly-sewage-green;
265 | color: white;
266 | min-width: 2.4rem;
267 | padding: 0.4rem 0.8rem;
268 | font-weight: 500;
269 | transition: all 0.2s;
270 | }
271 |
272 | .select-mem {
273 | top: 0.4rem;
274 | left: 4.3rem;
275 | }
276 |
277 | .select-cpu {
278 | top: 1.8rem;
279 | left: 4.3rem;
280 | }
281 |
282 | .select-mem:hover,
283 | .select-cpu:hover {
284 | background-color: white;
285 | color: $font-color;
286 | cursor: pointer;
287 | filter: drop-shadow(2px 2px 3px $font-color);
288 | }
289 | }
290 |
291 | .modal {
292 | animation-name: opac;
293 | animation-fill-mode: both;
294 | animation-duration: 0.5s;
295 |
296 | position: absolute;
297 | color: white;
298 | background-color: $ugly-sewage-green;
299 |
300 | box-shadow: 10px 10px 10px 6px rgb(64 0 0 / 20%);
301 | margin: 16px 20px 16px 20px;
302 | border-radius: 10px;
303 | padding: 0.4rem 1rem;
304 | z-index: 999;
305 |
306 | .modalClose {
307 | position: relative;
308 | display: flex;
309 | justify-content: flex-end;
310 |
311 | #modalCloseBtn {
312 | cursor: pointer;
313 | }
314 | }
315 |
316 | .modalList {
317 | list-style: none;
318 | padding-left: 0;
319 | }
320 | }
321 |
322 | @keyframes opac {
323 | from {
324 | opacity: 0;
325 | }
326 |
327 | to {
328 | opacity: 1;
329 | }
330 | }
331 |
332 | // Right side event styling
333 | #container-event {
334 | display: flex;
335 | flex-direction: column;
336 | height: 100%;
337 |
338 | // background-color: $tertiary-color;
339 |
340 | // & > * {
341 | // padding: 0.8rem;
342 | // margin: 0.5rem;
343 | // }
344 | }
345 |
346 | // Right side of event styling
347 | #container-event-logs {
348 | flex-grow: 1;
349 | overflow-y: auto;
350 | overflow-x: hidden;
351 | // I thiiink flex-grow maxes out the height, but a fixed height is needed to make
352 | // overflow-y scrolling feature working. I don't know why.
353 | height: 100px;
354 | color: $font-color;
355 | background-color: whitesmoke;
356 | // border: 4px solid $s-dark-color;
357 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
358 | margin: 0rem 2rem 1rem 1rem;
359 | border-radius: 10px;
360 | padding: 1rem;
361 |
362 | .event-card {
363 | position: relative;
364 | margin-bottom: 1rem;
365 | padding: 1rem 1.5rem;
366 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
367 | background-color: white;
368 | border-radius: 10px;
369 | word-wrap: break-word;
370 |
371 | &-header {
372 | display: flex;
373 | justify-content: space-between;
374 | }
375 |
376 | p {
377 | margin: 0.5rem 0;
378 | }
379 | }
380 |
381 | .no-logs-msg {
382 | font-size: 1rem;
383 | text-align: center;
384 | background-color: $grey;
385 | border-radius: 10px;
386 | padding: 1rem;
387 | color: $font-color;
388 | }
389 | }
390 |
391 | #container-select {
392 | display: flex;
393 | justify-content: flex-start;
394 | align-items: center;
395 | gap: 24px;
396 | height: 50px;
397 | margin-top: 0.4rem;
398 | margin-left: 1rem;
399 |
400 | & > select {
401 | width: 140px;
402 | font-size: 1.1rem;
403 | padding: 0.25rem 0.5rem;
404 | cursor: pointer;
405 | border-radius: 10px;
406 | border: none;
407 | }
408 | }
409 |
410 | .analyze {
411 | position: absolute;
412 | top: 10px;
413 | right: 10px;
414 | box-shadow: 0px 2px 3px rgb(64 0 0 / 20%);
415 | border-radius: 10px;
416 | border: none;
417 | padding: 0.3rem 0.6rem;
418 | font-size: 0.8rem;
419 | background-color: $ugly-sewage-green;
420 | color: white;
421 | transition: all 0.2s;
422 | text-transform: uppercase;
423 | }
424 |
425 | .analyze:hover {
426 | background-color: white;
427 | color: $font-color;
428 | cursor: pointer;
429 | }
430 |
431 | #graph {
432 | color: $font-color;
433 | background-color: white;
434 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
435 | border-radius: 10px;
436 | display: flex;
437 | flex-direction: column;
438 | gap: 1rem;
439 | margin: 2rem 2rem;
440 | padding: 0.5rem;
441 |
442 | .legend-btn-main {
443 | background-color: $ugly-sewage-green;
444 | color: white;
445 | box-shadow: 2px 2px 6px rgb(64 0 0 / 20%);
446 | border-radius: 10px;
447 | border: none;
448 | padding: 0.4rem 0.8rem;
449 | border-radius: 10px;
450 | width: fit-content;
451 | align-self: center;
452 | transition: all 0.2s;
453 | }
454 |
455 | .legend-btn-main:hover {
456 | background-color: white;
457 | color: $font-color;
458 | cursor: pointer;
459 | border: none;
460 | // filter: drop-shadow(2px 2px 3px $font-color);
461 | }
462 | }
463 |
464 | .loader {
465 | border: 6px solid $s-dark-color;
466 | /* Light grey */
467 | border-top: 6px solid $p-dark-color;
468 | /*#3498db; /* Blue */
469 | border-radius: 50%;
470 | width: 16px;
471 | height: 16px;
472 | margin: 0px;
473 | animation: spin 1s linear infinite;
474 | }
475 |
476 | @keyframes spin {
477 | 0% {
478 | transform: rotate(0deg);
479 | }
480 |
481 | 100% {
482 | transform: rotate(360deg);
483 | }
484 | }
485 |
486 | // This is styling for Landing page
487 | #landing-container {
488 | background-color: $secondary-color;
489 | width: 100%;
490 | display: flex;
491 | flex-direction: column;
492 | justify-content: flex-start;
493 | align-items: center;
494 |
495 | #logo-large {
496 | height: 48%;
497 | width: auto;
498 | }
499 |
500 | #namespace-selector-form {
501 | margin-top: 15vh;
502 | height: fit-content;
503 | width: 400px;
504 | padding: 10px 30px 30px 30px;
505 |
506 | // the code below will center the form absolutely
507 | // position: absolute;
508 | // margin-left: auto;
509 | // margin-right: auto;
510 | // left: 0;
511 | // right: 0;
512 |
513 | background-color: white;
514 | border-radius: 10px;
515 | box-shadow: 5px 10px 10px rgba(rgb(64, 0, 0), 0.2);
516 |
517 | text-align: center;
518 |
519 | #ns-error {
520 | margin-top: 1rem;
521 | display: flex;
522 | align-items: center;
523 | justify-content: center;
524 | }
525 |
526 | #error {
527 | color: red;
528 | font-weight: bold;
529 | font-size: 0.75rem;
530 | }
531 |
532 | h2 {
533 | color: $font-color;
534 | font-size: 1.25rem;
535 | }
536 |
537 | #selector-namespace {
538 | width: 320px;
539 | height: 3rem;
540 | padding: 10px;
541 | font-size: 1.2rem;
542 | border-radius: 5px;
543 | }
544 | }
545 | }
546 |
547 | select:hover,
548 | select:focus-visible,
549 | button:hover,
550 | button:focus-visible {
551 | outline: 2px solid $p-light-color;
552 | }
553 |
554 | // ANALYSIS PAGE ------------------------------------------------------------------------------------------>>
555 |
556 | #analysis-container {
557 | display: flex;
558 | flex-direction: column;
559 | // align-items: center;
560 | background-color: $secondary-color;
561 |
562 | // height: calc(100vh - $header-height);
563 | .analysis-nav {
564 | display: flex;
565 | justify-content: flex-start;
566 | align-items: center;
567 | margin: 1rem 2rem;
568 | color: $font-color;
569 |
570 | .analysis-oomkill-data {
571 | display: flex;
572 | flex-direction: column;
573 | justify-content: center;
574 | align-items: center;
575 | flex: 1;
576 | min-width: 30vw;
577 | min-height: 5.4rem;
578 | background-color: white;
579 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
580 | border-radius: 10px;
581 | padding: 0rem 1rem;
582 | font-size: 0.4rem;
583 | max-width: auto;
584 |
585 | .oomkilled-pod-data {
586 | margin-top: 0.4rem;
587 | text-transform: uppercase;
588 | font-size: 0.8rem;
589 | color: $font-color;
590 | font-weight: 700;
591 | text-align: center;
592 | }
593 |
594 | &-container {
595 | display: flex;
596 | font-size: 1rem;
597 | justify-content: center;
598 | gap: 3rem;
599 | }
600 | }
601 |
602 | &-left {
603 | display: flex;
604 | justify-content: flex-start;
605 | gap: 2rem;
606 |
607 | .loading-bar {
608 | display: flex;
609 | align-items: center;
610 | justify-content: space-between;
611 |
612 | #loadname {
613 | padding-right: 1rem;
614 | }
615 | }
616 |
617 | .analysis-form {
618 | display: grid;
619 | grid-template-columns: repeat(5, auto);
620 | width: fit-content;
621 |
622 | flex: 1;
623 | justify-content: center;
624 | align-items: center;
625 | gap: 1rem;
626 | min-height: 4rem;
627 |
628 | #oomkill-selector {
629 | font-size: 1rem;
630 | padding: 0.25rem 0.5rem;
631 | cursor: pointer;
632 | box-shadow: 2px 2px 6px rgb(64 0 0 / 20%);
633 | border-radius: 10px;
634 | border: none;
635 | height: fit-content;
636 | width: 380px;
637 | }
638 |
639 | .analysis-interval {
640 | font-size: 1rem;
641 | padding: 0.25rem 0.5rem;
642 | cursor: pointer;
643 | box-shadow: 2px 2px 6px rgb(64 0 0 / 20%);
644 | border-radius: 10px;
645 | border: none;
646 | height: fit-content;
647 | width: 3.5rem;
648 | }
649 |
650 | .interval-unit {
651 | font-size: 1rem;
652 | padding: 0.25rem 0.5rem;
653 | cursor: pointer;
654 | box-shadow: 2px 2px 6px rgb(64 0 0 / 20%);
655 | border-radius: 10px;
656 | border: none;
657 | height: fit-content;
658 | }
659 |
660 | .query-btn {
661 | font-size: 1rem;
662 | padding: 0.25rem 0.5rem;
663 | cursor: pointer;
664 | box-shadow: 2px 2px 6px rgb(64 0 0 / 20%);
665 | border-radius: 10px;
666 | border: none;
667 | background-color: $ugly-sewage-green;
668 | color: white;
669 | transition: all 0.2s;
670 | height: fit-content;
671 | }
672 |
673 | .query-btn:hover {
674 | background-color: white;
675 | color: $font-color;
676 | cursor: pointer;
677 | }
678 |
679 | .tooltip {
680 | cursor: pointer;
681 | }
682 | }
683 | }
684 | }
685 |
686 | .no-data-msg {
687 | flex-wrap: nowrap;
688 | font-size: 0.8rem;
689 | text-align: center;
690 | background-color: #ddd;
691 | border-radius: 10px;
692 | padding: 1rem;
693 | color: $font-color;
694 | }
695 |
696 | .analysis-oomkill-data-msg {
697 | font-size: 0.9rem;
698 | }
699 |
700 | .analysis-main {
701 | display: flex;
702 | width: 100%;
703 | height: 78vh;
704 | flex-grow: 1;
705 |
706 | & > div {
707 | margin: 1rem 2rem;
708 | }
709 |
710 | .pod-overview {
711 | display: flex;
712 | flex-direction: column;
713 | padding: 1rem 1.5rem;
714 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
715 | background-color: white;
716 | border-radius: 10px;
717 | height: 14rem;
718 | margin: 0rem 1rem 2rem 1rem;
719 |
720 | .summary {
721 | text-transform: uppercase;
722 | font-size: 0.8rem;
723 | color: $font-color;
724 | font-weight: 700;
725 | text-align: center;
726 | margin-bottom: 1rem;
727 | }
728 | }
729 |
730 | .analysis-oomkill-data {
731 | display: flex;
732 | flex-direction: column;
733 | justify-content: center;
734 | align-items: stretch;
735 | min-height: 5.4rem;
736 | height: fit-content;
737 | background-color: whitesmoke;
738 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
739 | border-radius: 10px;
740 | padding: 1rem 24px;
741 | font-size: 0.4rem;
742 |
743 | &-container {
744 | background-color: white;
745 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
746 | border-radius: 10px;
747 | // margin-right: 8px;
748 | padding: 16px 24px;
749 | font-size: 1rem;
750 |
751 | & > p {
752 | margin: 0.5rem;
753 | }
754 | }
755 |
756 | .oomkilled-pod-data {
757 | margin-bottom: 0.6rem;
758 | text-transform: uppercase;
759 | font-size: 0.8rem;
760 | color: $font-color;
761 | font-weight: 700;
762 | text-align: center;
763 | }
764 | }
765 |
766 | .filtered-log-container {
767 | display: flex;
768 | flex-direction: column;
769 | padding: 1rem 1.5rem;
770 | // adjustment for scroll bar
771 | padding-right: 0.5rem;
772 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
773 | background-color: whitesmoke;
774 | border-radius: 10px;
775 | // height: 30rem; // use this height for when the summary works
776 | overflow-y: auto;
777 | flex: 1;
778 |
779 | .no-data-msg {
780 | // also adjustment for scrollbar
781 | margin-right: calc(24px - 1rem);
782 | }
783 |
784 | .filtered-events-heading {
785 | text-transform: uppercase;
786 | font-size: 0.8rem;
787 | color: $font-color;
788 | font-weight: 700;
789 | text-align: center;
790 | margin-bottom: 1rem;
791 | margin-right: calc(24px - 0.5rem);
792 | }
793 |
794 | .filtered-events-container {
795 | padding-right: 0.5rem;
796 | overflow-y: auto;
797 | overflow-x: hidden;
798 | }
799 |
800 | .event-card {
801 | position: relative;
802 | font-size: 0.6rem;
803 | margin-bottom: 1rem;
804 | padding: 1rem 1.5rem;
805 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
806 | background-color: white;
807 | border-radius: 10px;
808 |
809 | & > * {
810 | word-wrap: break-word;
811 | }
812 | }
813 | }
814 |
815 | #left-side {
816 | width: 500px;
817 | gap: 16px;
818 | }
819 |
820 | // chart styling
821 | #chartarea {
822 | @extend .filtered-log-container;
823 | background-color: whitesmoke;
824 |
825 | // adjust for non-overlapping margins
826 | margin-left: 0;
827 |
828 | .chartarea-container {
829 | box-shadow: 5px 10px 10px rgb(64 0 0 / 20%);
830 | background-color: white;
831 | border-radius: 10px;
832 | margin-right: 1rem;
833 | }
834 |
835 | .chartarea-heading {
836 | text-transform: uppercase;
837 | font-size: 0.8rem;
838 | color: $font-color;
839 | font-weight: 700;
840 | text-align: center;
841 | margin-bottom: 0.6rem;
842 | margin-right: calc(24px - 0.5rem);
843 | }
844 |
845 | .graph-msg {
846 | margin-right: calc(24px - 0.5rem);
847 | }
848 |
849 | .chartarea-container {
850 | overflow-y: scroll;
851 | overflow-x: hidden;
852 | display: flex;
853 | flex-wrap: wrap;
854 | gap: 2rem;
855 | justify-content: space-around;
856 | }
857 | }
858 |
859 | .line-chart-div {
860 | display: flex;
861 | // border: 1px solid black;
862 | flex-direction: column;
863 | margin: 2rem;
864 | padding: 0.4rem;
865 | min-width: 0;
866 | min-width: 20rem;
867 |
868 | // chart height is fixed to prevent infinite height increase
869 | max-height: 20rem;
870 | flex: 0 1 calc(40% - 2rem);
871 | }
872 |
873 | .legend-btn-grid {
874 | background-color: $ugly-sewage-green;
875 | color: white;
876 | box-shadow: 2px 2px 6px rgb(64 0 0 / 20%);
877 | border-radius: 10px;
878 | border: none;
879 | padding: 0.4rem 0.8rem;
880 | border-radius: 10px;
881 | width: fit-content;
882 | align-self: center;
883 | transition: all 0.2s;
884 | font-size: 0.6rem;
885 | margin-top: 1rem;
886 | }
887 |
888 | .legend-btn-grid:hover {
889 | background-color: white;
890 | color: $font-color;
891 | cursor: pointer;
892 | border: none;
893 | // filter: drop-shadow(2px 2px 3px $font-color);
894 | }
895 | }
896 | }
897 |
898 | // MEDIA QUERIES
899 | @media only screen and (max-width: 800px) {
900 | #logo {
901 | width: 10rem;
902 | }
903 |
904 | #sidebar {
905 | flex: 1;
906 |
907 | .sidebar-page-container {
908 | .sidebar-icon {
909 | width: 1.2rem;
910 | }
911 |
912 | // .sidebar-names {
913 | // font-size: 0.8rem;
914 | // }
915 | }
916 | }
917 |
918 | .interval-unit,
919 | .query-btn,
920 | .tooltip-container {
921 | grid-row: 2;
922 | margin: 0;
923 | padding: 0;
924 | }
925 | }
926 |
--------------------------------------------------------------------------------
/client/components/AnalysisPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import ChartGrid from './ChartGrid';
3 | import { AnalysisPageProps, ChartGraphData } from '../Types';
4 | import LogCard from './LogCard';
5 | import Tooltip from './Tooltip';
6 |
7 | const AnalysisPage = (props: AnalysisPageProps) => {
8 | const [OOMKillsList, setOOMKillsList]: any = useState([]);
9 | const [allOOMKills, setAllOOMKills]: any = useState([]);
10 | const [filteredLogs, setFilteredLogs]: any = useState([]);
11 | const [logType, setLogType]: any = useState('events');
12 | const [tooltipState, setTooltipState]: any = useState(false);
13 | const [tooltip, setTooltip]: any = useState(<>>);
14 | const [hiddenInputs, setHiddenInputs]: any = useState([])
15 | const [loading, setLoading]: any = useState(false);
16 | const {
17 | analyzedPod,
18 | setAnalyzedPod,
19 | setAnalyzedData,
20 | analyzedData,
21 | showGraphs,
22 | setShowGraphs,
23 | }: any = props;
24 |
25 | const handleQuery = async (e: any) => {
26 | setLoading(true)
27 | let timeInterval = e.target["analysis-interval"].value + e.target['interval-unit'].value
28 | const podName = e.target['oomkill-selector'].value;
29 | if (podName === 'default') {
30 | setLoading(false)
31 | return
32 | }
33 | if (timeInterval === 'default' || !e.target["analysis-interval"].value) timeInterval = '5m'
34 |
35 | const nodeName = e.target[podName].value;
36 | const timeOfDeath = new Date(analyzedPod.started).toISOString();
37 |
38 | try {
39 | // analyzeData1 is object with all chart data. see type
40 | const analyzeData1: ChartGraphData = await window.api.getAnalysis(nodeName, timeInterval, timeOfDeath)
41 |
42 | props.setAnalyzedData(analyzeData1);
43 | setShowGraphs(true)
44 | setLoading(false)
45 | } catch (err) {
46 | setLoading(false)
47 | return err
48 | }
49 | }
50 |
51 | // displays analyzed pod
52 | const updateAnalyzedPod = (e: any) => {
53 | // sets podName as node name. we deliver all pod data based on parent node, so we extract the pod's parent (node)
54 | const podName = e.target.value;
55 | const newAnalysis = allOOMKills.filter(
56 | (oomkill: any) => oomkill.podName === podName
57 | );
58 | setAnalyzedPod({ ...newAnalysis[0] });
59 | };
60 |
61 | const openTooltip = (e: any) => {
62 | const position = {
63 | top: e.pageY.toString() + 'px',
64 | left: e.pageX.toString() + 'px',
65 | };
66 | setTooltip( );
67 | setTooltipState(true);
68 | };
69 |
70 | const closeTooltip = () => {
71 | setTooltipState(false);
72 | };
73 |
74 | useEffect(() => {
75 | // Queries for all OOMKilled pods and stores in state variables
76 | // 1) oomKillOptions - array of pod names used for drop down list
77 | // 2) allOomKills - array of oomkilled objects
78 | const renderOOMKills = async () => {
79 | const hiddenInps: any[] = []
80 | const oomkillData = await window.api.getOOMKills();
81 | const oomKillOptions: JSX.Element[] = oomkillData.map(
82 | (oomkill: any, i: number): JSX.Element => {
83 | hiddenInps.push( )
84 | return (
85 |
86 | {oomkill.podName}
87 |
88 | );
89 | }
90 | );
91 | setOOMKillsList([...oomKillOptions]);
92 | setAllOOMKills([...oomkillData]);
93 | setHiddenInputs([...hiddenInps]);
94 | };
95 |
96 | // Queries and generates filtered logs of events for pod being analyzed
97 | const createLogs = async () => {
98 | const logCards: JSX.Element[] = [];
99 | const logsData = await window.api.getEvents();
100 | const filtered = logsData.filter(
101 | (log: any) => log.object.slice(4) === analyzedPod.podName
102 | );
103 | for (let i = 0; i < filtered.length; i++) {
104 | logCards.push(
105 |
117 | );
118 | }
119 | setFilteredLogs([...logCards]);
120 | };
121 |
122 | // onChange, match the selected option pod with the pod in the allOOMKills then set analyzedPod to be that pod
123 | renderOOMKills();
124 | createLogs();
125 |
126 | }, [analyzedPod]);
127 |
128 | return (
129 |
130 |
131 |
132 | {/* -------------------- START OF FORM -------------------- */}
133 |
177 | {/* -------------------- END OF FORM -------------------- */}
178 | {loading && (
179 |
180 |
Loading
181 |
182 |
183 | )}
184 |
185 |
186 |
187 | {/* -------------------- START OF LEFT AREA -------------------- */}
188 |
189 | {/* -------------------- START OF OOM KILL DATA -------------------- */}
190 |
191 |
OOMKilled Pod Data
192 | {analyzedPod.podName ? (
193 |
194 |
195 | Pod: {analyzedPod.podName}
196 |
197 |
198 | Restarts: {analyzedPod.restartcount}
199 |
200 |
201 | Terminated at: {' '}
202 | {analyzedPod.started.slice(0, -6)}
203 |
204 |
205 | Restarted at: {' '}
206 | {analyzedPod.finished.slice(0, -6)}
207 |
208 |
209 | ) : (
210 |
Select OOMKilled Pod to Display Data
211 | )}
212 |
213 | {/* -------------------- END OF OOMKILL Data -------------------- */}
214 |
215 |
216 |
Events
217 |
218 | {analyzedPod.podName && filteredLogs.length > 0 ? (
219 | filteredLogs
220 | ) : analyzedPod.podName ? (
221 |
No Events to Display
222 | ) : (
223 |
224 | Select OOMKilled Pod to Display Data
225 |
226 | )}
227 |
228 |
229 |
230 |
231 | {/* -------------------- CHART AREA -------------------- */}
232 |
233 |
Metrics
234 | {showGraphs ? (
235 |
236 |
237 |
238 |
239 | ) : (
240 |
241 | Please Query an OOMKilled Pod to Display Charts
242 |
243 | )}
244 |
245 |
246 |
247 | );
248 | };
249 |
250 | export default AnalysisPage;
251 |
--------------------------------------------------------------------------------
/client/components/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { HashRouter, Link, Route, Routes } from 'react-router-dom';
3 |
4 | // page containers for React Router
5 | import HomePage from './HomePage';
6 | import LandingPage from './LandingPage';
7 | import AnalysisPage from './AnalysisPage';
8 |
9 | // import styles sheet here
10 | import '../assets/stylesheets/style.scss';
11 |
12 | // initial data for charts. app breaks if there's no initial data
13 | const initData: any = [
14 | {
15 | Port9090isClosed: {
16 | times: ['a', 'b', 'c'],
17 | values: [1, 2, 3],
18 | },
19 | },
20 | {
21 | Port9090isClosedOpenIt: {
22 | times: ['a', 'b', 'c'],
23 | values: [3, 2, 1],
24 | },
25 | },
26 | ];
27 |
28 | import Sidebar from './Sidebar';
29 |
30 | const App = () => {
31 | const [analyzedPod, setAnalyzedPod]: any = React.useState({});
32 | const [resourceError, setResourceError]: any = React.useState('');
33 | const [menuOpen, setMenuOpen]: any = React.useState(true);
34 | const [showGraphs, setShowGraphs]: any = React.useState(false);
35 | const [analyzedData, setAnalyzedData]: any = React.useState({
36 | podMem: initData,
37 | podCPU: initData,
38 | nodeMem: initData,
39 | nodeCPU: initData,
40 | netRead: initData,
41 | netWrite: initData,
42 | });
43 |
44 | return (
45 |
46 |
47 |
48 |
53 |
54 |
61 | }
62 | />
63 |
75 | }
76 | />
77 |
89 | }
90 | />
91 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default App;
98 |
--------------------------------------------------------------------------------
/client/components/ChartGrid.tsx:
--------------------------------------------------------------------------------
1 | import { Line } from 'react-chartjs-2';
2 | import { useState, useEffect } from 'react';
3 | import { GraphData, ChartGraphData, GraphableData } from '../Types';
4 | import {
5 | Chart as ChartJS,
6 | CategoryScale,
7 | LinearScale,
8 | PointElement,
9 | LineElement,
10 | Title,
11 | Tooltip,
12 | Legend,
13 | } from 'chart.js';
14 | import { watchPlugins } from '../../jest.config';
15 |
16 | ChartJS.register(
17 | CategoryScale,
18 | LinearScale,
19 | PointElement,
20 | LineElement,
21 | Title,
22 | Tooltip,
23 | Legend
24 | );
25 |
26 | // populates charts on analysis page
27 | const ChartGrid = (props: any) => {
28 | // show/hide legend buttons
29 | const [buttonClicked, setButtonClicked] = useState({
30 | podMem: false,
31 | podCPU: false,
32 | nodeMem: false,
33 | nodeCPU: false,
34 | netRead: false,
35 | netWrite: false,
36 | });
37 | const { analyzedData } = props;
38 | const initData: GraphData = [
39 | {
40 | Port9090isClosed: {
41 | times: ['a', 'b', 'c'],
42 | values: [1, 2, 3],
43 | },
44 | },
45 | {
46 | Port9090isClosedOpenIt: {
47 | times: ['a', 'b', 'c'],
48 | values: [3, 2, 1],
49 | },
50 | },
51 | ];
52 | // initData so app doesn't crash on load
53 | const [graphState, setGraphState] = useState({
54 | podCPU: initData,
55 | podMem: initData,
56 | nodeMem: initData,
57 | nodeCPU: initData,
58 | netRead: initData,
59 | netWrite: initData,
60 | });
61 |
62 | useEffect(() => {
63 | // set state with the data received from props object
64 | setGraphState(analyzedData);
65 | }, [props.analyzedData]);
66 |
67 | const colorArray: string[] = [
68 | 'red',
69 | 'blue',
70 | 'green',
71 | 'black',
72 | 'purple',
73 | 'cyan',
74 | 'yellow',
75 | 'orange',
76 | ];
77 |
78 | const xLabels: string[] =
79 | graphState.nodeMem[0][Object.keys(graphState.nodeMem[0])[0]].times;
80 |
81 | let options: string = JSON.stringify({
82 | responsive: true,
83 | responsiveAnimationDuration: 1000,
84 | maintainAspectRatio: false,
85 | pointRadius: 0,
86 | indexAxis: 'x',
87 | plugins: {
88 | legend: {
89 | display: false,
90 | position: 'bottom' as const,
91 | },
92 | title: {
93 | display: true,
94 | text: 'working title',
95 | },
96 | },
97 | scales: {
98 | x: {
99 | grid: {
100 | color: 'rgb(240, 240, 240)',
101 | },
102 | ticks: {
103 | color: '#797676',
104 | },
105 | title: {
106 | display: true,
107 | text: new Date().toDateString(),
108 | },
109 | },
110 | y: {
111 | grid: {
112 | color: 'rgb(240, 240, 240)',
113 | },
114 | ticks: {
115 | color: '#797676',
116 | },
117 | title: {
118 | display: true,
119 | text: 'Mibibytes',
120 | },
121 | },
122 | },
123 | });
124 |
125 | const multiOptions = {
126 | nodeMem: JSON.parse(options),
127 | nodeCPU: JSON.parse(options),
128 | podMem: JSON.parse(options),
129 | podCPU: JSON.parse(options),
130 | netRead: JSON.parse(options),
131 | netWrite: JSON.parse(options),
132 | };
133 |
134 | const charts: JSX.Element[] = [];
135 | let datasetData = [] as GraphableData[];
136 | let keyCounter: number = 0;
137 |
138 | // handles open legend for specific graph instead of all of them
139 | const handleLegendClick = (
140 | keyName:
141 | | 'podCPU'
142 | | 'podMem'
143 | | 'nodeMem'
144 | | 'nodeCPU'
145 | | 'netRead'
146 | | 'netWrite'
147 | ) => {
148 | const newButton = { ...buttonClicked };
149 | newButton[keyName] = !newButton[keyName];
150 | setButtonClicked(newButton);
151 | };
152 |
153 | // first we iterate of the total number of graphs we want
154 | (Object.keys(graphState) as (keyof typeof graphState)[]).forEach(key => {
155 | // then we iterate over all of the lines in that graph
156 | for (let i = 0; i < graphState[key].length; i++) {
157 | const podName: string = Object.keys(graphState[key][i])[0];
158 | if (!colorArray[i])
159 | colorArray.push(
160 | '#' + Math.floor(Math.random() * 16777215).toString(16)
161 | );
162 | datasetData.push({
163 | label: podName,
164 | backgroundColor: colorArray[i],
165 | borderColor: colorArray[i],
166 | data: graphState[key][i][podName].values,
167 | });
168 | }
169 | multiOptions[key].plugins.legend.display = buttonClicked[key];
170 |
171 | // this is part of the each individual graphs
172 | // multiOptions[key].scales.y.title.text = 'y-axis label';
173 | switch (key) {
174 | case 'nodeMem':
175 | multiOptions[key].scales.y.title.text = 'MegaBytes';
176 | multiOptions[key].plugins.title.text = 'Node Memory Usage';
177 | break;
178 | case 'nodeCPU':
179 | multiOptions[key].scales.y.title.text = 'Milicores';
180 | multiOptions[key].plugins.title.text = 'Node CPU Usage';
181 | break;
182 | case 'podMem':
183 | multiOptions[key].scales.y.title.text = 'MegaBytes';
184 | multiOptions[key].plugins.title.text = 'Pod Memory Usage';
185 | break;
186 | case 'podCPU':
187 | multiOptions[key].scales.y.title.text = 'Milicores';
188 | multiOptions[key].plugins.title.text = 'Pod CPU Usage';
189 | break;
190 | case 'netRead':
191 | multiOptions[key].scales.y.title.text = 'KiloBytes';
192 | multiOptions[key].plugins.title.text = 'Network Read';
193 | break;
194 | case 'netWrite':
195 | multiOptions[key].scales.y.title.text = 'KiloBytes';
196 | multiOptions[key].plugins.title.text = 'Network Write';
197 | break;
198 | default:
199 | console.log('Default Case Hit');
200 | break;
201 | }
202 |
203 | charts.push(
204 |
205 |
213 | handleLegendClick(key)}
216 | >
217 | {!buttonClicked[key] ? 'Show Legend' : 'Hide Legend'}
218 |
219 |
220 | );
221 | datasetData = [] as GraphableData[];
222 | });
223 |
224 | return <>{charts}>;
225 | };
226 |
227 | export default ChartGrid;
228 |
--------------------------------------------------------------------------------
/client/components/ClusterChart.tsx:
--------------------------------------------------------------------------------
1 | import { ClusterChartProps } from '../Types';
2 | import ClusterChartCard from './ClusterChartCard';
3 |
4 | const ClusterChart = (props: ClusterChartProps): JSX.Element => {
5 |
6 | // do not change order of this.
7 | const names: ["Pods", "Nodes"] = ["Pods", "Nodes"]
8 | const clusterCards: any = [];
9 |
10 | // async function to update each card's usage with query to prom via main process
11 | const getData = async (name: string, resource: string, obj: any) => {
12 | try {
13 | const usageData = await window.api.getUsage(name, resource);
14 | if (usageData) obj.usage = usageData[0]
15 | } catch(err) {
16 | console.log('Error', err)
17 | }
18 | }
19 |
20 | if (props.click)
21 | for (let i = 0; i < names.length; i++) {
22 | clusterCards.push(
23 |
30 | );
31 | }
32 |
33 | if (clusterCards.length > 0) {
34 | for (let i = 0; i < clusterCards.length; i++) {
35 | for (let j = 0; j < clusterCards[i].props.data.length; j++) {
36 | const card = clusterCards[i].props.data[j]
37 | // excluding the temp cards in initial state
38 | if (card.resource !== 'hello') {
39 | getData(card.name, card.resource, card);
40 | }
41 | }
42 | }
43 | }
44 |
45 | return (
46 |
47 | {clusterCards}
48 |
49 | )
50 | };
51 |
52 | export default ClusterChart;
53 |
--------------------------------------------------------------------------------
/client/components/ClusterChartCard.tsx:
--------------------------------------------------------------------------------
1 | import { ClusterChartCardProps } from '../Types';
2 |
3 | const ClusterChartCard = (props: ClusterChartCardProps): JSX.Element => {
4 | const squares: JSX.Element[] = [];
5 | const colors: string[] = [
6 | '#96ca82',
7 | '#cdd185',
8 | '#FAB733',
9 | '#FF8E15',
10 | '#FF4E11',
11 | '#FF0D0D',
12 | '#363636',
13 | ];
14 |
15 | let index: number = 0;
16 | // checks usage of cluster chard cards. if usage is over a certain limit, it will color differently
17 | for (let i = 0; i < props.data.length; i++) {
18 | let usage: number = props.data[i].usage - props.data[i].request;
19 | let limit: number = props.data[i].limit - props.data[i].request;
20 | if (usage < 0) index = 0;
21 | else if (usage < 0.2 * limit) index = 1;
22 | else if (usage < 0.4 * limit) index = 2;
23 | else if (usage < 0.6 * limit) index = 3;
24 | else if (usage < 0.8 * limit) index = 4;
25 | else if (props.data[i].unit === undefined || props.data[i].unit === null)
26 | index = 6;
27 | else index = 5;
28 | squares.push(
29 | props.click(e, props.data[i])}
31 | width={30}
32 | height={30}
33 | key={i + 1000}
34 | >
35 |
42 |
43 | );
44 | }
45 |
46 | return (
47 |
48 |
{props.title}
49 |
{squares}
50 |
51 | );
52 | };
53 |
54 | export default ClusterChartCard;
55 |
--------------------------------------------------------------------------------
/client/components/Events.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import LogCard from './LogCard';
3 | import { EventProps } from '../Types';
4 | import { capitalize } from '../../electron/utils';
5 |
6 | const Events = (props: EventProps): JSX.Element => {
7 | const [logs, setLogs]: any = useState([]);
8 | const [logType, setLogType]: any = useState('events');
9 | const [severityType, setSeverityType]: any = useState('Default');
10 | const [loading, setLoading]: any = useState(true);
11 | const { analyzedPod, setAnalyzedPod, setShowGraphs }: any = props;
12 |
13 | const handleLogTypeChange = (e: any) => {
14 | const logTypeStr = e.target.value;
15 | setLogType(logTypeStr);
16 | };
17 |
18 | const handleSeverityChange = (e: any) => {
19 | const severity = capitalize(e.target.value);
20 | setSeverityType(severity);
21 | };
22 |
23 | useEffect(() => {
24 | // populate and set logCards according to what type of logs is requested.
25 | // this is a helper function as typescript was not playing nicely with useEffect as an async function
26 | const createLogs = async () => {
27 | const logCards: JSX.Element[] = [];
28 | let logsData;
29 | if (logType === 'events') {
30 | // events is an object with prop "formatttedEvents", which is an array of objects
31 | // each obj in array has the following keys: namespace, lastSeen, severity, reason, message, object
32 | logsData = await window.api.getEvents();
33 | } else if (logType === 'alerts') {
34 | logsData = await window.api.getAlerts();
35 | } else if (logType === 'oomkills') {
36 | logsData = await window.api.getOOMKills();
37 | }
38 | setLoading(false);
39 | for (let i = 0; i < logsData.length; i++) {
40 | logCards.push(
41 |
54 | );
55 | }
56 |
57 | if (severityType !== 'Default') {
58 | const filteredLogs = logCards.filter((log: any) => {
59 | if (
60 | logType === 'events' &&
61 | log.props.eventObj.severity === severityType
62 | )
63 | return log;
64 | else if (
65 | logType === 'alerts' &&
66 | log.props.alertObj.severity === severityType
67 | )
68 | return log;
69 | });
70 | setLogs(filteredLogs);
71 | } else setLogs(logCards);
72 | };
73 |
74 | createLogs();
75 | }, [logType, severityType]);
76 |
77 | return (
78 |
79 |
80 | {
86 | setLoading(true);
87 | handleLogTypeChange(e);
88 | }}
89 | >
90 | Events
91 | Alerts
92 | OOMKills
93 |
94 | {logType !== 'oomkills' ? (
95 | {
101 | setLoading(true);
102 | handleSeverityChange(e);
103 | }}
104 | >
105 | Default
106 | Info
107 | Warning
108 | Error
109 | Critical
110 | Alert
111 | Emergency
112 | Debug
113 |
114 | ) : null}
115 | {loading && (
116 | <>
117 | Loading
118 |
119 | >
120 | )}
121 |
122 |
123 | {logs.length ? (
124 | logs
125 | ) : (
126 |
127 | No {logType[0].toUpperCase() + logType.slice(1)} in Current
128 | Namespace
129 |
130 | )}
131 |
132 |
133 | );
134 | };
135 |
136 | export default Events;
137 |
--------------------------------------------------------------------------------
/client/components/Graph.tsx:
--------------------------------------------------------------------------------
1 | import { GraphData } from '../Types';
2 | import { Line } from 'react-chartjs-2';
3 | import { useState, useEffect } from 'react';
4 | import { useNavigate } from 'react-router-dom';
5 | // I dont know why, but you need all this ChartJS stuff to make the react-chartjs-2 to work
6 | import {
7 | Chart as ChartJS,
8 | CategoryScale,
9 | LinearScale,
10 | PointElement,
11 | LineElement,
12 | Title,
13 | Tooltip,
14 | Legend,
15 | } from 'chart.js';
16 |
17 | ChartJS.register(
18 | CategoryScale,
19 | LinearScale,
20 | PointElement,
21 | LineElement,
22 | Title,
23 | Tooltip,
24 | Legend
25 | );
26 |
27 | const Graph = (props: any): JSX.Element => {
28 | const [portOpen, setPortOpen] = useState(false);
29 | const [buttonClicked, setButtonClicked] = useState(false);
30 | const [graphState, setGraphState] = useState([
31 | {
32 | Port9090isClosed: {
33 | times: ['a', 'b', 'c'],
34 | values: [1, 2, 3],
35 | },
36 | },
37 | {
38 | Port9090isClosed: {
39 | times: ['a', 'b', 'c'],
40 | values: [3, 2, 1],
41 | },
42 | },
43 | ]);
44 |
45 | const navigate = useNavigate();
46 |
47 | if (!portOpen)
48 | fetch('http://localhost:9090/').then(response => {
49 | if (response.status === 200) {
50 | console.log('Port 9090 is Open');
51 | setPortOpen(true);
52 | } else {
53 | //optional place to throw error when port 9090 is closed
54 | }
55 | });
56 |
57 | useEffect(() => {
58 | refreshGraphData();
59 | }, [portOpen]);
60 |
61 | const refreshGraphData = () => {
62 | if (portOpen) {
63 | window.api
64 | .getMemoryUsageByPods()
65 | .then((output: any) => {
66 | if (output.length < 1) {
67 | return navigate('/');
68 | } else if (!output.err) setGraphState(output);
69 | })
70 | .catch((err: any) => {
71 | return { err: err };
72 | });
73 | setTimeout(refreshGraphData, 5000);
74 | }
75 | };
76 |
77 | const datasetData = [];
78 | const colorArray = [
79 | 'red',
80 | 'blue',
81 | 'green',
82 | 'black',
83 | 'purple',
84 | 'cyan',
85 | 'yellow',
86 | 'orange',
87 | '#003d33',
88 | ];
89 |
90 | const xLabels: string[] = graphState[0][Object.keys(graphState[0])[0]].times;
91 |
92 | for (let i = 0; i < graphState.length; i++) {
93 | const podName: string = Object.keys(graphState[i])[0];
94 | datasetData.push({
95 | label: podName,
96 | backgroundColor: colorArray[i],
97 | borderColor: colorArray[i],
98 | data: graphState[i][podName].values,
99 | });
100 | }
101 |
102 | const options: any = {
103 | responsive: true,
104 | responsiveAnimationDuration: 1000,
105 | pointRadius: 0,
106 | indexAxis: 'x',
107 | plugins: {
108 | legend: {
109 | display: buttonClicked,
110 | position: 'bottom' as const,
111 | },
112 | title: {
113 | display: true,
114 | text: 'Current Memory Usage by Pods',
115 | },
116 | },
117 | scales: {
118 | x: {
119 | grid: {
120 | color: 'rgb(240, 240, 240)',
121 | },
122 | ticks: {
123 | color: '#797676',
124 | },
125 | title: {
126 | display: true,
127 | text: new Date().toDateString(),
128 | },
129 | },
130 | y: {
131 | grid: {
132 | color: 'rgb(240, 240, 240)',
133 | },
134 | ticks: {
135 | color: '#797676',
136 | },
137 | title: {
138 | display: true,
139 | text: 'Megabytes',
140 | },
141 | },
142 | },
143 | };
144 |
145 | const handleLegendClick = () => {
146 | setButtonClicked(prevCheck => !prevCheck);
147 | };
148 |
149 | const data = {
150 | labels: xLabels,
151 | datasets: datasetData,
152 | };
153 |
154 | return (
155 | <>
156 |
157 |
158 | {!buttonClicked ? 'Show' : 'Hide'}
159 |
160 | >
161 | );
162 | };
163 |
164 | export default Graph;
165 |
--------------------------------------------------------------------------------
/client/components/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, MouseEvent } from 'react';
2 | import ClusterChart from './ClusterChart';
3 | import Events from './Events';
4 | import Graph from './Graph';
5 | import DetailsModal from './Modal';
6 |
7 | import {
8 | ClusterChartProps,
9 | SvgInfo,
10 | ModalProps,
11 | ClusterAllInfo,
12 | } from '../Types';
13 | import { useNavigate } from 'react-router-dom';
14 |
15 | const tempData: SvgInfo[] = [
16 | {
17 | name: 'string',
18 | usage: 1,
19 | resource: 'hello',
20 | request: 0.9,
21 | limit: Math.random() + 1,
22 | parent: 'string',
23 | namespace: 'string',
24 | },
25 | ];
26 |
27 | const initalClusterChartData: ClusterAllInfo = {
28 | Nodes: tempData,
29 | Pods: tempData,
30 | };
31 |
32 | const HomePage = (props: any): JSX.Element => {
33 | const [pods, setPods]: any = useState([]);
34 | const [nodes, setNodes]: any = useState(['node1']);
35 | const [portOpen, setPortOpen]: any = useState(false);
36 | const [resource, setResource]: any = useState('');
37 | const [clusterChartData, setClusterChartData]: any = useState(
38 | initalClusterChartData
39 | );
40 | const navigate = useNavigate();
41 | const { menuOpen, setShowGraphs } = props;
42 |
43 | // Ways to clean up the modal:
44 | // the modal is split into two states. the modalState could probably accept the JSX component as a key value
45 | const [modalState, setModalState] = useState(false);
46 | const [theModal, setTheModal] = useState(No Modal Info
);
47 |
48 | const openModal = (e: MouseEvent, data: SvgInfo) => {
49 | console.log('Opening Modal');
50 | const position = {
51 | top: e.pageY.toString() + 'px',
52 | left: e.pageX.toString() + 'px',
53 | };
54 | const propData: ModalProps = {
55 | ...data,
56 | position: position,
57 | close: closeModal,
58 | };
59 | setTheModal( );
60 | setModalState(true);
61 | };
62 |
63 | const closeModal = (): void => {
64 | setModalState(false);
65 | };
66 |
67 | const closeModalFromAnywhere = (e: MouseEvent): void => {
68 | if (e.target instanceof HTMLDivElement) setModalState(false);
69 | };
70 |
71 | const gke: ClusterChartProps = {
72 | ...clusterChartData,
73 | click: openModal,
74 | close: closeModal,
75 | };
76 |
77 | // loads all cluster chart info. number of nodes, pods and names, etc.
78 | const renderData = async () => {
79 | const allTheInfo = await window.api.getAllInfo();
80 |
81 | if (resource === 'memory' || resource === 'cpu') {
82 | allTheInfo.Pods = allTheInfo.Pods.filter(
83 | (info: any) => info.resource === resource
84 | );
85 | setClusterChartData(allTheInfo);
86 | } else {
87 | setClusterChartData(allTheInfo);
88 | }
89 | };
90 |
91 | const handleResourceChange = (e: any) => {
92 | setResource(e.target.value);
93 | };
94 |
95 | useEffect(() => {
96 | renderData();
97 | }, [resource]);
98 |
99 | return (
100 |
101 |
130 |
139 | {modalState && theModal}
140 |
141 | );
142 | };
143 |
144 | export default HomePage;
145 |
--------------------------------------------------------------------------------
/client/components/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | const LandingPage = (props: any): JSX.Element => {
5 | const [namespaces, setNamespaces] = useState([]);
6 | const navigate = useNavigate();
7 | let j = 150;
8 |
9 | const error = props.resourceError;
10 |
11 | useEffect(() => {
12 | window.api
13 | .getNamespaces()
14 | .then((data) => {
15 | setNamespaces(data);
16 | })
17 | .catch((err: Error) => {
18 | console.log(
19 | "An error occurred in LandingPage in function useEffect() for namespaces"
20 | );
21 | });
22 | }, [namespaces]);
23 |
24 | const handleNamespaceChange = (e: React.ChangeEvent) => {
25 | localStorage.setItem("namespace", e.target.value);
26 | // the below fetch can end up being slow due to the time length
27 | props.setResourceError(
);
28 |
29 | window.api.getMemoryUsageByPods().then((output: any) => {
30 | if (output.length < 1) {
31 | // console.log("returning out without setGraphState");
32 | props.setResourceError(No resources found in this namespace
);
33 | return navigate("/");
34 | } else {
35 | props.setResourceError(<>>);
36 | navigate("/home");
37 | }
38 | });
39 | };
40 |
41 | return (
42 |
43 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default LandingPage;
68 |
--------------------------------------------------------------------------------
/client/components/LogCard.tsx:
--------------------------------------------------------------------------------
1 | import { LogCardProps } from '../Types';
2 | import { useState } from 'react';
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | const LogCard = (props: LogCardProps): JSX.Element => {
6 | const navigate = useNavigate();
7 | const {setLoading, setAnalyzedPod, setShowGraphs }: any = props;
8 |
9 | const handleAnalyze = async () => {
10 | setLoading(true)
11 | setAnalyzedPod({ ...props.oomObj });
12 | try {
13 | if (props.oomObj) {
14 | // sets end time as the time of death to query and get data leading up to pod's termination time
15 | const timeOfDeath = new Date(props.oomObj.started).toISOString();
16 | const analyzeData = await window.api.getAnalysis(props.oomObj.node, '15s', timeOfDeath);
17 | //access to cluster chart data which shows limits/requests
18 | setShowGraphs(true);
19 | // sets state of analyzeData so analysispage can preload with selected pod
20 | props.setAnalyzedData(analyzeData);
21 | }
22 | setLoading(false)
23 | navigate('/analysis');
24 | } catch (err) {
25 | return console.log('error: ', err);
26 | }
27 | };
28 |
29 | // create the header elements
30 | let headerObj: { [key: string]: string } = {};
31 | let bodyObj: { [key: string]: string } = {};
32 | if (props.logType === 'events' && props.eventObj) {
33 | // part 1: anonymous function with input and output (take in args, returns and object)
34 | // (({namespace, severity}) => ({namespace, severity}))
35 | // part 2: invoke the function on props.eventObj
36 | headerObj = (({ namespace, severity }) => ({ namespace, severity }))(
37 | props.eventObj
38 | );
39 | bodyObj = (({ reason, message, object, lastSeen }) => ({
40 | reason,
41 | message,
42 | object,
43 | lastSeen,
44 | }))(props.eventObj);
45 | } else if (props.logType === 'alerts' && props.alertObj) {
46 | headerObj = (({ group, severity }) => ({ group, severity }))(
47 | props.alertObj
48 | );
49 | bodyObj = (({ name, state, description, summary }) => ({
50 | name,
51 | state,
52 | description,
53 | summary,
54 | }))(props.alertObj);
55 | } else if (props.logType === 'oomkills' && props.oomObj) {
56 | // the line below is an IIFE, which returns a single object with a key-value pair namaespace: namespace, taken from the props.oomObj object
57 | // we do this so we can extract only the data we want à la GraphQL style
58 | headerObj = (({ namespace }) => ({ namespace }))(props.oomObj);
59 | bodyObj = (({
60 | podName,
61 | restartcount,
62 | laststate,
63 | reason,
64 | exitcode,
65 | started,
66 | finished,
67 | }) => ({
68 | podName,
69 | restartcount,
70 | laststate,
71 | reason,
72 | exitcode,
73 | started,
74 | finished,
75 | }))(props.oomObj);
76 | }
77 |
78 | const header: JSX.Element[] = [];
79 | const body: JSX.Element[] = [];
80 | // need to make sure order is perserved in objects!!!
81 | let k = 500;
82 | for (const x in headerObj) {
83 | const label: string = x[0].toUpperCase() + x.slice(1) + ':';
84 |
85 | header.push(
86 |
87 |
88 | {label} {headerObj[x]}
89 |
90 |
91 | );
92 | }
93 | for (const x in bodyObj) {
94 | let label: string = x[0].toUpperCase() + x.slice(1) + ':';
95 |
96 | // Fixes the capitalization of body labels
97 | switch (label) {
98 | case 'PodName:':
99 | label = 'Pod Name:';
100 | break;
101 | case 'Restartcount:':
102 | label = 'Restart Count:';
103 | break;
104 | case 'Laststate:':
105 | label = 'Last State:';
106 | break;
107 | case 'Exitcode:':
108 | label = 'Exit Code:';
109 | break;
110 | case 'LastSeen:':
111 | label = 'Last Seen:';
112 | break;
113 | default:
114 | break;
115 | }
116 |
117 | body.push(
118 |
119 |
120 | {label} {bodyObj[x]}
121 |
122 |
123 | );
124 | }
125 |
126 | return (
127 |
128 | {props.logType === 'oomkills' && (
129 |
130 |
131 | Analyze
132 |
133 |
134 | )}
135 |
136 | {body}
137 |
138 | );
139 | };
140 |
141 | export default LogCard;
142 |
--------------------------------------------------------------------------------
/client/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { ModalProps } from '../Types';
2 |
3 | const DetailsModal = (props: ModalProps) => {
4 | const {
5 | name,
6 | usage,
7 | request,
8 | resource,
9 | unit,
10 | limit,
11 | parent,
12 | namespace,
13 | position,
14 | close,
15 | } = props;
16 |
17 | return (
18 |
19 |
20 |
35 |
36 |
37 | Name: {name}
38 |
39 |
40 | Namespace: {namespace}
41 |
42 |
43 | Parent: {parent}
44 |
45 |
46 | Usage: {usage}
47 |
48 |
49 | Request: {request}
50 |
51 |
52 | Limit: {limit}
53 |
54 |
55 | Unit: {unit}
56 |
57 |
58 | Resource: {resource}
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default DetailsModal;
67 |
--------------------------------------------------------------------------------
/client/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { SidebarProps } from '../Types';
3 |
4 | const Sidebar = (props: SidebarProps) => {
5 | const { menuOpen, setMenuOpen } = props;
6 |
7 | const toggleMenu = () => {
8 | setMenuOpen(menuOpen ? false : true);
9 | };
10 |
11 | return (
12 |
77 | );
78 | };
79 |
80 | export default Sidebar;
81 |
--------------------------------------------------------------------------------
/client/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { TooltipProps } from '../Types';
2 |
3 | const Tooltip = (props: TooltipProps) => {
4 | const { position } = props;
5 |
6 | return (
7 |
8 |
9 |
10 | 1. Select an OOMKilled Pod to begin analysis
11 |
12 | 2. By default, the time interval will be set to 5 minutes before the
13 | OOMKill event
14 |
15 |
16 | 3. To adjust the time, input desired time interval, select a unit of
17 | time, and click "Query"
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default Tooltip;
26 |
--------------------------------------------------------------------------------
/client/global.d.ts:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/extend-expect";
2 |
3 | declare global {
4 | /**
5 | * We define all IPC APIs here to give devs auto-complete
6 | * use window.electron anywhere in app
7 | * Also note the capital "Window" here. When accessing window object, the "w" is not capitalized.
8 | */
9 | interface Window {
10 | api: {
11 | getEvents: () => Promise;
12 | getNodes: () => Promise;
13 | getLogs: () => Promise;
14 | getDeployments: () => Promise;
15 | getMemoryUsageByPods: () => Promise;
16 | getServices: () => Promise;
17 | getPods: () => Promise;
18 | getNamespaces: () => Promise;
19 | getAlerts: () => Promise;
20 | getAllInfo: () => Promise;
21 | getOOMKills: () => Promise;
22 | getUsage: (name: string, resource: string) => Promise;
23 | getAnalysis: (nodeName: string, timeInterval?: string, timeOfDeath?: string) => Promise;
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Palaemon
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot, Root } from 'react-dom/client';
2 | import HomePage from './components/HomePage';
3 | import App from './components/App';
4 |
5 | const rootElement: HTMLElement | null = document.getElementById('root');
6 |
7 | if (!rootElement) throw new Error('Fail to get root element in index.ts');
8 |
9 | const root: Root = createRoot(rootElement);
10 |
11 | root.render( );
--------------------------------------------------------------------------------
/electron/main.ts:
--------------------------------------------------------------------------------
1 | import {
2 | app,
3 | BrowserWindow,
4 | ipcMain,
5 | dialog,
6 | } from 'electron';
7 | import { ClusterAllInfo } from '../client/Types';
8 | import path from 'path';
9 | // import installExtension, {
10 | // REACT_DEVELOPER_TOOLS,
11 | // REDUX_DEVTOOLS,
12 | // } from 'electron-devtools-installer';
13 |
14 | import * as k8s from '@kubernetes/client-node';
15 | import * as cp from 'child_process';
16 | const fetch: any = (...args: any) =>
17 | import('node-fetch').then(({ default: fetch }: any) => fetch(...args));
18 |
19 | import {
20 | setStartAndEndTime,
21 | formatEvents,
22 | formatAlerts,
23 | parseNode,
24 | fetchMem,
25 | fetchCPU,
26 | formatOOMKills,
27 | } from './utils';
28 |
29 | // metrics modules
30 | import {
31 | formatMatrix,
32 | formatUsage,
33 | formatAnalysis,
34 | } from './metricsData/formatMatrix';
35 | import { SvgInfo, SvgInfoObj } from '../client/Types';
36 |
37 | // K8S API BOILERPLATE
38 | const kc = new k8s.KubeConfig();
39 | kc.loadFromDefault();
40 | const k8sApiCore = kc.makeApiClient(k8s.CoreV1Api);
41 | const k8sApiApps = kc.makeApiClient(k8s.AppsV1Api);
42 |
43 | const PROM_URL = 'http://127.0.0.1:9090/api/v1/';
44 |
45 | const isDev: boolean = process.env.NODE_ENV === 'development';
46 | // const PORT: string | number = process.env.PORT || 8080;
47 |
48 | // this is to allow the BrowserWindow object to be referrable globally
49 | // however, BrowserWindow cannot be created before app is 'ready'
50 | let mainWindow: any = null;
51 |
52 | const loadMainWindow = () => {
53 | mainWindow = new BrowserWindow({
54 | width: 1200,
55 | height: 800,
56 | show: false,
57 | icon: path.resolve(__dirname, '../client/assets/logo_hat.ico'),
58 | webPreferences: {
59 | nodeIntegration: true,
60 | // contextIsolation: false,
61 | devTools: isDev, //whether to enable DevTools
62 | preload: path.join(__dirname, 'preload.js'),
63 | },
64 | });
65 |
66 | mainWindow.loadFile(path.join(__dirname, '../client/index.html'));
67 | console.log('Main Window loaded file index.html');
68 |
69 | // check to see if port 9090 is open
70 | const checkPort = () => {
71 | fetch('http://localhost:9090/')
72 | .then((res: any) => {
73 | console.log('status code in loadMainWindow is ', res.status);
74 | mainWindow.show();
75 | })
76 | .catch((err: Error) => {
77 | console.log('fetch to 9090 has failed in main.ts in loadMainWindow');
78 | const num = dialog.showMessageBoxSync({
79 | message:
80 | 'PALAEMON ERROR: 9090',
81 | type: 'warning',
82 | // Cancel returns 0, OK returns 1
83 | buttons: ['Cancel', 'OK'],
84 | title: 'PALAEMON',
85 | detail: 'Port-forward Prometheus service to port 9090, then press OK.\n \nVisit palaemon.io for more information.',
86 | });
87 | if (num === 1) checkPort();
88 | else if (num === 0) app.quit();
89 | });
90 | };
91 | checkPort();
92 | };
93 |
94 | app.on('ready', async () => {
95 | // if(isDev){
96 | // const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
97 | // const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS];
98 | // installExtension(
99 | // extensions,
100 | // {loadExtensionOptions: {allowFileAccess: true}, forceDownload: forceDownload}
101 | // ).then((name:string) => {console.log(`Added Extension: ${name}`)})
102 | // .then(loadMainWindow)
103 | // // .catch((err: Error) => {console.log('There was an Error: ', err)})
104 | // }
105 | // else loadMainWindow();
106 | loadMainWindow();
107 | });
108 |
109 | app.on('window-all-closed', () => {
110 | if (process.platform !== 'darwin') {
111 | app.quit();
112 | }
113 | });
114 |
115 | // K8S API //
116 |
117 | // populates cluster chart cards
118 |
119 | ipcMain.handle('getAllInfo', async (): Promise => {
120 | // nodes
121 |
122 | const tempData: SvgInfo = {
123 | name: 'deploy',
124 | usage: 1,
125 | resource: 'deploy',
126 | request: 0.9,
127 | limit: Math.random() + 1,
128 | parent: 'deploy',
129 | namespace: 'deploy',
130 | };
131 |
132 | try {
133 | const nsSelect = await mainWindow.webContents
134 | .executeJavaScript('({...localStorage});', true)
135 | /* check what type this is with team */ /* check what type this is with team */
136 | .then((localStorage: any) => {
137 | return localStorage.namespace;
138 | });
139 | const getNodes = await k8sApiCore.listNode(`${nsSelect}`);
140 | const nodeData = getNodes.body.items.map(node => {
141 | return parseNode(node);
142 | }); // end of nodeData
143 |
144 | const getPods = await k8sApiCore.listNamespacedPod(`${nsSelect}`);
145 | // console.log('this is getPods: ',getPods.body.items[0]);
146 |
147 | const memData = await Promise.all(
148 | getPods.body.items.map(pod => {
149 | // console.log('this is all pods fom k8s', pod)
150 | return fetchMem(pod);
151 | })
152 | );
153 | const cpuData = await Promise.all(
154 | getPods.body.items.map(pod => fetchCPU(pod))
155 | );
156 |
157 | const filteredMem = memData;
158 | // const filteredMem = memData.filter(el => el.request > 1)
159 | const filteredCPU = cpuData.filter(el => el.resource === 'cpu');
160 | const filteredPods = filteredMem;
161 |
162 | for (let i = 0; i < filteredCPU.length; i++) {
163 | filteredPods.push(filteredCPU[i]);
164 | }
165 |
166 | if (filteredPods) {
167 | const newObj: ClusterAllInfo = {
168 | Nodes: nodeData,
169 | Pods: filteredPods,
170 | };
171 | return newObj;
172 | }
173 | } catch (error) {
174 | return [tempData];
175 | }
176 | });
177 |
178 | // get names of nodes in cluster related to selected namespace
179 | ipcMain.handle('getNodes', async (): Promise => {
180 | // dynamically get this from frontend later
181 | try {
182 | const nsSelect = await mainWindow.webContents
183 | .executeJavaScript('({...localStorage});', true)
184 | /* check what type this is with team */ /* check what type this is with team */
185 | .then((localStorage: any) => {
186 | return localStorage.namespace;
187 | });
188 | const data = await k8sApiCore.listNode(`${nsSelect}`);
189 |
190 | return data.body.items;
191 | } catch (error) {
192 | return console.log(`Error in getNodes function: ERROR: ${error}`);
193 | }
194 | });
195 |
196 | // get namespaces in cluster. used for multiple queries and to display relevant data
197 | ipcMain.handle('getNamespaces', async () => {
198 | try {
199 | const data = await k8sApiCore.listNamespace();
200 | const formattedData: any = data.body.items.map(pod => pod?.metadata?.name);
201 | return formattedData;
202 | } catch (error) {
203 | console.log(`Error in getNamespaces function: ERROR: ${error}`);
204 | }
205 | });
206 |
207 | // COMMAND LINE //
208 | // get all events in all namespaces
209 | ipcMain.handle('getEvents', async () => {
210 | try {
211 | const response: string = cp
212 | .execSync('kubectl get events --all-namespaces', {
213 | encoding: 'utf-8',
214 | })
215 | .toString();
216 | return formatEvents(response);
217 | } catch (error) {
218 | return console.log(`Error in getEvents function: ERROR: ${error}`); // add return statement to make async () => on line 112 happy
219 | }
220 | });
221 |
222 | // HOMEPAGE CHART QUERY FOR MEMORY
223 |
224 | ipcMain.handle('getMemoryUsageByPods', async () => {
225 | const { startTime, endTime } = setStartAndEndTime();
226 |
227 | const interval = '15s';
228 | try {
229 | const nsSelect = await mainWindow.webContents
230 | .executeJavaScript('({...localStorage});', true)
231 | .then((localStorage: any) => {
232 | return localStorage.namespace;
233 | });
234 | // fetch time series data from prom api
235 | const query = `${PROM_URL}query_range?query=container_memory_working_set_bytes{namespace="${nsSelect}",image=""}&start=${startTime}&end=${endTime}&step=${interval}`;
236 | // fetch request
237 | const res = await fetch(query);
238 | const data = await res.json();
239 |
240 | return formatMatrix(data.data);
241 | } catch (error) {
242 | console.log(`Error in getMemoryUsageByPod function: ERROR: ${error}`);
243 | return { err: error };
244 | }
245 | });
246 |
247 | // get alerts from alert manager
248 | ipcMain.handle('getAlerts', async (): Promise => {
249 | try {
250 | const data: any = await fetch(`${PROM_URL}rules`);
251 | const alerts: any = await data.json();
252 | return formatAlerts(alerts);
253 | } catch (error) {
254 | console.log(`Error in getAlerts function: ERROR: ${error}`);
255 | }
256 | });
257 |
258 | ipcMain.handle('getOOMKills', async (): Promise => {
259 | try {
260 | // query for pods' last terminated reasons
261 | const response = await fetch(
262 | `${PROM_URL}query?query=kube_pod_container_status_last_terminated_reason`
263 | );
264 | const data = await response.json();
265 | const pods = data.data.result;
266 |
267 | // filters for OOMKilledPods and returns an array of names
268 | const OOMKilledPods = pods
269 | .filter((pod: any) => pod.metric.reason === 'OOMKilled')
270 | .map((pod: any) => pod.metric.pod);
271 |
272 | return formatOOMKills(OOMKilledPods);
273 | } catch (error) {
274 | console.log(`Error in getOOMKills function: ERROR: ${error}`);
275 | }
276 | });
277 |
278 | // TEST FOR USAGE ON HOMEPAGE AND ANALYSIS PAGE
279 |
280 | ipcMain.handle('getUsage', async (event, ...args) => {
281 | const time = new Date().toISOString();
282 | const interval = '15s';
283 | const podName = args[0];
284 | const resource = args[1];
285 | let namespace;
286 | try {
287 | await mainWindow.webContents
288 | .executeJavaScript('({...localStorage});', true)
289 | .then((localStorage: any) => {
290 | namespace = localStorage.namespace;
291 | });
292 |
293 | // fetch time series matrix both cpu and mem usage filtered by pod
294 |
295 | const query =
296 | resource === 'memory'
297 | ? `${PROM_URL}query_range?query=
298 | container_memory_working_set_bytes{namespace="${namespace}",pod="${podName}",image=""}
299 | &start=${time}&end=${time}&step=${interval}`
300 | : `${PROM_URL}query_range?query=
301 | sum(rate(container_cpu_usage_seconds_total{namespace="${namespace}",pod="${podName}",image=""}[5m]))
302 | &start=${time}&end=${time}&step=${interval}`;
303 |
304 | const res = await fetch(query);
305 | const data = await res.json();
306 |
307 | // based on second argument, different calculations for units will take place in formatUsage
308 | return resource === 'memory'
309 | ? formatUsage(data.data, 'megabytes')
310 | : formatUsage(data.data, 'milicores');
311 | } catch (error) {
312 | console.log(`Error in getUSAGE function: ERROR: ${error}`);
313 | return { err: error };
314 | }
315 | });
316 |
317 | /* -------------- Analysis Page -------------- */
318 |
319 | ipcMain.handle("getAnalysis", async (event, parentNode, interval = '5m', timeOfDeath) => {
320 |
321 | const endTime = timeOfDeath;
322 | const now = new Date(timeOfDeath);
323 | const copyNow = new Date(now.getTime());
324 | // convert to ISO String for promQL
325 | copyNow.setHours(copyNow.getHours() - 1);
326 | const startTime: string = copyNow.toISOString();
327 |
328 | try {
329 | // build mem usage by PODS graph
330 | const podMEMQuery = `${PROM_URL}query_range?query=
331 | container_memory_working_set_bytes{node="${parentNode}",image=""}
332 | &start=${startTime}&end=${endTime}&step=${interval}`;
333 | const podMEMRes = await fetch(podMEMQuery);
334 | const podMEMData = await podMEMRes.json();
335 | const podMem = await formatAnalysis(podMEMData.data, "megabytes", startTime, endTime);
336 |
337 | // build CPU usage by PODS graph
338 | const podCPUQuery = `${PROM_URL}query_range?query=
339 | rate(container_cpu_usage_seconds_total{node="${parentNode}",image=""}[5m])
340 | &start=${startTime}&end=${endTime}&step=${interval}`;
341 | const podCPURes = await fetch(podCPUQuery);
342 | const podCPUData = await podCPURes.json();
343 | const podCPU = await formatAnalysis(podCPUData.data, 'milicores');
344 |
345 | // build sum of mem usage by node from start to end
346 | const nodeMEMQuery = `${PROM_URL}query_range?query=
347 | sum(container_memory_working_set_bytes{node="${parentNode}",image=""}) by (node)
348 | &start=${startTime}&end=${endTime}&step=${interval}`;
349 | const nodeMEMRes = await fetch(nodeMEMQuery);
350 | const nodeMEMData = await nodeMEMRes.json();
351 | const nodeMem = await formatAnalysis(nodeMEMData.data, 'megabytes');
352 |
353 | // build sum of CPU usage by node from start to end
354 | const nodeCPUQuery = `${PROM_URL}query_range?query=
355 | sum(rate(container_cpu_usage_seconds_total{node="${parentNode}",image=""}[5m])) by (node)
356 | &start=${startTime}&end=${endTime}&step=${interval}`;
357 | const nodeCPURes = await fetch(nodeCPUQuery);
358 | const nodeCPUData = await nodeCPURes.json();
359 | const nodeCPU = await formatAnalysis(nodeCPUData.data, 'milicores');
360 |
361 | // build network bytes read graph
362 | const networkReadQuery = `${PROM_URL}query_range?query=
363 | rate(container_network_receive_bytes_total{node="${parentNode}"}[5m])
364 | &start=${startTime}&end=${endTime}&step=${interval}`
365 | const networkReadRes = await fetch(networkReadQuery);
366 | const networkReadData = await networkReadRes.json();
367 | const netRead = await formatAnalysis(networkReadData.data, 'bytes')
368 |
369 | // build network bytes write graph
370 | const networkWriteQuery = `${PROM_URL}query_range?query=
371 | rate(container_network_transmit_bytes_total{node="${parentNode}"}[5m])
372 | &start=${startTime}&end=${endTime}&step=${interval}`
373 | const networkWriteRes = await fetch(networkWriteQuery);
374 | const networkWriteData = await networkWriteRes.json();
375 | const netWrite = await formatAnalysis(networkWriteData.data, 'kilobytes')
376 |
377 | return {
378 | podMem,
379 | podCPU,
380 | nodeMem,
381 | nodeCPU,
382 | netRead,
383 | netWrite
384 | };
385 | } catch (error) {
386 | console.log(`Error in getAnalysis function: ERROR: ${error}`);
387 | return { err: error };
388 | }
389 | });
390 |
--------------------------------------------------------------------------------
/electron/metricsData/formatMatrix.ts:
--------------------------------------------------------------------------------
1 | import { Metrics } from "@kubernetes/client-node";
2 |
3 | import { SvgInfo, ClusterChartProps } from "../../client/Types";
4 |
5 | const fetch: any = (...args: any) =>
6 | import("node-fetch").then(({ default: fetch }: any) => fetch(...args));
7 |
8 | interface matrix {
9 | resultType?: string;
10 | result: [{ metric: {}; values: Array<[number, string]> }];
11 | }
12 |
13 | interface graph {
14 | [name: string]: {
15 | times: string[];
16 | values: number[];
17 | limits?: number;
18 | requests?: number;
19 | // units?: string;
20 | };
21 | }
22 |
23 | export function formatMatrix(matrix: matrix, unitType?: string) {
24 | const arr: any = [];
25 | // console.log('matrix THIS IS ', matrix)
26 |
27 | const dateOptions: any = {
28 | // dateStyle: "full",
29 | timeStyle: "short",
30 | };
31 |
32 | matrix.result.forEach((obj: any) => {
33 | const output: graph = {};
34 |
35 | const podName: string = obj.metric.pod;
36 | output[podName] = {
37 | times: [],
38 | values: [],
39 | // units: '',
40 | };
41 |
42 | output[podName].times = obj.values.map((el: [number, number]) => {
43 | // time value
44 | return new Date(el[0] * 1000).toLocaleTimeString("en-US", dateOptions);
45 | });
46 | //this is bytes/units - convert bytes to GB when unit type is bytes
47 |
48 | output[podName].values = obj.values.map((el: [number, number]) => {
49 | // change to megabytes
50 |
51 | return Number(el[1] / 1000000);
52 | });
53 | arr.push(output);
54 | });
55 |
56 | return arr;
57 | }
58 |
59 | export function formatUsage(matrix: matrix, unitType?: string) {
60 | let output;
61 |
62 | matrix.result.forEach((obj: any) => {
63 | output = obj.values.map((el: [number, number]) => {
64 | // change to megabytes
65 |
66 | if (unitType === "megabytes") {
67 | return Number(el[1] / 1000000);
68 | } else {
69 | return Number(el[1] * 1000);
70 | }
71 | });
72 | });
73 | return output;
74 | }
75 |
76 | export async function getPodReqLimits(podName: string, startTime?: string, endTime?: string) {
77 | const limitsQuery = `http://127.0.0.1:9090/api/v1/query_range?query=kube_pod_container_resource_limits{pod="${podName}",resource="memory"}&start=${startTime}&end=${endTime}&step=24h`;
78 | const requestsQuery = `http://127.0.0.1:9090/api/v1/query_range?query=kube_pod_container_resource_requests{pod="${podName}",resource="memory"}&start=${startTime}&end=${endTime}&step=24h`;
79 | const limit = await fetch(limitsQuery);
80 | const request = await fetch(requestsQuery);
81 | const limits: any = await limit.json();
82 | const requests: any = await request.json();
83 | let limitData:any;
84 | let requestData:any;
85 |
86 | limits.data.result.forEach((obj: any) => {
87 | limitData = obj.values.map((entry: [number, number]) => {
88 | return Number(entry[1] / 1000000)
89 | })
90 |
91 |
92 | requests.data.result.forEach((obj: any) => {
93 | requestData = obj.values.map((entry: [number, number]):any => {
94 | return Number(entry[1] / 1000000)
95 | })
96 | })
97 |
98 | // console.log('limitdata', limitData)
99 | // console.log('requestdata', requestData)
100 | })
101 | return {
102 | limitData,
103 | requestData
104 | }
105 | }
106 |
107 | export async function formatAnalysis(matrix: matrix, unitType?: string, startTime?: string, endTime?: string) {
108 | const arr: any = [];
109 | let reqObj: any = {
110 | limitData: [],
111 | requestData: []
112 | }
113 |
114 | const dateOptions: any = {
115 | timeStyle: "short",
116 | };
117 |
118 | await matrix.result.forEach(async (obj: any) => {
119 |
120 | const output: graph = {};
121 | let name: string = 'n/a';
122 | if (obj.metric.pod) {
123 | name = obj.metric.pod;
124 | if (unitType === "megabytes") {
125 | reqObj = await getPodReqLimits(name, startTime, endTime)
126 | }
127 | }
128 | // if theres no metric.pod, then the object being passed in is a node
129 | else if (!obj.metric.pod) {
130 | name = obj.metric.node;
131 | }
132 | if (!reqObj.limitData){
133 |
134 | reqObj = {
135 | limitData: [],
136 | requestData: []
137 | }
138 | }
139 | output[name] = {
140 | times: [],
141 | values: [],
142 | limits: reqObj.limitData,
143 | requests: reqObj.requestData
144 | };
145 |
146 | output[name].times = obj.values.map((el: [number, number]) => {
147 | // time value
148 | return new Date(el[0] * 1000).toLocaleTimeString("en-US", dateOptions);
149 | });
150 | //this is bytes/units - convert bytes to GB when unit type is bytes
151 |
152 | output[name].values = obj.values.map((el: [number, number]) => {
153 | // change to megabytes
154 |
155 | if (unitType === "megabytes") return Number(el[1] / 1000000);
156 | else if (unitType === "milicores") return Number(el[1]*1000)
157 | else if (unitType === "kilobytes") return Number(el[1]/1000)
158 | else if (unitType === "bytes") return Number(el[1])
159 | return;
160 | });
161 |
162 | arr.push(output);
163 | });
164 |
165 | return arr;
166 | }
--------------------------------------------------------------------------------
/electron/preload.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer, contextBridge } from 'electron';
2 |
3 | // contextBridge allows for functions in main.ts to be available in the frontend
4 |
5 | // Create window API with events at specific channels
6 |
7 | // Stretch goals: Accept namespace and pass in namespace to invoke method as 2nd arg
8 | const WINDOW_API = {
9 | // getNamespaces: async () => ipcRenderer.invoke('getNamespaces'),
10 | getNodes: async () => ipcRenderer.invoke('getNodes'),
11 | getDeployments: async () => ipcRenderer.invoke('getDeployments'),
12 | getServices: async () => ipcRenderer.invoke('getServices'),
13 | getPods: async () => ipcRenderer.invoke('getPods'),
14 | getLogs: async () => ipcRenderer.invoke('getLogs'),
15 | getEvents: async () => ipcRenderer.invoke('getEvents'),
16 | getNamespaces: async () => ipcRenderer.invoke('getNamespaces'),
17 | getMemoryUsageByPods: async () => ipcRenderer.invoke('getMemoryUsageByPods'),
18 | getCPUsage: async () => ipcRenderer.invoke('getCPUUsageByPods'),
19 | getAlerts: async () => ipcRenderer.invoke('getAlerts'),
20 | getLimits: async () => ipcRenderer.invoke('getLimits'),
21 | getAllInfo: async () => ipcRenderer.invoke('getAllInfo'),
22 | getOOMKills: async () => ipcRenderer.invoke('getOOMKills'),
23 | getUsage: async (...args: any) => ipcRenderer.invoke('getUsage', ...args),
24 | getAnalysis: async (...args: any) =>
25 | ipcRenderer.invoke('getAnalysis', ...args),
26 | };
27 |
28 | // exposes WINDOW_API methods to the frontend under "window.api" object
29 | contextBridge.exposeInMainWorld('api', WINDOW_API);
30 |
--------------------------------------------------------------------------------
/electron/utils.ts:
--------------------------------------------------------------------------------
1 | import { SvgInfo, SvgInfoObj, oomObject } from '../client/Types';
2 | import * as cp from 'child_process';
3 |
4 | const fetch: any = (...args: any) =>
5 | import('node-fetch').then(({ default: fetch }: any) => fetch(...args));
6 |
7 | // utilized for start and end times when querying for metrics
8 | export const setStartAndEndTime = () => {
9 | var now = new Date();
10 | var copyNow = new Date(now.getTime());
11 | copyNow.setHours(copyNow.getHours() - 1);
12 | var startTime = copyNow.toISOString();
13 | var endTime = new Date().toISOString();
14 | return {
15 | startTime: startTime,
16 | endTime: endTime,
17 | };
18 | };
19 |
20 | export const formatClusterData = (data: any): string[] => {
21 | const formattedData: string[] = data.body.items.map(
22 | (pod: any) => pod?.metadata?.name
23 | );
24 | return formattedData;
25 | };
26 |
27 | export const formatEvents = (data: any): {}[] => {
28 | const dataArr: string[] = data.split('\n');
29 | const trimmed: string[] = dataArr.map((el: any) => el.split(/[ ]{2,}/));
30 | const formattedEvents: {}[] = trimmed.map((event: any) => {
31 | return {
32 | namespace: event[0],
33 | lastSeen: event[1],
34 | severity: event[2],
35 | reason: event[3],
36 | message: event[5],
37 | object: event[4],
38 | };
39 | });
40 | return formattedEvents.slice(1, -1);
41 | };
42 |
43 | export const formatAlerts = (data: any): {}[] => {
44 | const formattedAlerts: object[] = [];
45 | data.data.groups.forEach((group: any) => {
46 | group.rules.forEach((rule: any) => {
47 | if (rule.state) {
48 | rule.labels.severity = capitalize(rule.labels.severity);
49 | rule.state = capitalize(rule.state);
50 |
51 | const alert: {} = {
52 | group: group.name,
53 | state: rule.state,
54 | name: rule.name,
55 | severity: rule.labels.severity,
56 | description: rule.annotations.description,
57 | summary: rule.annotations.summary,
58 | alerts: rule.alerts,
59 | };
60 | formattedAlerts.push(alert);
61 | }
62 | });
63 | });
64 | return formattedAlerts;
65 | };
66 |
67 | export function capitalize(data: string) {
68 | return data[0].toUpperCase() + data.slice(1);
69 | }
70 |
71 | export function parseMem(entry: string) {
72 | // if dealing with memory (ki, mb, mi, etc.)
73 |
74 | return parseInt(entry.slice(0, entry.length - 2));
75 | }
76 |
77 | export function parseNode(obj: any) {
78 | // for each node from query we spit back this object
79 |
80 | // using Type Assertion to create an empty object for the typed variable
81 | // this could potentially create inconsistencies.
82 | // const output: SvgInfo = {} as SvgInfo;
83 |
84 | // best practice might be to create a new class object with default values and set
85 | const output: SvgInfo = new SvgInfoObj();
86 |
87 | if (obj.status?.allocatable !== undefined) {
88 | const memRequest: number = parseMem(obj.status.allocatable.memory);
89 | output.request = memRequest / 1000;
90 | output.unit = 'megabytes'
91 | }
92 | if (obj.status?.capacity !== undefined) {
93 | const memLimit: number = parseMem(obj.status.capacity.memory);
94 | output.limit = memLimit / 1000;
95 | output.unit = 'megabytes'
96 | }
97 |
98 | // (if node is truthy, and if node.metadata is truthy, and if node.metadat.name is truthy)
99 | if (obj?.metadata?.name) output.name = obj.metadata.name;
100 | return output;
101 | }
102 |
103 | export async function fetchMem(obj: any) {
104 | const output: SvgInfo = new SvgInfoObj();
105 | const podName = obj.metadata.name;
106 | const { startTime, endTime } = setStartAndEndTime();
107 |
108 | if (obj?.metadata?.name) {
109 | output.name = obj.metadata.name;
110 | output.parent = obj.spec.nodeName;
111 | output.namespace = obj.metadata.namespace;
112 | }
113 |
114 | try {
115 | // PromQL to query resource limits for pods in all namespaces
116 | const limitsQuery = `http://127.0.0.1:9090/api/v1/query_range?query=kube_pod_container_resource_limits{pod="${podName}",resource="memory"}&start=${startTime}&end=${endTime}&step=24h`;
117 | const requestsQuery = `http://127.0.0.1:9090/api/v1/query_range?query=kube_pod_container_resource_requests{pod="${podName}",resource="memory"}&start=${startTime}&end=${endTime}&step=24h`;
118 | const limit = await fetch(limitsQuery);
119 | const request = await fetch(requestsQuery);
120 | const limitData: any = await limit.json();
121 | const requestData: any = await request.json();
122 |
123 | if (limitData.data.result[0]) {
124 | if (limitData.data.result[0].metric.resource === 'memory') {
125 | output.resource = 'memory';
126 | output.limit =
127 | parseInt(limitData.data.result[0].values[0][1]) / 1000000;
128 | output.request =
129 | parseInt(requestData.data.result[0].values[0][1]) / 1000000;
130 | output.unit = 'megabytes';
131 | }
132 | }
133 | return output;
134 | } catch (err) {
135 | return {
136 | name: '',
137 | usage: 1,
138 | resource: '',
139 | limit: 1,
140 | request: 1,
141 | parent: '',
142 | namespace: '',
143 | };
144 | }
145 | }
146 |
147 | export async function fetchCPU(obj: any) {
148 | const output: SvgInfo = new SvgInfoObj();
149 | const podName = obj.metadata.name;
150 | const { startTime, endTime } = setStartAndEndTime();
151 | // console.log('I AM POD NAME', podName)
152 | if (obj?.metadata?.name) {
153 | output.name = obj.metadata.name;
154 | output.parent = obj.spec.nodeName;
155 | output.namespace = obj.metadata.namespace;
156 | }
157 |
158 | try {
159 | const limitsQuery = `http://127.0.0.1:9090/api/v1/query_range?query=kube_pod_container_resource_limits{pod="${podName}",resource="cpu"}&start=${startTime}&end=${endTime}&step=24h`;
160 | const requestsQuery = `http://127.0.0.1:9090/api/v1/query_range?query=kube_pod_container_resource_requests{pod="${podName}",resource="cpu"}&start=${startTime}&end=${endTime}&step=24h`;
161 | const limit = await fetch(limitsQuery);
162 | const limitData: any = await limit.json();
163 | const request = await fetch(requestsQuery);
164 | const requestData: any = await request.json();
165 |
166 | if (limitData.data.result[0]) {
167 | if (limitData.data.result[0].metric.resource === 'cpu') {
168 | output.resource = 'cpu';
169 | output.limit = limitData.data.result[0].values[0][1] * 1000;
170 | output.request = requestData.data.result[0].values[0][1] * 1000;
171 | output.unit = 'milicores';
172 | }
173 | }
174 | return output;
175 | } catch (error) {
176 | return {
177 | name: '',
178 | usage: 1,
179 | request: 1,
180 | resource: '',
181 | limit: 1,
182 | parent: '',
183 | namespace: '',
184 | };
185 | }
186 | }
187 |
188 | export const formatOOMKills = (data: string[]) => {
189 | const OOMKills: {}[] = [];
190 |
191 | data.forEach((el: any) => {
192 | const podDesc = cp.execSync(`kubectl describe pod ${el}`).toString();
193 | const podData = podDesc.split('\n');
194 | const updatedPodData = podData.map(pod =>
195 | pod.replace(/^\s+|\s+$|\s+(?=\s)/g, '')
196 | );
197 | // console.log("this is updated pod data", updatedPodData);
198 | const indexOfTerm = updatedPodData.indexOf('Last State: Terminated');
199 | // console.log(indexOfTerm);
200 | const filteredPodData: string[] = updatedPodData.slice(
201 | indexOfTerm,
202 | indexOfTerm + 13
203 | );
204 | // console.log(filteredPodData);
205 |
206 | const oomObject: { [index: string]: any } = {};
207 |
208 | const namespaceStr: string = updatedPodData[1];
209 | const nsColonIdx: any = namespaceStr.indexOf(':');
210 | const namespace: string = namespaceStr.slice(nsColonIdx + 1).trim();
211 |
212 | const nodeStr: any = updatedPodData.filter(str => str.includes('Node:'))[0];
213 | const nodeColonIdx: any = nodeStr.indexOf(':');
214 | const nodeSlashIdx: any = nodeStr.indexOf('/');
215 | const node: string = nodeStr.slice(nodeColonIdx + 1, nodeSlashIdx).trim();
216 |
217 | const limitIdx: any = filteredPodData.indexOf('Limits:');
218 | const limitCpu = filteredPodData[limitIdx + 1];
219 | const limitMemory = filteredPodData[limitIdx + 2];
220 | const limits = {
221 | limitCpu,
222 | limitMemory,
223 | };
224 |
225 | const requestIdx = filteredPodData.indexOf('Requests:');
226 | const requestCpu = filteredPodData[requestIdx + 1];
227 | const requestMemory = filteredPodData[requestIdx + 2];
228 | const requests = {
229 | requestCpu,
230 | requestMemory,
231 | };
232 |
233 | oomObject.namespace = namespace;
234 | oomObject.podName = el;
235 | oomObject[filteredPodData[limitIdx]] = limits;
236 | oomObject[filteredPodData[requestIdx]] = requests;
237 | oomObject.node = node;
238 | filteredPodData.slice(0, 7).forEach((el: any) => {
239 | const colon: any = el.indexOf(':');
240 | // Extracts key from the left of colon and lowercases to send properly to frontend
241 | const key: keyof oomObject = el
242 | .slice(0, colon)
243 | .toLowerCase()
244 | .split(' ')
245 | .join('');
246 | // Extracts value from the right of colon and removes white space
247 | oomObject[key] = el.slice(colon + 2);
248 | });
249 |
250 | OOMKills.push(oomObject);
251 | });
252 |
253 | return OOMKills;
254 | };
255 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 |
5 | // jsdom is used for testing frontend, will need to use 'node' for backend server side testing
6 | testEnvironment: 'jsdom',
7 |
8 | // Test spec file resolution pattern
9 | // Matches parent folder `__tests__` and filename
10 | // should contain `test` or `spec`.
11 | testRegex: "(/__tests__/.*\\.(test|spec))\\.tsx?$",
12 |
13 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "palaemon",
3 | "version": "1.0.0",
4 | "description": "- A gentle, euthanization and diagnosis tool for out-of-memory (OOM) kubernetes pods. - Palaemon is a Greek, childe sea-god who came to aid sailors in distress. He was often depicted as riding a dolphin. Also, a genus of shrimp.",
5 | "scripts": {
6 | "build": "webpack --watch --config ./webpack.config.js",
7 | "build:production": "webpack --config ./webpack.production.js",
8 | "rmdir": "node -e \"var fs = require('fs'); process.argv.slice(1).map((fpath) => fs.rmdirSync(fpath, { recursive: true })); process.exit(0);\"",
9 | "clean:prebuild": "npm run rmdir -- dist",
10 | "dev": "concurrently \"webpack --watch --config ./webpack.config.js\" \"wait-on ./dist/electron/main.js && electronmon ./dist/electron/main.js\"",
11 | "electronmon": "electronmon ./dist/electron/main.js",
12 | "start": "npm run clean:prebuild && npm run dev",
13 | "start:production": "npm run build:production && electron ./dist/electron/main.js",
14 | "test": "jest",
15 | "test:watch": "jest --watch",
16 | "test:e2e": "playwright test"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/oslabs-beta/Palaemon.git"
21 | },
22 | "keywords": [],
23 | "author": "",
24 | "license": "ISC",
25 | "bugs": {
26 | "url": "https://github.com/oslabs-beta/Palaemon/issues"
27 | },
28 | "homepage": "https://github.com/oslabs-beta/Palaemon#readme",
29 | "browser": {
30 | "child_process": false
31 | },
32 | "dependencies": {
33 | "@kubernetes/client-node": "^0.17.0",
34 | "ajv": "^7.2.4",
35 | "chart.js": "^3.9.1",
36 | "child_process": "^1.0.2",
37 | "node-fetch": "^2.2.10",
38 | "react": "^18.2.0",
39 | "react-chartjs-2": "^4.3.1",
40 | "react-dom": "^18.2.0",
41 | "react-router-dom": "^6.3.0",
42 | "sass": "^1.54.8",
43 | "typescript": "^4.8.3"
44 | },
45 | "devDependencies": {
46 | "@playwright/test": "^1.25.2",
47 | "@testing-library/jest-dom": "^5.16.5",
48 | "@testing-library/react": "^13.4.0",
49 | "@types/jest": "^29.0.3",
50 | "@types/node-fetch": "^2.6.2",
51 | "@types/react": "^18.0.17",
52 | "@types/react-dom": "^18.0.6",
53 | "concurrently": "^7.3.0",
54 | "copy-webpack-plugin": "^11.0.0",
55 | "css-loader": "^6.7.1",
56 | "electron": "^20.1.0",
57 | "electron-builder": "^23.3.3",
58 | "electron-devtools-installer": "^3.2.0",
59 | "electronmon": "^2.0.2",
60 | "html-webpack-plugin": "^5.5.0",
61 | "image-webpack-loader": "^8.1.0",
62 | "jest": "^29.0.3",
63 | "jest-dom": "^4.0.0",
64 | "jest-environment-jsdom": "^29.0.3",
65 | "playwright": "^1.25.2",
66 | "postcss-loader": "^7.0.1",
67 | "sass-loader": "^13.0.2",
68 | "style-loader": "^3.3.1",
69 | "ts-jest": "^29.0.1",
70 | "ts-loader": "^9.3.1",
71 | "wait-on": "^6.0.1",
72 | "webpack": "^5.74.0",
73 | "webpack-cli": "^4.10.0"
74 | },
75 | "electronmon": {
76 | "patterns": [
77 | "dist/**/*",
78 | "!client/**/*",
79 | "!electron/**/*",
80 | "!__tests__/**/*",
81 | "!__mocks__/**/*",
82 | "!journal/**/*",
83 | "!node_modules/**/*"
84 | ]
85 | }
86 | }
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 | const config: PlaywrightTestConfig = {
3 | use: {
4 | headless: false,
5 | viewport: { width: 1280, height: 720 },
6 | ignoreHTTPSErrors: true,
7 | trace: 'on',
8 | video: 'on-first-retry',
9 | },
10 | //
11 | reporter:[
12 | ['list'],
13 | ['html', {open: 'never', outputFolder:'./__tests__/playwright_reports'}]
14 | ],
15 |
16 | // Each test is given 15 seconds
17 | timeout: 15 * 1000,
18 |
19 | // two retries for each test
20 | retries: 2,
21 |
22 | // only the files matching one of these patterns are executed as test files
23 | // default is .*(test|spec)\.(js|ts|mjs)
24 | testMatch: /.*\.e2e\.(js|ts|jsx|tsx)/,
25 |
26 | // directroy to be scanned for test files. Defaults to directory of the config file
27 | testDir: './__tests__',
28 | // outputDir: './__tests__/playwright_reports'
29 | };
30 | export default config;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "types": ["node", "jest", "@testing-library/jest-dom"],
5 | "module": "commonjs",
6 | "lib": ["dom", "es2015", "es2016", "es2017"],
7 | "allowJs": true,
8 | "jsx": "react-jsx",
9 | "sourceMap": true,
10 | "outDir": "dist",
11 | "strict": true,
12 | "esModuleInterop": true,
13 | "noImplicitAny": true,
14 | "noImplicitReturns": true,
15 | "strictNullChecks": true,
16 | "forceConsistentCasingInFileNames": true
17 | },
18 | "exclude": ["node_modules", "dist"]
19 | }
20 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 | // const NormalModuleReplacementPlugin = require("module-replace-webpack-plugin");
4 | const path = require('path');
5 |
6 | module.exports = [
7 | {
8 | mode: 'development',
9 | entry: {
10 | main: './electron/main.ts',
11 | preload: './electron/preload.ts',
12 | },
13 | target: 'electron-main',
14 | module: {
15 | rules: [
16 | {
17 | test: /\.ts(x?)$/,
18 | exclude: /node-modules/,
19 | use: 'ts-loader',
20 | },
21 | ],
22 | },
23 | resolve: {
24 | extensions: ['.ts', '.tsx', '.js', '.jsx'],
25 | },
26 | output: {
27 | path: path.resolve(__dirname, './dist/electron/'),
28 | clean: true,
29 | },
30 | externals: [
31 | {
32 | 'utf-8-validate': 'commonjs utf-8-validate',
33 | bufferutil: 'commonjs bufferutil',
34 | fsevents: "require('fsevents')",
35 | },
36 | ],
37 | },
38 | {
39 | mode: 'development',
40 | entry: './client/index.tsx',
41 | devtool: 'inline-source-map',
42 | module: {
43 | rules: [
44 | {
45 | test: /\.ts(x?)$/,
46 | exclude: /node_modules/,
47 | use: 'ts-loader',
48 | },
49 | {
50 | test: /\.s?[ac]ss$/,
51 | use: ['style-loader', 'css-loader', 'sass-loader'],
52 | },
53 | {
54 | test: /\.png/,
55 | type: 'asset/resource',
56 | },
57 | ],
58 | },
59 | resolve: {
60 | extensions: ['.ts', '.tsx', '.js', '.jsx'],
61 | },
62 | output: {
63 | path: path.resolve(__dirname, './dist/client'),
64 | filename: 'index.js',
65 | clean: true,
66 | },
67 | // devServer: {
68 | // host: 'localhost',
69 | // port: 8080,
70 | // hot: true,
71 | // static: {
72 | // directory: path.resolve(__dirname, './client/assets'),
73 | // publicPath: '/assets',
74 | // },
75 | // },
76 | plugins: [
77 | new HtmlWebpackPlugin({
78 | template: 'client/index.html',
79 | }),
80 | new CopyWebpackPlugin({
81 | patterns: [
82 | {
83 | from: path.resolve(__dirname, 'client/assets'),
84 | to: 'assets/',
85 | },
86 | ],
87 | }),
88 | // new webpack.NormalModuleReplacementPlugin(/node:/, (resource) => {
89 | // resource.request = resource.request.replace(/^node:/, "");
90 | // }),
91 | ],
92 | },
93 | ];
94 |
--------------------------------------------------------------------------------
/webpack.production.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 | const path = require('path');
4 |
5 | module.exports = [
6 | {
7 | mode: 'production',
8 | entry: {
9 | main: './electron/main.ts',
10 | preload: './electron/preload.ts'
11 | },
12 | target: 'electron-main',
13 | module: {
14 | rules: [
15 | {
16 | test: /\.ts(x?)$/,
17 | exclude: /node-modules/,
18 | use: 'ts-loader',
19 | },
20 | ],
21 | },
22 | resolve: {
23 | extensions: ['.ts', '.tsx', '.js', '.jsx'],
24 | },
25 | output: {
26 | path: path.resolve(__dirname, 'dist/electron'),
27 | clean: true
28 | },
29 | externals: [
30 | {
31 | 'utf-8-validate': 'commonjs utf-8-validate',
32 | bufferutil: 'commonjs bufferutil',
33 | fsevents: "require('fsevents')",
34 | },
35 | ],
36 | },
37 | {
38 | mode: 'production',
39 | entry: './client/index.tsx',
40 | devtool: false,
41 | module: {
42 | rules: [
43 | {
44 | test: /\.ts(x?)$/,
45 | exclude: /node_modules/,
46 | use: 'ts-loader',
47 | },
48 | {
49 | test: /\.s?[ac]ss$/,
50 | use: ['style-loader', 'css-loader', 'sass-loader'],
51 | },
52 | {
53 | test: /\.png/,
54 | type: 'asset/resource',
55 | },
56 | ],
57 | },
58 | resolve: {
59 | modules: [__dirname, 'client', 'node_modules'],
60 | extensions: ['*', '.ts', '.tsx', '.js', '.jsx'],
61 | },
62 | output: {
63 | path: path.resolve(__dirname, 'dist/client'),
64 | filename: 'index.js',
65 | clean: true,
66 | },
67 | plugins: [
68 | new HtmlWebpackPlugin({
69 | template: '/client/index.html',
70 | }),
71 | new CopyWebpackPlugin({
72 | patterns: [
73 | {
74 | from: path.resolve(__dirname, 'client/assets'),
75 | to: 'assets/',
76 | },
77 | ],
78 | }),
79 | ],
80 | },
81 | ];
82 |
--------------------------------------------------------------------------------