1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Complete a majority of the rest of DI

- All KubeApi and KubeObjectStore's

- All of Catalog

- ApiManager

- KubeObjectDetailRegistry

- All BaseStore's and their migrations

- LensProxy and Router

- WindowManager

- App Menu

- Tray Icon

- ThemeStore

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-01-28 11:32:02 -05:00
parent 6cba82c491
commit 1747cbf6c8
1345 changed files with 34695 additions and 27703 deletions

View File

@ -11,6 +11,7 @@ module.exports = {
"**/dist/**/*",
"**/static/**/*",
"**/site/**/*",
"**/__mocks__/**/*",
],
settings: {
react: {
@ -109,12 +110,15 @@ module.exports = {
],
parserOptions: {
ecmaVersion: 2018,
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
sourceType: "module",
},
rules: {
"no-constant-condition": ["error", { "checkLoops": false }],
"header/header": [2, "./license-header"],
"no-invalid-this": "off",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
@ -129,6 +133,7 @@ module.exports = {
"named": "never",
"asyncArrow": "always",
}],
"require-await": "error",
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
"unused-imports/no-unused-vars-ts": [
"warn", {
@ -193,7 +198,9 @@ module.exports = {
],
parserOptions: {
ecmaVersion: 2018,
tsconfigRootDir: __dirname,
sourceType: "module",
project: ["./tsconfig.json"],
jsx: true,
},
rules: {
@ -201,6 +208,7 @@ module.exports = {
"header/header": [2, "./license-header"],
"react/prop-types": "off",
"no-invalid-this": "off",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
@ -220,6 +228,7 @@ module.exports = {
"named": "never",
"asyncArrow": "always",
}],
"require-await": "error",
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
"unused-imports/no-unused-vars-ts": [
"warn", {

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
module.exports = {
languages: {
register: jest.fn(),
setMonarchTokensProvider: jest.fn(),
registerCompletionItemProvider: jest.fn(),
},
editor: {
defineTheme: jest.fn(),
getModel: jest.fn(),
createModel: jest.fn(),
},
Uri: {
file: jest.fn(),
},
};

View File

@ -5,11 +5,11 @@
import fs from "fs-extra";
import path from "path";
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark.json";
import defaultBaseLensTheme from "../src/renderer/internal-themes/lens-dark.json";
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
const outputCssFile = path.resolve("src/renderer/internal-themes/theme-vars.css");
const banner = `/*
const banner = `/*
Generated Lens theme CSS-variables, don't edit manually.
To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
*/`;
@ -28,4 +28,4 @@ ${themeCssVars.join("\n")}
// Run
console.info(`"Saving default Lens theme css-variables to "${outputCssFile}""`);
fs.ensureFileSync(outputCssFile);
fs.writeFile(outputCssFile, content);
fs.writeFileSync(outputCssFile, content);

View File

@ -38,7 +38,7 @@ export async function generateTrayIcon(
await fs.writeFile(pngIconDestPath, pngIconBuffer);
console.info(`[DONE]: Tray icon saved at "${pngIconDestPath}"`);
} catch (err) {
console.error(`[ERROR]: ${err}`);
console.error(`[ERROR]: ${String(err)}`);
}
}
@ -49,7 +49,9 @@ const iconSizes: Record<string, number> = {
"3x": 48,
};
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
for (const dpiSuffix in iconSizes) {
const pixelSize = iconSizes[dpiSuffix];
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
});
}

View File

@ -32,7 +32,7 @@ class KubectlDownloader {
method: "HEAD",
uri: this.url,
resolveWithFullResponse: true,
}).catch(console.error);
});
if (response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, "");

View File

@ -1,6 +1,6 @@
{
"name": "kube-object-event-status",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "kube-object-event-status",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"description": "Adds kube object status from events",
"renderer": "dist/renderer.js",
"lens": {

View File

@ -1,6 +1,6 @@
{
"name": "lens-metrics-cluster-feature",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "lens-metrics-cluster-feature",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"description": "Lens metrics cluster feature",
"renderer": "dist/renderer.js",
"lens": {

View File

@ -71,7 +71,7 @@ export class MetricsFeature {
return this.stack.kubectlApplyFolder(this.resourceFolder, config, ["--prune"]);
}
async upgrade(config: MetricsConfiguration): Promise<string> {
upgrade(config: MetricsConfiguration): Promise<string> {
return this.install(config);
}
@ -101,7 +101,7 @@ export class MetricsFeature {
return status;
}
async uninstall(config: MetricsConfiguration): Promise<string> {
uninstall(config: MetricsConfiguration): Promise<string> {
return this.stack.kubectlDeleteFolder(this.resourceFolder, config);
}
}

View File

@ -156,17 +156,17 @@ export class MetricsSettings extends React.Component<Props> {
}
}
async togglePrometheus(enabled: boolean) {
togglePrometheus(enabled: boolean) {
this.featureStates.prometheus = enabled;
this.changed = true;
}
async toggleKubeStateMetrics(enabled: boolean) {
toggleKubeStateMetrics(enabled: boolean) {
this.featureStates.kubeStateMetrics = enabled;
this.changed = true;
}
async toggleNodeExporter(enabled: boolean) {
toggleNodeExporter(enabled: boolean) {
this.featureStates.nodeExporter = enabled;
this.changed = true;
}

View File

@ -1,6 +1,6 @@
{
"name": "lens-node-menu",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "lens-node-menu",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"description": "Lens node menu",
"renderer": "dist/renderer.js",
"lens": {

View File

@ -1,6 +1,6 @@
{
"name": "lens-pod-menu",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "lens-pod-menu",
"version": "0.0.1",
"version": "5.3.0-latest.1642433271626.1642519794031",
"description": "Lens pod menu",
"renderer": "dist/renderer.js",
"lens": {

View File

@ -55,7 +55,7 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
title: `Pod: ${pod.getName()} (namespace: ${pod.getNs()}) [Attached]`,
});
terminalStore.sendCommand(commandParts.join(" "), {
await terminalStore.sendCommand(commandParts.join(" "), {
enter: true,
tabId: shell.id,
});

View File

@ -63,7 +63,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
title: `Pod: ${pod.getName()} (namespace: ${pod.getNs()})`,
});
terminalStore.sendCommand(commandParts.join(" "), {
await terminalStore.sendCommand(commandParts.join(" "), {
enter: true,
tabId: shell.id,
});

View File

@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "5.3.0",
"version": "5.3.4",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
@ -62,7 +62,8 @@
},
"moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
"^monaco-editor": "<rootDir>/node_modules/monaco-editor"
},
"modulePathIgnorePatterns": [
"<rootDir>/dist",
@ -195,8 +196,8 @@
"@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "3.1.1",
"@ogre-tools/injectable-react": "3.1.1",
"@ogre-tools/injectable": "3.2.0",
"@ogre-tools/injectable-react": "3.2.0",
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0",
"@types/circular-dependency-plugin": "5.0.4",

View File

@ -10,7 +10,7 @@ import { readFileSync } from "fs";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
from "../app-paths/directory-for-user-data.injectable";
jest.mock("electron", () => ({
ipcMain: {
@ -59,7 +59,7 @@ class TestStore extends BaseStore<TestStoreModel> {
super.onSync(data);
}
async saveToFile(model: TestStoreModel) {
saveToFile(model: TestStoreModel) {
return super.saveToFile(model);
}
@ -85,7 +85,6 @@ describe("BaseStore", () => {
await dis.runSetups();
store = undefined;
TestStore.resetInstance();
const mockOpts = {
"some-user-data-directory": {
@ -95,12 +94,11 @@ describe("BaseStore", () => {
mockFs(mockOpts);
store = TestStore.createInstance();
store = new TestStore();
});
afterEach(() => {
store.disableSync();
TestStore.resetInstance();
mockFs.restore();
});

View File

@ -64,26 +64,26 @@ describe("CatalogCategoryRegistry", () => {
registry.add(new TestCatalogCategory2());
expect(registry.items.length).toBe(2);
expect(registry.filteredItems.length).toBe(2);
expect(registry.filteredItems.get().length).toBe(2);
const disposer = registry.addCatalogCategoryFilter(category => category.metadata.name === "Test Category");
expect(registry.items.length).toBe(2);
expect(registry.filteredItems.length).toBe(1);
expect(registry.filteredItems.get().length).toBe(1);
const disposer2 = registry.addCatalogCategoryFilter(category => category.metadata.name === "foo");
expect(registry.items.length).toBe(2);
expect(registry.filteredItems.length).toBe(0);
expect(registry.filteredItems.get().length).toBe(0);
disposer();
expect(registry.items.length).toBe(2);
expect(registry.filteredItems.length).toBe(0);
expect(registry.filteredItems.get().length).toBe(0);
disposer2();
expect(registry.items.length).toBe(2);
expect(registry.filteredItems.length).toBe(2);
expect(registry.filteredItems.get().length).toBe(2);
});
});

View File

@ -8,11 +8,11 @@ import mockFs from "mock-fs";
import path from "path";
import fse from "fs-extra";
import type { Cluster } from "../cluster/cluster";
import { ClusterStore } from "../cluster-store/cluster-store";
import type { ClusterStore } from "../cluster-store/store";
import { Console } from "console";
import { stdout, stderr } from "process";
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/store.injectable";
import type { ClusterModel } from "../cluster-types";
import type {
DependencyInjectionContainer,
@ -23,7 +23,7 @@ import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
from "../app-paths/directory-for-user-data.injectable";
console = new Console(stdout, stderr);
@ -100,15 +100,11 @@ describe("cluster-store", () => {
describe("empty config", () => {
let getCustomKubeConfigDirectory: (directoryName: string) => string;
beforeEach(async () => {
beforeEach(() => {
getCustomKubeConfigDirectory = mainDi.inject(
getCustomKubeConfigDirectoryInjectable,
);
// TODO: Remove these by removing Singleton base-class from BaseStore
ClusterStore.getInstance(false)?.unregisterIpcListener();
ClusterStore.resetInstance();
const mockOpts = {
"some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({}),
@ -143,7 +139,7 @@ describe("cluster-store", () => {
clusterStore.addCluster(cluster);
});
it("adds new cluster to store", async () => {
it("adds new cluster to store", () => {
const storedCluster = clusterStore.getById("foo");
expect(storedCluster.id).toBe("foo");
@ -197,8 +193,6 @@ describe("cluster-store", () => {
describe("config with existing clusters", () => {
beforeEach(() => {
ClusterStore.resetInstance();
const mockOpts = {
"temp-kube-config": kubeconfig,
"some-directory-for-user-data": {
@ -251,7 +245,7 @@ describe("cluster-store", () => {
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
it("allows getting all of the clusters", async () => {
it("allows getting all of the clusters", () => {
const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(3);
@ -285,8 +279,6 @@ users:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
ClusterStore.resetInstance();
const mockOpts = {
"invalid-kube-config": invalidKubeconfig,
"valid-kube-config": kubeconfig,
@ -335,7 +327,6 @@ users:
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
const mockOpts = {
"some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({
@ -368,13 +359,13 @@ users:
mockFs.restore();
});
it("migrates to modern format with kubeconfig in a file", async () => {
it("migrates to modern format with kubeconfig in a file", () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", async () => {
it("migrates to modern format with icon not in file", () => {
const { icon } = clusterStore.clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true);

View File

@ -1,416 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { anyObject } from "jest-mock-extended";
import { merge } from "lodash";
import mockFs from "mock-fs";
import logger from "../../main/logger";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { HotbarStore } from "../hotbar-store";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: {
items: [
{
metadata: {
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
name: "mycluster",
source: "local",
},
},
{
metadata: {
uid: "55b42c3c7ba3b04193416cda405269a5",
name: "my_shiny_cluster",
source: "remote",
},
},
{
metadata: {
uid: "catalog-entity",
name: "Catalog",
source: "app",
},
},
],
},
}));
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return merge(data, {
getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
isEnabled: jest.fn(() => data.status?.enabled ?? true),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {},
spec: {},
status: {},
}) as CatalogEntity;
}
const testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "test",
name: "test",
labels: {},
},
});
const minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "minikube",
name: "minikube",
labels: {},
},
});
const awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "aws",
name: "aws",
labels: {},
},
});
describe("HotbarStore", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
mockFs({
"some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({}),
},
});
HotbarStore.createInstance();
});
afterEach(() => {
HotbarStore.resetInstance();
mockFs.restore();
});
describe("load", () => {
it("loads one hotbar by default", () => {
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
});
});
describe("add", () => {
it("adds a hotbar", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.add({ name: "hottest" });
expect(hotbarStore.hotbars.length).toEqual(2);
});
});
describe("hotbar items", () => {
it("initially creates 12 empty cells", () => {
const hotbarStore = HotbarStore.getInstance();
expect(hotbarStore.getActive().items.length).toEqual(12);
});
it("initially adds catalog entity as first item", () => {
const hotbarStore = HotbarStore.getInstance();
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
});
it("adds items", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("removes items", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("test");
hotbarStore.removeFromHotbar("catalog-entity");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items).toStrictEqual([]);
});
it("does nothing if removing with invalid uid", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("invalid uid");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("moves item to empty cell", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
expect(hotbarStore.getActive().items[6]).toBeNull();
hotbarStore.restackItems(1, 5);
expect(hotbarStore.getActive().items[5]).toBeTruthy();
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test");
});
it("moves items down", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// aws -> catalog
hotbarStore.restackItems(3, 0);
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]);
});
it("moves items up", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// test -> aws
hotbarStore.restackItems(1, 3);
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
});
it("logs an error if cellIndex is out of bounds", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.setActiveHotbar("hottest");
const { error } = logger;
const mocked = jest.fn();
logger.error = mocked;
hotbarStore.addToHotbar(testCluster, -1);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 12);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 13);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
logger.error = error;
});
it("throws an error if getId is invalid or returns not a string", () => {
const hotbarStore = HotbarStore.getInstance();
expect(() => hotbarStore.addToHotbar({} as any)).toThrowError(TypeError);
expect(() => hotbarStore.addToHotbar({ getId: () => true } as any)).toThrowError(TypeError);
});
it("throws an error if getName is invalid or returns not a string", () => {
const hotbarStore = HotbarStore.getInstance();
expect(() => hotbarStore.addToHotbar({ getId: () => "" } as any)).toThrowError(TypeError);
expect(() => hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError);
});
it("does nothing when item moved to same cell", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1);
expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test");
});
it("new items takes first empty cell", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikubeCluster);
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube");
});
it("throws if invalid arguments provided", () => {
// Prevent writing to stderr during this render.
const { error, warn } = console;
console.error = jest.fn();
console.warn = jest.fn();
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
expect(() => hotbarStore.restackItems(2, -1)).toThrow();
expect(() => hotbarStore.restackItems(14, 1)).toThrow();
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
// Restore writing to stderr.
console.error = error;
console.warn = warn;
});
it("checks if entity already pinned to hotbar", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy();
expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy();
});
});
describe("pre beta-5 migrations", () => {
beforeEach(() => {
HotbarStore.resetInstance();
const mockOpts = {
"some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({
__internal__: {
migrations: {
version: "5.0.0-beta.3",
},
},
"hotbars": [
{
"id": "3caac17f-aec2-4723-9694-ad204465d935",
"name": "myhotbar",
"items": [
{
"entity": {
"uid": "1dfa26e2ebab15780a3547e9c7fa785c",
},
},
{
"entity": {
"uid": "55b42c3c7ba3b04193416cda405269a5",
},
},
{
"entity": {
"uid": "176fd331968660832f62283219d7eb6e",
},
},
{
"entity": {
"uid": "61c4fb45528840ebad1badc25da41d14",
"name": "user1-context",
"source": "local",
},
},
{
"entity": {
"uid": "27d6f99fe9e7548a6e306760bfe19969",
"name": "foo2",
"source": "local",
},
},
null,
{
"entity": {
"uid": "c0b20040646849bb4dcf773e43a0bf27",
"name": "multinode-demo",
"source": "local",
},
},
null,
null,
null,
null,
null,
],
},
],
}),
},
};
mockFs(mockOpts);
HotbarStore.createInstance();
});
afterEach(() => {
mockFs.restore();
});
it("allows to retrieve a hotbar", () => {
const hotbar = HotbarStore.getInstance().getById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
});
it("clears cells without entity", () => {
const items = HotbarStore.getInstance().hotbars[0].items;
expect(items[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = HotbarStore.getInstance().hotbars[0].items;
expect(items[0]).toEqual({
entity: {
name: "mycluster",
source: "local",
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
},
});
expect(items[1]).toEqual({
entity: {
name: "my_shiny_cluster",
source: "remote",
uid: "55b42c3c7ba3b04193416cda405269a5",
},
});
});
});
});

View File

@ -151,13 +151,13 @@ describe("kube helpers", () => {
};
});
it("single context is ok", async () => {
it("single context is ok", () => {
const { config } = loadConfigFromString(JSON.stringify(mockKubeConfig));
expect(config.getCurrentContext()).toBe("minikube");
});
it("multiple context is ok", async () => {
it("multiple context is ok", () => {
mockKubeConfig.contexts.push({ context: { cluster: "cluster-2", user: "cluster-2" }, name: "cluster-2" });
const { config } = loadConfigFromString(JSON.stringify(mockKubeConfig));
@ -192,7 +192,7 @@ describe("kube helpers", () => {
};
});
it("empty name in context causes it to be removed", async () => {
it("empty name in context causes it to be removed", () => {
mockKubeConfig.contexts.push({ context: { cluster: "cluster-2", user: "cluster-2" }, name: "" });
expect(mockKubeConfig.contexts.length).toBe(2);
const { config } = loadConfigFromString(JSON.stringify(mockKubeConfig));
@ -201,7 +201,7 @@ describe("kube helpers", () => {
expect(config.contexts.length).toBe(1);
});
it("empty cluster in context causes it to be removed", async () => {
it("empty cluster in context causes it to be removed", () => {
mockKubeConfig.contexts.push({ context: { cluster: "", user: "cluster-2" }, name: "cluster-2" });
expect(mockKubeConfig.contexts.length).toBe(2);
const { config } = loadConfigFromString(JSON.stringify(mockKubeConfig));
@ -210,7 +210,7 @@ describe("kube helpers", () => {
expect(config.contexts.length).toBe(1);
});
it("empty user in context causes it to be removed", async () => {
it("empty user in context causes it to be removed", () => {
mockKubeConfig.contexts.push({ context: { cluster: "cluster-2", user: "" }, name: "cluster-2" });
expect(mockKubeConfig.contexts.length).toBe(2);
const { config } = loadConfigFromString(JSON.stringify(mockKubeConfig));
@ -219,7 +219,7 @@ describe("kube helpers", () => {
expect(config.contexts.length).toBe(1);
});
it("invalid context in between valid contexts is removed", async () => {
it("invalid context in between valid contexts is removed", () => {
mockKubeConfig.contexts.push({ context: { cluster: "cluster-2", user: "" }, name: "cluster-2" });
mockKubeConfig.contexts.push({ context: { cluster: "cluster-3", user: "cluster-3" }, name: "cluster-3" });
expect(mockKubeConfig.contexts.length).toBe(3);

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { LogSearchStore } from "../../renderer/components/dock/log-search/store";
const logs = [
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
"1:M 30 Oct 2020 16:17:41.623 * Starting Partial resynchronization request from 172.17.0.12:6379 accepted. Sending 0 bytes of backlog starting from offset 14407.",
];
describe("search store tests", () => {
let logSearchStore: LogSearchStore;
beforeEach(() => {
logSearchStore = new LogSearchStore();
});
it("does nothing with empty search query", () => {
logSearchStore.onSearch([], "");
expect(logSearchStore.occurrences).toEqual([]);
});
it("doesn't break if no text provided", () => {
logSearchStore.onSearch(null, "replica");
expect(logSearchStore.occurrences).toEqual([]);
logSearchStore.onSearch([], "replica");
expect(logSearchStore.occurrences).toEqual([]);
});
it("find 3 occurrences across 3 lines", () => {
logSearchStore.onSearch(logs, "172");
expect(logSearchStore.occurrences).toEqual([0, 1, 2]);
});
it("find occurrences within 1 line (case-insensitive)", () => {
logSearchStore.onSearch(logs, "Starting");
expect(logSearchStore.occurrences).toEqual([2, 2]);
});
it("sets overlay index equal to first occurrence", () => {
logSearchStore.onSearch(logs, "Replica");
expect(logSearchStore.activeOverlayIndex).toBe(0);
});
it("set overlay index to next occurrence", () => {
logSearchStore.onSearch(logs, "172");
logSearchStore.setNextOverlayActive();
expect(logSearchStore.activeOverlayIndex).toBe(1);
});
it("sets overlay to last occurrence", () => {
logSearchStore.onSearch(logs, "172");
logSearchStore.setPrevOverlayActive();
expect(logSearchStore.activeOverlayIndex).toBe(2);
});
it("gets line index where overlay is located", () => {
logSearchStore.onSearch(logs, "synchronization");
expect(logSearchStore.activeOverlayLine).toBe(1);
});
it("escapes string for using in regex", () => {
const regex = LogSearchStore.escapeRegex("some.interesting-query\\#?()[]");
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
});
it("gets active find number", () => {
logSearchStore.onSearch(logs, "172");
logSearchStore.setNextOverlayActive();
expect(logSearchStore.activeFind).toBe(2);
});
it("gets total finds number", () => {
logSearchStore.onSearch(logs, "Starting");
expect(logSearchStore.totalFinds).toBe(2);
});
});

View File

@ -4,6 +4,17 @@
*/
import mockFs from "mock-fs";
import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import userPreferencesStoreInjectable from "../user-preferences/store.injectable";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data.injectable";
import type { ClusterStoreModel } from "../cluster-store/store";
import { defaultTheme } from "../vars";
import type { UserPreferencesStore } from "../user-preferences";
jest.mock("electron", () => ({
app: {
@ -21,22 +32,10 @@ jest.mock("electron", () => ({
},
}));
import { UserStore } from "../user-store";
import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
import { defaultTheme } from "../vars";
console = new Console(stdout, stderr);
describe("user store tests", () => {
let userStore: UserStore;
let userStore: UserPreferencesStore;
let mainDi: DependencyInjectionContainer;
beforeEach(async () => {
@ -59,12 +58,11 @@ describe("user store tests", () => {
beforeEach(() => {
mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }});
userStore = mainDi.inject(userStoreInjectable);
userStore = mainDi.inject(userPreferencesStoreInjectable);
});
afterEach(() => {
mockFs.restore();
UserStore.resetInstance();
});
it("allows setting and retrieving lastSeenAppVersion", () => {
@ -82,7 +80,7 @@ describe("user store tests", () => {
expect(userStore.colorTheme).toBe("light");
});
it("correctly resets theme to default value", async () => {
it("correctly resets theme to default value", () => {
userStore.colorTheme = "some other theme";
userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultTheme);
@ -126,11 +124,10 @@ describe("user store tests", () => {
},
});
userStore = mainDi.inject(userStoreInjectable);
userStore = mainDi.inject(userPreferencesStoreInjectable);
});
afterEach(() => {
UserStore.resetInstance();
mockFs.restore();
});

View File

@ -4,7 +4,7 @@
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable from "./directory-for-user-data.injectable";
const directoryForBinariesInjectable = getInjectable({
instantiate: (di) =>

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
import { appPathsInjectionToken } from "./app-path-injection-token";
const directoryForDownloadsInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).downloads,

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
import { appPathsInjectionToken } from "./app-path-injection-token";
const directoryForExesInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).exe,

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable from "./directory-for-user-data.injectable";
import path from "path";
const directoryForKubeConfigsInjectable = getInjectable({

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
import { appPathsInjectionToken } from "./app-path-injection-token";
const directoryForTempInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).temp,

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
import { appPathsInjectionToken } from "./app-path-injection-token";
const directoryForUserDataInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).userData,

View File

@ -4,12 +4,12 @@
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForKubeConfigsInjectable from "./directory-for-kube-configs.injectable";
const getCustomKubeConfigDirectoryInjectable = getInjectable({
instantiate: (di) => (directoryName: string) => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
return path.resolve(
directoryForKubeConfigs,
directoryName,

View File

@ -8,15 +8,14 @@ import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types";
import { ipcMain, ipcRenderer } from "electron";
import { IEqualsComparer, makeObservable, reaction, runInAction } from "mobx";
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
import { getAppVersion, toJS, Disposer } from "./utils";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars";
import { kebabCase } from "lodash";
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable
from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: {
@ -28,14 +27,13 @@ export interface BaseStoreParams<T> extends ConfOptions<T> {
/**
* Note: T should only contain base JSON serializable types.
*/
export abstract class BaseStore<T> extends Singleton {
export abstract class BaseStore<T> {
protected storeConfig?: Config<T>;
protected syncDisposers: Disposer[] = [];
readonly displayName: string = this.constructor.name;
protected constructor(protected params: BaseStoreParams<T>) {
super();
makeObservable(this);
if (ipcRenderer) {

View File

@ -2,9 +2,15 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { kubernetesClusterCategory } from "../kubernetes-cluster";
import { KubernetesClusterCategory } from "../kubernetes-cluster";
describe("kubernetesClusterCategory", () => {
let kubernetesClusterCategory: KubernetesClusterCategory;
beforeEach(() => {
kubernetesClusterCategory = new KubernetesClusterCategory();
});
describe("filteredItems", () => {
const item1 = {
icon: "Icon",

View File

@ -5,7 +5,6 @@
import { navigate } from "../../renderer/navigation";
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
interface GeneralEntitySpec extends CatalogEntitySpec {
path: string;
@ -19,21 +18,9 @@ export class GeneralEntity extends CatalogEntity<CatalogEntityMetadata, CatalogE
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "General";
async onRun() {
onRun() {
navigate(this.spec.path);
}
public onSettingsOpen(): void {
return;
}
public onDetailsOpen(): void {
return;
}
public onContextMenuOpen(): void {
return;
}
}
export class GeneralCategory extends CatalogCategory {
@ -56,5 +43,3 @@ export class GeneralCategory extends CatalogCategory {
},
};
}
catalogCategoryRegistry.add(new GeneralCategory());

View File

@ -3,12 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage, requestMain } from "../ipc";
import { app } from "electron";
import { app, ipcMain } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
@ -67,7 +65,8 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
async connect(): Promise<void> {
if (app) {
await ClusterStore.getInstance().getById(this.metadata.uid)?.activate();
// TODO refactor
ipcMain.emit(clusterActivateHandler, this.metadata.uid, false);
} else {
await requestMain(clusterActivateHandler, this.metadata.uid, false);
}
@ -75,25 +74,17 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
async disconnect(): Promise<void> {
if (app) {
ClusterStore.getInstance().getById(this.metadata.uid)?.disconnect();
ipcMain.emit(clusterDisconnectHandler, this.metadata.uid, false);
} else {
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
}
}
async onRun(context: CatalogEntityActionContext) {
onRun(context: CatalogEntityActionContext) {
context.navigate(`/cluster/${this.metadata.uid}`);
}
onDetailsOpen(): void {
//
}
onSettingsOpen(): void {
//
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (!this.metadata.source || this.metadata.source === "local") {
context.menuItems.push({
title: "Settings",
@ -122,14 +113,10 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
});
break;
}
catalogCategoryRegistry
.getCategoryForEntity<KubernetesClusterCategory>(this)
?.emit("contextMenuOpen", this, context);
}
}
class KubernetesClusterCategory extends CatalogCategory {
export class KubernetesClusterCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
@ -149,7 +136,3 @@ class KubernetesClusterCategory extends CatalogCategory {
},
};
}
export const kubernetesClusterCategory = new KubernetesClusterCategory();
catalogCategoryRegistry.add(kubernetesClusterCategory);

View File

@ -3,10 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { CatalogCategory, CatalogEntity, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { productName } from "../vars";
import { WeblinkStore } from "../weblink-store";
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
export type WebLinkStatusPhase = "available" | "unavailable";
@ -14,9 +11,9 @@ export interface WebLinkStatus extends CatalogEntityStatus {
phase: WebLinkStatusPhase;
}
export type WebLinkSpec = {
export interface WebLinkSpec {
url: string;
};
}
export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
@ -25,30 +22,9 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
public readonly apiVersion = WebLink.apiVersion;
public readonly kind = WebLink.kind;
async onRun() {
onRun() {
window.open(this.spec.url, "_blank");
}
public onSettingsOpen(): void {
return;
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (this.metadata.source === "local") {
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: async () => WeblinkStore.getInstance().removeById(this.metadata.uid),
confirm: {
message: `Remove Web Link "${this.metadata.name}" from ${productName}?`,
},
});
}
catalogCategoryRegistry
.getCategoryForEntity<WebLinkCategory>(this)
?.emit("contextMenuOpen", this, context);
}
}
export class WebLinkCategory extends CatalogCategory {
@ -71,5 +47,3 @@ export class WebLinkCategory extends CatalogCategory {
},
};
}
catalogCategoryRegistry.add(new WebLinkCategory());

View File

@ -5,7 +5,7 @@
import { action, computed, observable, makeObservable } from "mobx";
import { Disposer, ExtendedMap, iter } from "../utils";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { once } from "lodash";
export type CategoryFilter = (category: CatalogCategory) => any;
@ -37,22 +37,19 @@ export class CatalogCategoryRegistry {
return Array.from(this.categories);
}
@computed get filteredItems() {
return Array.from(
iter.reduce(
this.filters,
iter.filter,
this.items.values(),
),
);
readonly filteredItems = computed(() => Array.from(
iter.reduce(
this.filters,
iter.filter,
this.categories.values(),
),
));
getForGroupKind(group: string, kind: string): CatalogCategory | undefined {
return this.groupKinds.get(group)?.get(kind);
}
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
return this.groupKinds.get(group)?.get(kind) as T;
}
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
getEntityForData = (data: CatalogEntityData & CatalogEntityKindData): CatalogEntity | null => {
const category = this.getCategoryForEntity(data);
if (!category) {
@ -69,18 +66,23 @@ export class CatalogCategoryRegistry {
}
return new specVersion.entityClass(data);
}
};
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
getCategoryForEntity = <T extends CatalogCategory>({ kind, apiVersion }: CatalogEntityData & CatalogEntityKindData): T => {
const group = apiVersion.split("/")[0];
const category = this.getForGroupKind(group, kind);
return this.getForGroupKind(group, data.kind);
}
if (!category) {
// Throw here because it is very important that this is always true
throw new Error(`Unable to find a category for group=${group} kind=${kind}`);
}
getByName(name: string) {
return category as T;
};
getByName = (name: string) => {
return this.items.find(category => category.metadata?.name == name);
}
};
/**
* Add a new filter to the set of category filters
@ -93,5 +95,3 @@ export class CatalogCategoryRegistry {
return once(() => void this.filters.delete(fn));
}
}
export const catalogCategoryRegistry = new CatalogCategoryRegistry();

View File

@ -351,7 +351,7 @@ export abstract class CatalogEntity<
return this.status.enabled ?? true;
}
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
public onRun?(context: CatalogEntityActionContext): void;
public onContextMenuOpen?(context: CatalogEntityContextMenuContext): void;
public onSettingsOpen?(context: CatalogEntitySettingsContext): void;
}

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import clusterStoreInjectable from "./store.injectable";
const getClusterByIdInjectable = getInjectable({
instantiate: (di) => di.inject(clusterStoreInjectable).getById,
lifecycle: lifecycleEnum.singleton,
});
export default getClusterByIdInjectable;

View File

@ -4,15 +4,10 @@
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { getHostedClusterId } from "../../utils";
import clusterStoreInjectable from "../cluster-store.injectable";
import clusterStoreInjectable from "../store.injectable";
const hostedClusterInjectable = getInjectable({
instantiate: (di) => {
const hostedClusterId = getHostedClusterId();
return di.inject(clusterStoreInjectable).getById(hostedClusterId);
},
instantiate: (di) => di.inject(clusterStoreInjectable).getById(getHostedClusterId()),
lifecycle: lifecycleEnum.singleton,
});

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { Migrations } from "conf/dist/source/types";
import type { ClusterStoreModel } from "./store";
export const clusterStoreMigrationsInjectionToken = getInjectionToken<Migrations<ClusterStoreModel> | undefined>();

View File

@ -3,14 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store";
import { ClusterStore } from "./store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import { clusterStoreMigrationsInjectionToken } from "./migrations-injection-token";
const clusterStoreInjectable = getInjectable({
instantiate: (di) =>
ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken),
}),
instantiate: (di) => new ClusterStore({
createCluster: di.inject(createClusterInjectionToken),
migrations: di.inject(clusterStoreMigrationsInjectionToken),
}),
lifecycle: lifecycleEnum.singleton,
});

View File

@ -2,18 +2,18 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "../base-store";
import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle, requestMain } from "../ipc";
import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
import type { Migrations } from "conf/dist/source/types";
export interface ClusterStoreModel {
clusters?: ClusterModel[];
@ -22,7 +22,8 @@ export interface ClusterStoreModel {
const initialStates = "cluster:states";
interface Dependencies {
createCluster: (model: ClusterModel) => Cluster
createCluster: (model: ClusterModel) => Cluster;
migrations: Migrations<ClusterStoreModel>;
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
@ -38,7 +39,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
syncOptions: {
equals: comparer.structural,
},
migrations,
migrations: dependencies.migrations,
});
makeObservable(this);
@ -103,9 +104,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.size > 0;
}
getById(id: ClusterId): Cluster | null {
getById = (id: ClusterId): Cluster | null => {
return this.clusters.get(id) ?? null;
}
};
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" });

View File

@ -13,19 +13,20 @@ import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../rbac";
import logger from "../../main/logger";
import { VersionDetector } from "../../main/cluster-detectors/version-detector";
import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import { disposer, toJS } from "../utils";
import type { Response } from "request";
import type { ClusterDetectionResult } from "../../main/cluster-detectors/base-cluster-detector";
interface Dependencies {
directoryForKubeConfigs: string,
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager,
createContextHandler: (cluster: Cluster) => ContextHandler,
createKubectl: (clusterVersion: string) => Kubectl
export interface ClusterDependencies {
directoryForKubeConfigs: string;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
createContextHandler: (cluster: Cluster) => ContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
detectMetadataForCluster: (cluster: Cluster) => Promise<ClusterMetadata>;
detectVersion: (cluster: Cluster) => Promise<ClusterDetectionResult>;
}
/**
@ -212,7 +213,11 @@ export class Cluster implements ClusterModel, ClusterState {
return this.preferences.defaultNamespace;
}
constructor(private dependencies: Dependencies, model: ClusterModel) {
static create(...args: ConstructorParameters<typeof Cluster>) {
return new Cluster(...args);
}
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel) {
makeObservable(this);
this.id = model.id;
this.updateModel(model);
@ -414,7 +419,7 @@ export class Cluster implements ClusterModel, ClusterState {
@action
async refreshMetadata() {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await DetectorRegistry.getInstance().detectForCluster(this);
const metadata = await this.dependencies.detectMetadataForCluster(this);
const existingMetadata = this.metadata;
this.metadata = Object.assign(existingMetadata, metadata);
@ -461,16 +466,15 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
async getProxyKubeconfigPath(): Promise<string> {
getProxyKubeconfigPath(): Promise<string> {
return this.proxyKubeconfigManager.getPath();
}
protected async getConnectionStatus(): Promise<ClusterStatus> {
try {
const versionDetector = new VersionDetector(this);
const versionData = await versionDetector.detect();
const { value } = await this.dependencies.detectVersion(this);
this.metadata.version = versionData.value;
this.metadata.version = value;
return ClusterStatus.AccessGranted;
} catch (error) {
@ -531,7 +535,7 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
async isClusterAdmin(): Promise<boolean> {
isClusterAdmin(): Promise<boolean> {
return this.canI({
namespace: "kube-system",
resource: "*",
@ -542,7 +546,7 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
return this.canI({
verb: "watch",
resource: "*",
@ -697,9 +701,9 @@ export class Cluster implements ClusterModel, ClusterState {
return true; // allowed by default for other resources
}
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
isMetricHidden = (resource: ClusterMetricsResourceType): boolean => {
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
}
};
get nodeShellImage(): string {
return this.preferences?.nodeShellImage || initialNodeShellImage;

View File

@ -6,5 +6,4 @@ import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterModel } from "../cluster-types";
import type { Cluster } from "./cluster";
export const createClusterInjectionToken =
getInjectionToken<(model: ClusterModel) => Cluster>();
export const createClusterInjectionToken = getInjectionToken<(model: ClusterModel) => Cluster>();

View File

@ -4,7 +4,7 @@
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data.injectable";
const directoryForLensLocalStorageInjectable = getInjectable({
instantiate: (di) =>

View File

@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { HotbarStore } from "./hotbar-store";
import fsInjectable from "./fs.injectable";
const hotbarManagerInjectable = getInjectable({
instantiate: () => HotbarStore.getInstance(),
const readDirInjectable = getInjectable({
instantiate: (di) => di.inject(fsInjectable).readdir,
lifecycle: lifecycleEnum.singleton,
});
export default hotbarManagerInjectable;
export default readDirInjectable;

View File

@ -2,12 +2,12 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiManager } from "../../../../common/k8s-api/api-manager";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
const apiManagerInjectable = getInjectable({
instantiate: () => apiManager,
const readFileInjectable = getInjectable({
instantiate: (di) => di.inject(fsInjectable).readFile,
lifecycle: lifecycleEnum.singleton,
});
export default apiManagerInjectable;
export default readFileInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
const readJsonFileInjectable = getInjectable({
instantiate: (di) => di.inject(fsInjectable).readJson,
lifecycle: lifecycleEnum.singleton,
});
export default readJsonFileInjectable;

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { UserStore } from "./user-store";
const userStoreInjectable = getInjectable({
instantiate: () => UserStore.createInstance(),
import { watch } from "chokidar";
const watchFilePathInjectable = getInjectable({
instantiate: () => watch,
lifecycle: lifecycleEnum.singleton,
});
export default userStoreInjectable;
export default watchFilePathInjectable;

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { EnsureOptions, WriteOptions } from "fs-extra";
import path from "path";
import type { JsonValue } from "type-fest";
import { bind } from "../utils";
import fsInjectable from "./fs.injectable";
interface Dependencies {
writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise<void>;
ensureDir: (dir: string, options?: EnsureOptions | number) => Promise<void>;
}
async function writeJsonFile({ writeJson, ensureDir }: Dependencies, filePath: string, content: JsonValue, options?: WriteOptions | BufferEncoding) {
await ensureDir(path.dirname(filePath), { mode: 0o755 });
const resolvedOptions = typeof options === "string"
? {
encoding: options,
}
: options;
await writeJson(filePath, content, {
encoding: "utf-8",
spaces: 2,
...resolvedOptions,
});
}
const writeJsonFileInjectable = getInjectable({
instantiate: (di) => {
const { writeJson, ensureDir } = di.inject(fsInjectable);
return bind(writeJsonFile, null, {
writeJson,
ensureDir,
});
},
lifecycle: lifecycleEnum.singleton,
});
export default writeJsonFileInjectable;

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import glob from "glob";
import { memoize } from "lodash/fp";
import {
createContainer,
ConfigurableDependencyInjectionContainer,
} from "@ogre-tools/injectable";
export const getDiForUnitTesting = () => {
const di: ConfigurableDependencyInjectionContainer = createContainer();
getInjectableFilePaths()
.map(key => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const injectable = require(key).default;
return {
id: key,
...injectable,
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();
return di;
};
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);

View File

@ -0,0 +1,207 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { anyObject } from "jest-mock-extended";
import { merge } from "lodash";
import type { CatalogEntityData, CatalogEntityKindData, CatalogEntity } from "../../catalog";
import type { LensLogger } from "../../logger";
import { Hotbar } from "../hotbar";
import { getEmptyHotbar } from "../hotbar-types";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return merge(data, {
getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
isEnabled: jest.fn(() => data.status?.enabled ?? true),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {},
spec: {},
status: {},
}) as CatalogEntity;
}
const testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "test",
name: "test",
labels: {},
},
});
const minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "minikube",
name: "minikube",
labels: {},
},
});
const awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "aws",
name: "aws",
labels: {},
},
});
describe("Hotbar", () => {
let hotbar: Hotbar;
let logger: LensLogger;
beforeEach(() => {
logger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
silly: jest.fn(),
warn: jest.fn(),
};
hotbar = new Hotbar(getEmptyHotbar("Default"), { logger });
});
it("adds items", () => {
hotbar.addItem(testCluster);
const items = hotbar.items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("removes items", () => {
hotbar.addItem(testCluster);
hotbar.removeItemById("test");
hotbar.removeItemById("catalog-entity");
const items = hotbar.items.filter(Boolean);
expect(items).toStrictEqual([]);
});
it("does nothing if removing with invalid uid", () => {
hotbar.addItem(testCluster);
hotbar.removeItemById("invalid uid");
const items = hotbar.items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("moves item to empty cell", () => {
hotbar.addItem(testCluster);
hotbar.addItem(minikubeCluster);
hotbar.addItem(awsCluster);
expect(hotbar.items[6]).toBeNull();
hotbar.restackItems(1, 5);
expect(hotbar.items[5]).toBeTruthy();
expect(hotbar.items[5].entity.uid).toEqual("test");
});
it("moves items down", () => {
hotbar.addItem(testCluster);
hotbar.addItem(minikubeCluster);
hotbar.addItem(awsCluster);
// aws -> catalog
hotbar.restackItems(3, 0);
const items = hotbar.items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]);
});
it("moves items up", () => {
hotbar.addItem(testCluster);
hotbar.addItem(minikubeCluster);
hotbar.addItem(awsCluster);
// test -> aws
hotbar.restackItems(1, 3);
const items = hotbar.items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
});
it("logs an error if cellIndex is out of bounds", () => {
hotbar.addItem(testCluster, -1);
expect(logger.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbar.addItem(testCluster, 12);
expect(logger.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbar.addItem(testCluster, 13);
expect(logger.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
});
it("throws an error if getId is invalid or returns not a string", () => {
expect(() => hotbar.addItem({} as any)).toThrowError(TypeError);
expect(() => hotbar.addItem({ getId: () => true } as any)).toThrowError(TypeError);
});
it("throws an error if getName is invalid or returns not a string", () => {
expect(() => hotbar.addItem({ getId: () => "" } as any)).toThrowError(TypeError);
expect(() => hotbar.addItem({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError);
});
it("does nothing when item moved to same cell", () => {
hotbar.addItem(testCluster);
hotbar.restackItems(1, 1);
expect(hotbar.items[1].entity.uid).toEqual("test");
});
it("new items takes first empty cell", () => {
hotbar.addItem(testCluster);
hotbar.addItem(awsCluster);
hotbar.restackItems(0, 3);
hotbar.addItem(minikubeCluster);
expect(hotbar.items[0].entity.uid).toEqual("minikube");
});
it("throws if invalid arguments provided", () => {
// Prevent writing to stderr during this render.
const { error, warn } = console;
console.error = jest.fn();
console.warn = jest.fn();
hotbar.addItem(testCluster);
expect(() => hotbar.restackItems(-5, 0)).toThrow();
expect(() => hotbar.restackItems(2, -1)).toThrow();
expect(() => hotbar.restackItems(14, 1)).toThrow();
expect(() => hotbar.restackItems(11, 112)).toThrow();
// Restore writing to stderr.
console.error = error;
console.warn = warn;
});
it("checks if entity already pinned to hotbar", () => {
hotbar.addItem(testCluster);
expect(hotbar.hasItem(testCluster)).toBeTruthy();
expect(hotbar.hasItem(awsCluster)).toBeFalsy();
});
});

View File

@ -0,0 +1,166 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import mockFs from "mock-fs";
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data.injectable";
import type { HotbarStore } from "../store";
describe("HotbarStore", () => {
let hotbarStore: HotbarStore;
let mainDi: DependencyInjectionContainer;
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
mainDi = dis.mainDi;
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await dis.runSetups();
});
beforeEach(() => {
mockFs({
"some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({}),
},
});
});
afterEach(() => {
mockFs.restore();
});
describe("load", () => {
it("loads one hotbar by default", () => {
expect(hotbarStore.hotbars.length).toEqual(1);
});
});
describe("add", () => {
it("adds a hotbar", () => {
hotbarStore.add({ name: "hottest" });
expect(hotbarStore.hotbars.length).toEqual(2);
});
});
describe("hotbar items", () => {
it("initially creates 12 empty cells", () => {
expect(hotbarStore.getActive().items.length).toEqual(12);
});
it("initially adds catalog entity as first item", () => {
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
});
});
describe("pre beta-5 migrations", () => {
beforeEach(() => {
const mockOpts = {
"some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({
__internal__: {
migrations: {
version: "5.0.0-beta.3",
},
},
"hotbars": [
{
"id": "3caac17f-aec2-4723-9694-ad204465d935",
"name": "myhotbar",
"items": [
{
"entity": {
"uid": "1dfa26e2ebab15780a3547e9c7fa785c",
},
},
{
"entity": {
"uid": "55b42c3c7ba3b04193416cda405269a5",
},
},
{
"entity": {
"uid": "176fd331968660832f62283219d7eb6e",
},
},
{
"entity": {
"uid": "61c4fb45528840ebad1badc25da41d14",
"name": "user1-context",
"source": "local",
},
},
{
"entity": {
"uid": "27d6f99fe9e7548a6e306760bfe19969",
"name": "foo2",
"source": "local",
},
},
null,
{
"entity": {
"uid": "c0b20040646849bb4dcf773e43a0bf27",
"name": "multinode-demo",
"source": "local",
},
},
null,
null,
null,
null,
null,
],
},
],
}),
},
};
mockFs(mockOpts);
});
afterEach(() => {
mockFs.restore();
});
it("allows to retrieve a hotbar", () => {
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
});
it("clears cells without entity", () => {
const items = hotbarStore.hotbars[0].items;
expect(items[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = hotbarStore.hotbars[0].items;
expect(items[0]).toEqual({
entity: {
name: "mycluster",
source: "local",
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
},
});
expect(items[1]).toEqual({
entity: {
name: "my_shiny_cluster",
source: "remote",
uid: "55b42c3c7ba3b04193416cda405269a5",
},
});
});
});
});

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import hotbarStoreInjectable from "./store.injectable";
const activeHotbarInjectable = getInjectable({
instantiate: (di) => {
const hotbarStore = di.inject(hotbarStoreInjectable);
return computed(() => hotbarStore.getActive());
},
lifecycle: lifecycleEnum.singleton,
});
export default activeHotbarInjectable;

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { CatalogEntity } from "../catalog";
import { bind } from "../utils";
import activeHotbarInjectable from "./active-hotbar.injectable";
import type { Hotbar } from "./hotbar";
interface Dependencies {
hotbar: IComputedValue<Hotbar>;
}
function addToActiveHotbar({ hotbar }: Dependencies, entity: CatalogEntity, cellIndex?: number) {
return hotbar.get().addItem(entity, cellIndex);
}
const addToActiveHotbarInjectable = getInjectable({
instantiate: (di) => bind(addToActiveHotbar, null, {
hotbar: di.inject(activeHotbarInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default addToActiveHotbarInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "./store.injectable";
const createNewHotbarInjectable = getInjectable({
instantiate: (di) => di.inject(hotbarStoreInjectable).add,
lifecycle: lifecycleEnum.singleton,
});
export default createNewHotbarInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "./store.injectable";
const getHotbarByNameInjectable = getInjectable({
instantiate: (di) => di.inject(hotbarStoreInjectable).getByName,
lifecycle: lifecycleEnum.singleton,
});
export default getHotbarByNameInjectable;

View File

@ -4,7 +4,7 @@
*/
import * as uuid from "uuid";
import { tuple, Tuple } from "./utils";
import { tuple, Tuple } from "../utils";
export interface HotbarItem {
entity: {
@ -17,8 +17,6 @@ export interface HotbarItem {
}
}
export type Hotbar = Required<CreateHotbarData>;
export interface CreateHotbarData {
id?: string;
name: string;
@ -31,7 +29,7 @@ export interface CreateHotbarOptions {
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Required<CreateHotbarData> {
return {
id,
items: tuple.filled(defaultHotbarCells, null),

View File

@ -0,0 +1,121 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action } from "mobx";
import type { CatalogEntity } from "../catalog";
import { broadcastMessage, HotbarTooManyItems } from "../ipc";
import type { LensLogger } from "../logger";
import type { Tuple } from "../utils";
import type { HotbarItem, defaultHotbarCells, CreateHotbarData } from "./hotbar-types";
export interface HotbarDependencies {
readonly logger: LensLogger;
}
export class Hotbar {
public readonly id: string;
public name: string;
public readonly items: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
constructor(data: Required<CreateHotbarData>, protected readonly dependencies: HotbarDependencies) {
this.id = data.id;
this.name = data.name;
this.items = data.items;
}
setName = (name: string) => {
this.name = name;
};
addItem = action((item: CatalogEntity, cellIndex?: number) => {
const uid = item.getId();
const name = item.getName();
if (typeof uid !== "string") {
throw new TypeError("item's id must be a string");
}
if (typeof name !== "string") {
throw new TypeError("item's name must be a string");
}
const newItem = { entity: {
uid,
name,
source: item.metadata.source,
}};
if (this.hasItem(item)) {
return;
}
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = this.items.indexOf(null);
if (emptyCellIndex != -1) {
this.items[emptyCellIndex] = newItem;
} else {
broadcastMessage(HotbarTooManyItems);
}
} else if (0 <= cellIndex && cellIndex < this.items.length) {
this.items[cellIndex] = newItem;
} else {
this.dependencies.logger.error(`[HOTBAR-${this.id}]: cannot pin entity to hotbar outside of index range`, { entityId: uid, cellIndex });
}
});
private findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
while(this.items[index] != null) {
index += direction;
}
return index;
}
/**
* Checks if entity already pinned to hotbar
* @returns boolean
*/
hasItem = (entity: CatalogEntity): boolean => {
return this.items.findIndex(item => item?.entity.uid === entity.metadata.uid) >= 0;
};
removeItemById = action((uid: string): void => {
const index = this.items.findIndex(item => item?.entity.uid === uid);
if (index < 0) {
return;
}
this.items[index] = null;
});
restackItems = action((from: number, to: number): void => {
const { items } = this;
const source = items[from];
const moveDown = from < to;
if (from < 0 || to < 0 || from >= items.length || to >= items.length || isNaN(from) || isNaN(to)) {
throw new Error("Invalid 'from' or 'to' arguments");
}
if (from == to) {
return;
}
items.splice(from, 1, null);
if (items[to] == null) {
items.splice(to, 1, source);
} else {
// Move cells up or down to closes empty cell
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
items.splice(to, 0, source);
}
});
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { CatalogEntity } from "../catalog";
import { bind } from "../utils";
import activeHotbarInjectable from "./active-hotbar.injectable";
import type { Hotbar } from "./hotbar";
interface Dependencies {
hotbar: IComputedValue<Hotbar>;
}
function isAddedToActiveHotbar({ hotbar }: Dependencies, entity: CatalogEntity) {
return hotbar.get().hasItem(entity);
}
const isItemInActiveHotbarInjectable = getInjectable({
instantiate: (di) => bind(isAddedToActiveHotbar, null, {
hotbar: di.inject(activeHotbarInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default isItemInActiveHotbarInjectable;

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { Migrations } from "conf/dist/source/types";
import type { HotbarStoreModel } from "./store";
export const hotbarStoreMigrationsInjectionToken = getInjectionToken<Migrations<HotbarStoreModel> | undefined>();

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "./store.injectable";
const removeAllHotbarItemsInjectable = getInjectable({
instantiate: (di) => di.inject(hotbarStoreInjectable).removeAllHotbarItems,
lifecycle: lifecycleEnum.singleton,
});
export default removeAllHotbarItemsInjectable;

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { bind } from "../utils";
import activeHotbarInjectable from "./active-hotbar.injectable";
import type { Hotbar } from "./hotbar";
interface Dependencies {
hotbar: IComputedValue<Hotbar>;
}
function removeByIdFromActiveHotbar({ hotbar }: Dependencies, id: string) {
return hotbar.get().removeItemById(id);
}
const removeByIdFromActiveHotbarInjectable = getInjectable({
instantiate: (di) => bind(removeByIdFromActiveHotbar, null, {
hotbar: di.inject(activeHotbarInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default removeByIdFromActiveHotbarInjectable;

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { hotbarStoreMigrationsInjectionToken } from "./migrations-injectable-token";
import { HotbarStore } from "./store";
const hotbarStoreInjectable = getInjectable({
instantiate: (di) => new HotbarStore({
migrations: di.inject(hotbarStoreMigrationsInjectionToken),
}),
lifecycle: lifecycleEnum.singleton,
});
export default hotbarStoreInjectable;

View File

@ -4,26 +4,29 @@
*/
import { action, comparer, observable, makeObservable, computed } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import { toJS } from "./utils";
import { CatalogEntity } from "./catalog";
import { catalogEntity } from "../main/catalog-sources/general";
import logger from "../main/logger";
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
import { BaseStore } from "../base-store";
import { toJS } from "../utils";
import { catalogEntity } from "../../main/catalog-sources/general";
import { defaultHotbarCells, getEmptyHotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
import { Hotbar } from "./hotbar";
import logger from "../logger";
import type { Migrations } from "conf/dist/source/types";
export interface HotbarStoreModel {
hotbars: Hotbar[];
hotbars: Required<CreateHotbarData>[];
activeHotbarId: string;
}
export interface HotbarStoreDependencies {
migrations: Migrations<HotbarStoreModel> | undefined;
}
export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string;
constructor() {
constructor({ migrations }: HotbarStoreDependencies) {
super({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -81,9 +84,9 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
hotbar.items[0] = initialItem;
this.hotbars = [hotbar];
this.hotbars = [new Hotbar(hotbar, { logger })];
} else {
this.hotbars = data.hotbars;
this.hotbars = data.hotbars.map(hotbar => new Hotbar(hotbar, { logger }));
}
this.hotbars.forEach(ensureExactHotbarItemLength);
@ -110,16 +113,16 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return this.getById(this.activeHotbarId);
}
getByName(name: string) {
getByName = (name: string) => {
return this.hotbars.find((hotbar) => hotbar.name === name);
}
};
getById(id: string) {
return this.hotbars.find((hotbar) => hotbar.id === id);
}
add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => {
const hotbar = getEmptyHotbar(data.name, data.id);
const hotbar = new Hotbar(getEmptyHotbar(data.name, data.id), { logger });
this.hotbars.push(hotbar);
@ -150,138 +153,35 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
});
@action
addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive();
const uid = item.metadata?.uid;
const name = item.metadata?.name;
if (typeof uid !== "string") {
throw new TypeError("CatalogEntity.metadata.uid must be a string");
}
if (typeof name !== "string") {
throw new TypeError("CatalogEntity.metadata.name must be a string");
}
const newItem = { entity: {
uid,
name,
source: item.metadata.source,
}};
if (this.isAddedToActive(item)) {
return;
}
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = hotbar.items.indexOf(null);
if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem;
} else {
broadcastMessage(HotbarTooManyItems);
}
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem;
} else {
logger.error(`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, { entityId: uid, hotbarId: hotbar.id, cellIndex });
}
}
@action
removeFromHotbar(uid: string): void {
const hotbar = this.getActive();
const index = hotbar.items.findIndex(item => item?.entity.uid === uid);
if (index < 0) {
return;
}
hotbar.items[index] = null;
}
/**
* Remove all hotbar items that reference the `uid`.
* @param uid The `EntityId` that each hotbar item refers to
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
*/
@action
removeAllHotbarItems(uid: string) {
removeAllHotbarItems = action((uid: string) => {
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
hotbar.items[index] = null;
}
hotbar.removeItemById(uid);
}
}
findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
while(this.getActive().items[index] != null) {
index += direction;
}
return index;
}
@action
restackItems(from: number, to: number): void {
const { items } = this.getActive();
const source = items[from];
const moveDown = from < to;
if (from < 0 || to < 0 || from >= items.length || to >= items.length || isNaN(from) || isNaN(to)) {
throw new Error("Invalid 'from' or 'to' arguments");
}
if (from == to) {
return;
}
items.splice(from, 1, null);
if (items[to] == null) {
items.splice(to, 1, source);
} else {
// Move cells up or down to closes empty cell
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
items.splice(to, 0, source);
}
}
});
switchToPrevious() {
const hotbarStore = HotbarStore.getInstance();
let index = hotbarStore.activeHotbarIndex - 1;
let index = this.activeHotbarIndex - 1;
if (index < 0) {
index = hotbarStore.hotbars.length - 1;
index = this.hotbars.length - 1;
}
hotbarStore.setActiveHotbar(index);
this.setActiveHotbar(index);
}
switchToNext() {
const hotbarStore = HotbarStore.getInstance();
let index = hotbarStore.activeHotbarIndex + 1;
let index = this.activeHotbarIndex + 1;
if (index >= hotbarStore.hotbars.length) {
if (index >= this.hotbars.length) {
index = 0;
}
hotbarStore.setActiveHotbar(index);
}
/**
* Checks if entity already pinned to hotbar
* @returns boolean
*/
isAddedToActive(entity: CatalogEntity) {
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
this.setActiveHotbar(index);
}
getDisplayLabel(hotbar: Hotbar): string {

View File

@ -28,7 +28,7 @@ const electronRemote = (() => {
const subFramesChannel = "ipc:get-sub-frames";
export async function requestMain(channel: string, ...args: any[]) {
export function requestMain(channel: string, ...args: any[]) {
return ipcRenderer.invoke(channel, ...args.map(sanitizePayload));
}

View File

@ -38,7 +38,7 @@ export function onceCorrect<
if (verifier(args)) {
source.removeListener(channel, wrappedListener); // remove immediately
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
(async () => await listener(event, ...args))() // might return a promise, or throw, or reject
.catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error }));
} else {
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
@ -70,7 +70,7 @@ export function onCorrect<
}): Disposer {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]) {
if (verifier(args)) {
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
(async () => await listener(event, ...args))() // might return a promise, or throw, or reject
.catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error }));
} else {
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });

View File

@ -28,6 +28,8 @@ export abstract class ItemStore<Item extends ItemObject> {
autoBind(this);
}
readonly computedItems = computed(() => [...this.items]);
@computed get selectedItems(): Item[] {
return this.items.filter(item => this.selectedItemsIds.get(item.getId()));
}

View File

@ -3,45 +3,53 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ingressStore } from "../../../renderer/components/+network-ingresses/ingress.store";
import { apiManager } from "../api-manager";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { ApiManager } from "../api-manager";
import apiManagerInjectable from "../api-manager.injectable";
import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
import { KubeObjectStore } from "../kube-object.store";
class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() {
return;
protected checkPreferredVersion() {
return Promise.resolve();
}
}
describe("ApiManager", () => {
describe("registerApi", () => {
it("re-register store if apiBase changed", async () => {
const apiBase = "apis/v1/foo";
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
const kubeApi = new TestApi({
objectConstructor: KubeObject,
apiBase,
fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true,
});
let di: ConfigurableDependencyInjectionContainer;
let apiManager: ApiManager;
apiManager.registerApi(apiBase, kubeApi);
beforeEach(() => {
di = getDiForUnitTesting();
apiManager = di.inject(apiManagerInjectable);
});
// Define to use test api for ingress store
Object.defineProperty(ingressStore, "api", { value: kubeApi });
apiManager.registerStore(ingressStore, [kubeApi]);
// Test that store is returned with original apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
// Change apiBase similar as checkPreferredVersion does
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
apiManager.registerApi(fallbackApiBase, kubeApi);
// Test that store is returned with new apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
it("allows apis to be accessible by their new apiBase if it changes", () => {
const apiBase = "apis/v1/foo";
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
const kubeApi = new TestApi({
objectConstructor: KubeObject,
apiBase,
fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true,
});
const kubeStore = new class extends KubeObjectStore<KubeObject> {
api = kubeApi;
};
apiManager.registerApi(kubeApi);
apiManager.registerStore(kubeStore);
// Test that store is returned with original apiBase
expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
// Change apiBase similar as checkPreferredVersion does
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
apiManager.registerApi(fallbackApiBase, kubeApi);
// Test that store is returned with new apiBase
expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
});
});

View File

@ -4,7 +4,7 @@
*/
import { anyObject } from "jest-mock-extended";
import { HelmChart } from "../endpoints/helm-charts.api";
import { HelmChart } from "../endpoints/helm-chart.api";
describe("HelmChart tests", () => {
describe("HelmChart.create() tests", () => {

View File

@ -10,12 +10,6 @@ import { KubeObject } from "../kube-object";
import AbortController from "abort-controller";
import { delay } from "../../utils/delay";
import { PassThrough } from "stream";
import { ApiManager, apiManager } from "../api-manager";
import { Ingress, Pod } from "../endpoints";
jest.mock("../api-manager");
const mockApiManager = apiManager as jest.Mocked<ApiManager>;
class TestKubeObject extends KubeObject {
static kind = "Pod";
@ -24,13 +18,13 @@ class TestKubeObject extends KubeObject {
}
class TestKubeApi extends KubeApi<TestKubeObject> {
public async checkPreferredVersion() {
public checkPreferredVersion() {
return super.checkPreferredVersion();
}
}
describe("forRemoteCluster", () => {
it("builds api client for KubeObject", async () => {
it("builds api client for KubeObject", () => {
const api = forRemoteCluster({
cluster: {
server: "https://127.0.0.1:6443",
@ -43,7 +37,7 @@ describe("forRemoteCluster", () => {
expect(api).toBeInstanceOf(KubeApi);
});
it("builds api client for given KubeApi", async () => {
it("builds api client for given KubeApi", () => {
const api = forRemoteCluster({
cluster: {
server: "https://127.0.0.1:6443",
@ -66,7 +60,7 @@ describe("forRemoteCluster", () => {
},
}, TestKubeObject);
(fetch as any).mockResponse(async (request: any) => {
(fetch as any).mockResponse((request: any) => {
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
return {
@ -91,7 +85,7 @@ describe("KubeApi", () => {
});
it("uses url from apiBase if apiBase contains the resource", async () => {
(fetch as any).mockResponse(async (request: any) => {
(fetch as any).mockResponse((request: any) => {
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return {
body: JSON.stringify({
@ -137,7 +131,7 @@ describe("KubeApi", () => {
});
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
(fetch as any).mockResponse(async (request: any) => {
(fetch as any).mockResponse((request: any) => {
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return {
body: JSON.stringify({
@ -178,94 +172,6 @@ describe("KubeApi", () => {
expect(kubeApi.apiGroup).toEqual("extensions");
});
describe("checkPreferredVersion", () => {
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => {
expect.hasAssertions();
const api = new TestKubeApi({
objectConstructor: Ingress,
checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
request: {
get: jest.fn()
.mockImplementationOnce((path: string) => {
expect(path).toBe("/apis/networking.k8s.io/v1");
throw new Error("no");
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/apis/extensions/v1beta1");
return {
resources: [
{
name: "ingresses",
},
],
};
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/apis/extensions");
return {
preferredVersion: {
version: "v1beta1",
},
};
}),
} as any,
});
await api.checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1");
expect(mockApiManager.registerApi).toBeCalledWith("/apis/extensions/v1beta1/ingresses", expect.anything());
});
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => {
expect.hasAssertions();
const api = new TestKubeApi({
objectConstructor: Pod,
checkPreferredVersion: true,
fallbackApiBases: ["/api/v1beta1/pods"],
request: {
get: jest.fn()
.mockImplementationOnce((path: string) => {
expect(path).toBe("/api/v1");
throw new Error("no");
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/api/v1beta1");
return {
resources: [
{
name: "pods",
},
],
};
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/api");
return {
preferredVersion: {
version: "v1beta1",
},
};
}),
} as any,
});
await api.checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1");
expect(mockApiManager.registerApi).toBeCalledWith("/api/v1beta1/pods", expect.anything());
});
});
describe("patch", () => {
let api: TestKubeApi;
@ -279,7 +185,7 @@ describe("KubeApi", () => {
it("sends strategic patch by default", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("strategic-merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
@ -295,7 +201,7 @@ describe("KubeApi", () => {
it("allows to use merge patch", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
@ -311,7 +217,7 @@ describe("KubeApi", () => {
it("allows to use json patch", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("json-patch");
expect(request.body.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
@ -327,7 +233,7 @@ describe("KubeApi", () => {
it("allows deep partial patch", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
@ -355,7 +261,7 @@ describe("KubeApi", () => {
it("sends correct request with empty namespace", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background");
@ -367,7 +273,7 @@ describe("KubeApi", () => {
it("sends correct request without namespace", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background");
@ -379,7 +285,7 @@ describe("KubeApi", () => {
it("sends correct request with namespace", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background");
@ -391,7 +297,7 @@ describe("KubeApi", () => {
it("allows to change propagationPolicy", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("DELETE");
expect(request.url).toMatch("propagationPolicy=Orphan");
@ -422,7 +328,7 @@ describe("KubeApi", () => {
it("sends a valid watch request", () => {
const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => {
(fetch as any).mockResponse(() => {
return {
body: stream,
};
@ -432,10 +338,10 @@ describe("KubeApi", () => {
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", expect.anything(), expect.anything());
});
it("sends timeout as a query parameter", async () => {
it("sends timeout as a query parameter", () => {
const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => {
(fetch as any).mockResponse(() => {
return {
body: stream,
};
@ -448,7 +354,7 @@ describe("KubeApi", () => {
it("aborts watch using abortController", async (done) => {
const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
(request as any).signal.addEventListener("abort", () => {
done();
});
@ -489,7 +395,7 @@ describe("KubeApi", () => {
});
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
jest.spyOn(global, "fetch").mockImplementation(async () => {
jest.spyOn(global, "fetch").mockImplementation(() => {
return {
ok: true,
body: stream,
@ -511,7 +417,7 @@ describe("KubeApi", () => {
it("if request not closed after timeout", (done) => {
const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => {
(fetch as any).mockResponse(() => {
return {
body: stream,
};
@ -547,7 +453,7 @@ describe("KubeApi", () => {
});
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
jest.spyOn(global, "fetch").mockImplementation(async () => {
jest.spyOn(global, "fetch").mockImplementation(() => {
return {
ok: true,
body: stream,
@ -588,7 +494,7 @@ describe("KubeApi", () => {
it("should add kind and apiVersion", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("POST");
expect(JSON.parse(request.body.toString())).toEqual({
kind: "Pod",
@ -642,7 +548,7 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("POST");
expect(JSON.parse(request.body.toString())).toEqual({
kind: "Pod",
@ -685,7 +591,7 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
(fetch as any).mockResponse((request: Request) => {
expect(request.method).toEqual("PUT");
expect(JSON.parse(request.body.toString())).toEqual({
metadata: {

View File

@ -0,0 +1,256 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ClusterStore } from "../../renderer/components/+cluster/store";
import { HorizontalPodAutoscalerStore } from "../../renderer/components/+autoscalers/store";
import { LimitRangeStore } from "../../renderer/components/+limit-ranges/store";
import { ConfigMapStore } from "../../renderer/components/+config-maps/store";
import { PodDisruptionBudgetStore } from "../../renderer/components/+pod-disruption-budgets/store";
import { ResourceQuotaStore } from "../../renderer/components/+resource-quotas/store";
import { SecretStore } from "../../renderer/components/+secrets/store";
import { CustomResourceDefinitionStore } from "../../renderer/components/+custom-resource/store";
import { EventStore } from "../../renderer/components/+events/store";
import { NamespaceStore } from "../../renderer/components/+namespaces/store";
import { EndpointStore } from "../../renderer/components/+endpoints/store";
import { IngressStore } from "../../renderer/components/+ingresses/store";
import { NetworkPolicyStore } from "../../renderer/components/+network-policies/store";
import { ServiceStore } from "../../renderer/components/+services/store";
import { NodeStore } from "../../renderer/components/+nodes/store";
import { PodSecurityPolicyStore } from "../../renderer/components/+pod-security-policies/store";
import { StorageClassStore } from "../../renderer/components/+storage-classes/store";
import { PersistentVolumeClaimStore } from "../../renderer/components/+persistent-volume-claims/store";
import { PersistentVolumeStore } from "../../renderer/components/+persistent-volumes/store";
import { ClusterRoleBindingStore } from "../../renderer/components/+cluster-role-bindings/store";
import { ClusterRoleStore } from "../../renderer/components/+cluster-roles/store";
import { RoleBindingStore } from "../../renderer/components/+role-bindings/store";
import { RoleStore } from "../../renderer/components/+roles/store";
import { ServiceAccountStore } from "../../renderer/components/+service-accounts/store";
import { CronJobStore } from "../../renderer/components/+cronjobs/store";
import { DaemonSetStore } from "../../renderer/components/+daemonsets/store";
import { DeploymentStore } from "../../renderer/components/+deployments/store";
import { JobStore } from "../../renderer/components/+jobs/store";
import { PodStore } from "../../renderer/components/+pods/store";
import { ReplicaSetStore } from "../../renderer/components/+replica-sets/store";
import { StatefulSetStore } from "../../renderer/components/+stateful-sets/store";
import { ApiManager } from "./api-manager";
import { ClusterApi, ClusterRoleApi, ClusterRoleBindingApi, ConfigMapApi, CronJobApi, CustomResourceDefinition, CustomResourceDefinitionApi, DaemonSetApi, DeploymentApi, EndpointApi, EventApi, HorizontalPodAutoscalerApi, IngressApi, JobApi, LimitRangeApi, NamespaceApi, NetworkPolicyApi, NodeApi, PersistentVolumeApi, PersistentVolumeClaimApi, PodApi, PodDisruptionBudgetApi, PodMetricsApi, PodSecurityPolicyApi, ReplicaSetApi, ResourceQuotaApi, RoleApi, RoleBindingApi, SecretApi, SelfSubjectRulesReviewApi, ServiceAccountApi, ServiceApi, StatefulSetApi, StorageClassApi } from "./endpoints";
import { KubeApi } from "./kube-api";
import { KubeObject } from "./kube-object";
import { KubeObjectStore } from "./kube-object.store";
function createAndInit(): ApiManager {
const apiManager = new ApiManager();
const clusterApi = new ClusterApi();
apiManager.registerApi(clusterApi);
apiManager.registerStore(new ClusterStore(clusterApi));
const clusterRoleApi = new ClusterRoleApi();
apiManager.registerApi(clusterRoleApi);
apiManager.registerStore(new ClusterRoleStore(clusterRoleApi));
const clusterRoleBindingApi = new ClusterRoleBindingApi();
apiManager.registerApi(clusterRoleBindingApi);
apiManager.registerStore(new ClusterRoleBindingStore(clusterRoleBindingApi));
const configMapApi = new ConfigMapApi();
apiManager.registerApi(configMapApi);
apiManager.registerStore(new ConfigMapStore(configMapApi));
const podApi = new PodApi();
const podStore = new PodStore(podApi);
apiManager.registerApi(podApi);
apiManager.registerStore(podStore);
const jobApi = new JobApi();
const jobStore = new JobStore(jobApi, {
podStore,
});
apiManager.registerApi(jobApi);
apiManager.registerStore(jobStore);
const cronJobApi = new CronJobApi();
apiManager.registerApi(cronJobApi);
apiManager.registerStore(new CronJobStore(cronJobApi, {
jobStore,
}));
const customResourceDefinitionApi = new CustomResourceDefinitionApi();
apiManager.registerApi(customResourceDefinitionApi);
apiManager.registerStore(new CustomResourceDefinitionStore(customResourceDefinitionApi, {
initCustomResourceStore(crd: CustomResourceDefinition) {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
const api = apiManager.getApi(objectConstructor.apiBase)
?? new KubeApi({ objectConstructor });
if (!apiManager.hasApi(api)) {
apiManager.registerApi(api);
}
if (!apiManager.getStore(api)) {
apiManager.registerStore(new class extends KubeObjectStore<KubeObject> {
api = api;
});
}
},
}));
const daemonSetApi = new DaemonSetApi();
apiManager.registerApi(daemonSetApi);
apiManager.registerStore(new DaemonSetStore(daemonSetApi, {
podStore,
}));
const deploymentApi = new DeploymentApi();
apiManager.registerApi(deploymentApi);
apiManager.registerStore(new DeploymentStore(deploymentApi, {
podStore,
}));
const endpointApi = new EndpointApi();
apiManager.registerApi(endpointApi);
apiManager.registerStore(new EndpointStore(endpointApi));
const eventApi = new EventApi();
apiManager.registerApi(eventApi);
apiManager.registerStore(new EventStore(eventApi, {
podStore,
}));
const horizontalPodAutoscalerApi = new HorizontalPodAutoscalerApi();
apiManager.registerApi(horizontalPodAutoscalerApi);
apiManager.registerStore(new HorizontalPodAutoscalerStore(horizontalPodAutoscalerApi));
const ingressApi = new IngressApi();
apiManager.registerApi(ingressApi);
apiManager.registerStore(new IngressStore(ingressApi));
const limitRangeApi = new LimitRangeApi();
apiManager.registerApi(limitRangeApi);
apiManager.registerStore(new LimitRangeStore(limitRangeApi));
const namespaceApi = new NamespaceApi();
apiManager.registerApi(namespaceApi);
apiManager.registerStore(new NamespaceStore(namespaceApi));
const networkPolicyApi = new NetworkPolicyApi();
apiManager.registerApi(networkPolicyApi);
apiManager.registerStore(new NetworkPolicyStore(networkPolicyApi));
const nodeApi = new NodeApi();
apiManager.registerApi(nodeApi);
apiManager.registerStore(new NodeStore(nodeApi));
const persistentVolumeApi = new PersistentVolumeApi();
const persistentVolumeStore = new PersistentVolumeStore(persistentVolumeApi);
apiManager.registerApi(persistentVolumeApi);
apiManager.registerStore(persistentVolumeStore);
const persistentVolumeClaimApi = new PersistentVolumeClaimApi();
apiManager.registerApi(persistentVolumeClaimApi);
apiManager.registerStore(new PersistentVolumeClaimStore(persistentVolumeClaimApi));
const podDisruptionBudgetApi = new PodDisruptionBudgetApi();
apiManager.registerApi(podDisruptionBudgetApi);
apiManager.registerStore(new PodDisruptionBudgetStore(podDisruptionBudgetApi));
const podSecurityPolicyApi = new PodSecurityPolicyApi();
apiManager.registerApi(podSecurityPolicyApi);
apiManager.registerStore(new PodSecurityPolicyStore(podSecurityPolicyApi));
const replicaSetApi = new ReplicaSetApi();
apiManager.registerApi(replicaSetApi);
apiManager.registerStore(new ReplicaSetStore(replicaSetApi, {
podStore,
}));
const resourceQuotaApi = new ResourceQuotaApi();
apiManager.registerApi(resourceQuotaApi);
apiManager.registerStore(new ResourceQuotaStore(resourceQuotaApi));
const roleApi = new RoleApi();
apiManager.registerApi(roleApi);
apiManager.registerStore(new RoleStore(roleApi));
const roleBindingApi = new RoleBindingApi();
apiManager.registerApi(roleBindingApi);
apiManager.registerStore(new RoleBindingStore(roleBindingApi));
const secretApi = new SecretApi();
apiManager.registerApi(secretApi);
apiManager.registerStore(new SecretStore(secretApi));
const serviceAccountApi = new ServiceAccountApi();
apiManager.registerApi(serviceAccountApi);
apiManager.registerStore(new ServiceAccountStore(serviceAccountApi));
const serviceApi = new ServiceApi();
apiManager.registerApi(serviceApi);
apiManager.registerStore(new ServiceStore(serviceApi));
const statefulSetApi = new StatefulSetApi();
apiManager.registerApi(statefulSetApi);
apiManager.registerStore(new StatefulSetStore(statefulSetApi, {
podStore,
}));
const storageClassApi = new StorageClassApi();
apiManager.registerApi(storageClassApi);
apiManager.registerStore(new StorageClassStore(storageClassApi, {
persistentVolumeStore,
}));
// There is no store for these apis, so just register them
apiManager.registerApi(new PodMetricsApi());
apiManager.registerApi(new SelfSubjectRulesReviewApi());
return apiManager;
}
const apiManagerInjectable = getInjectable({
instantiate: createAndInit,
lifecycle: lifecycleEnum.singleton,
});
export default apiManagerInjectable;

View File

@ -5,44 +5,109 @@
import type { KubeObjectStore } from "./kube-object.store";
import { action, observable, makeObservable } from "mobx";
import { action, observable, makeObservable, computed } from "mobx";
import { autoBind, iter } from "../utils";
import type { KubeApi } from "./kube-api";
import type { KubeObject } from "./kube-object";
import { IKubeObjectRef, parseKubeApi, createKubeApiURL } from "./kube-api-parse";
export class ApiManager {
private apis = observable.map<string, KubeApi<KubeObject>>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
private apiSet = observable.set<KubeApi<KubeObject>>();
private stores = observable.map<KubeApi<KubeObject>, KubeObjectStore<KubeObject>>();
constructor() {
makeObservable(this);
autoBind(this);
}
getApi(pathOrCallback: string | ((api: KubeApi<KubeObject>) => boolean)) {
if (typeof pathOrCallback === "string") {
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
/**
* The private `apiBase` mapping of api instances. This is computed so that
* it can react to changes in the instances' apiBase fields.
*/
@computed private get apis() {
const res = new Map<string, KubeApi<KubeObject>>();
for (const api of this.apiSet) {
if (typeof api.apiBase !== "string" || !api.apiBase) {
throw new TypeError("KubeApi.apiBase must be a non-empty string");
}
if (res.has(api.apiBase)) {
throw new Error(`Multiple api instances for ${api.apiBase}`);
}
res.set(api.apiBase, api);
}
return iter.find(this.apis.values(), pathOrCallback ?? (() => true));
return res;
}
/**
* @param api The instance to check if it has been registered
* @returns Returns `true` if the api instance has been registered
*/
hasApi(api: KubeApi<KubeObject>): boolean {
return this.apiSet.has(api);
}
/**
* Get a registered api, if a callback is provided then the registered
* instances are iterated until it returns `true`
* @param pathOrCallbacks Either the `apiBase` of an instance, a resource path for the kind of the api, or a callback function. Will search for each until one is found.
* @returns The kube api instance that was registered
*/
getApi(...pathOrCallbacks: (string | ((api: KubeApi<KubeObject>) => boolean))[]): KubeApi<KubeObject> | undefined {
for (const pathOrCallback of pathOrCallbacks) {
if (typeof pathOrCallback === "string") {
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
}
return iter.find(this.apis.values(), pathOrCallback);
}
return undefined;
}
/**
* Get the registered api instance by the kube object kind and version
* @param kind The kind of resource that the api is for
* @param apiVersion The version of the resource that the api is for
* @returns The kube api instance that was registered
*/
getApiByKind(kind: string, apiVersion: string) {
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
}
registerApi(apiBase: string, api: KubeApi<KubeObject>) {
if (!api.apiBase) return;
/**
* Registeres `api` so that it can be retreived in the future.
*
* Notes:
* - Changes to the instance's `apiBase` field are reacted to for the `getApi()` method
* @param api The instance to register
* @throws if `api.apiBase` is not a non-empty string
* @throws if there is already an instance with the same `apiBase` registered
*/
registerApi(api: KubeApi<KubeObject>): void;
/**
* @deprecated Just provide the `api` instance
*/
registerApi(apiOrBase: string, api: KubeApi<KubeObject>): void
@action
registerApi(apiOrBase: string | KubeApi<KubeObject>, api?: KubeApi<KubeObject>): void {
api = typeof apiOrBase === "string"
? api
: apiOrBase;
if (!this.apis.has(apiBase)) {
this.stores.forEach((store) => {
if (store.api === api) {
this.stores.set(apiBase, store);
}
});
if (!this.apiSet.has(api)) {
if (typeof api.apiBase !== "string" || !api.apiBase) {
throw new TypeError("api.apiBase but be defined");
}
this.apis.set(apiBase, api);
if (this.apis.has(api.apiBase)) {
throw new Error(`Cannot register second api for ${api.apiBase}`);
}
this.apiSet.add(api);
}
}
@ -58,28 +123,70 @@ export class ApiManager {
return api;
}
unregisterApi(api: string | KubeApi<KubeObject>) {
if (typeof api === "string") this.apis.delete(api);
else {
const apis = Array.from(this.apis.entries());
const entry = apis.find(entry => entry[1] === api);
/**
* Removes `api` from the set of registered apis
* @param api The instance to de-register
* @returns `true` if the instance was previously registered
*/
@action
unregisterApi(api: KubeApi<KubeObject>) {
return this.apiSet.delete(api);
}
if (entry) this.unregisterApi(entry[0]);
/**
* Registeres a `KubeObjectStore` instance that can be retrieved by the `apiBase` its api is for
* @param store The store to register
*/
registerStore(store: KubeObjectStore<KubeObject>): void;
/**
* @deprecated stores should only be registered for the single api that the store is for.
*/
registerStore(store: KubeObjectStore<KubeObject>, apis: KubeApi<KubeObject>[]): void;
@action
registerStore(store: KubeObjectStore<KubeObject>, apis?: KubeApi<KubeObject>[]) {
apis ??= [store.api];
for (const api of apis) {
if (!this.apiSet.has(api)) {
throw new Error(`Cannot register store under ${api.apiBase} api, as that api is not registered`);
}
if (this.stores.has(api)) {
throw new Error(`Each api instance can only have one store associated with it. Attempt to register a duplicate store for the ${api.apiBase} api`);
}
this.stores.set(api, store);
}
}
@action
registerStore(store: KubeObjectStore<KubeObject>, apis: KubeApi<KubeObject>[] = [store.api]) {
apis.filter(Boolean).forEach(api => {
if (api.apiBase) this.stores.set(api.apiBase, store);
});
/**
*
* @param apiOrBases The `apiBase`, resource descriptor, or `KubeApi` instance that the store is for. In order of searching
* @returns The registered store whose api has also been registered
*/
getStore(...apiOrBases: (string | KubeApi<KubeObject>)[]): KubeObjectStore<KubeObject> | undefined;
/**
* @deprecated Should use a cast instead as this is an unchecked type param.
*/
getStore<S extends KubeObjectStore<KubeObject>>(...apiOrBases: (string | KubeApi<KubeObject>)[]): S | undefined {
for (const apiOrBase of apiOrBases) {
const store = this.stores.get(this.resolveApi(apiOrBase)) as S;
if (store) {
return store;
}
}
return undefined;
}
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
}
lookupApiLink(ref: IKubeObjectRef, parentObject?: KubeObject): string {
/**
* Get a URL pathname for a specific kube resource instance
* @param ref The kube object reference
* @param parentObject If provided then the namespace of this will be used if the `ref` does not provided it
* @returns A kube resource string
*/
lookupApiLink = (ref: IKubeObjectRef, parentObject?: KubeObject): string => {
const {
kind, apiVersion, name,
namespace = parentObject?.getNs(),
@ -116,7 +223,5 @@ export class ApiManager {
// otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app
return createKubeApiURL({ apiVersion, name, namespace, resource });
}
};
}
export const apiManager = new ApiManager();

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { ClusterRoleBindingApi } from "./cluster-role-binding.api";
const clusterRoleBindingApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/rbac.authorization.k8s.io/v1/clusterrolebindings") as ClusterRoleBindingApi,
lifecycle: lifecycleEnum.singleton,
});
export default clusterRoleBindingApiInjectable;

View File

@ -2,8 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { KubeObject } from "../kube-object";
export type ClusterRoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
@ -38,17 +37,11 @@ export class ClusterRoleBinding extends KubeObject {
}
}
/**
* Only available within kubernetes cluster pages
*/
let clusterRoleBindingApi: KubeApi<ClusterRoleBinding>;
if (isClusterPageContext()) {
clusterRoleBindingApi = new KubeApi({
objectConstructor: ClusterRoleBinding,
});
export class ClusterRoleBindingApi extends KubeApi<ClusterRoleBinding> {
constructor(args: SpecificApiOptions<ClusterRoleBinding> = {}) {
super({
...args,
objectConstructor: ClusterRoleBinding,
});
}
}
export {
clusterRoleBindingApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { ClusterRoleApi } from "./cluster-role.api";
const clusterRoleApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/rbac.authorization.k8s.io/v1/clusterroles") as ClusterRoleApi,
lifecycle: lifecycleEnum.singleton,
});
export default clusterRoleApiInjectable;

View File

@ -3,8 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { KubeObject } from "../kube-object";
export interface ClusterRole {
@ -26,17 +25,11 @@ export class ClusterRole extends KubeObject {
}
}
/**
* Only available within kubernetes cluster pages
*/
let clusterRoleApi: KubeApi<ClusterRole>;
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
clusterRoleApi = new KubeApi({
objectConstructor: ClusterRole,
});
export class ClusterRoleApi extends KubeApi<ClusterRole> {
constructor(args: SpecificApiOptions<ClusterRole> = {}) {
super({
...args,
objectConstructor: ClusterRole,
});
}
}
export {
clusterRoleApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { ClusterApi } from "./cluster.api";
const clusterApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/cluster.k8s.io/v1alpha1/clusters") as ClusterApi,
lifecycle: lifecycleEnum.singleton,
});
export default clusterApiInjectable;

View File

@ -5,13 +5,7 @@
import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class ClusterApi extends KubeApi<Cluster> {
static kind = "Cluster";
static namespaced = true;
}
import { KubeApi, SpecificApiOptions } from "../kube-api";
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
const nodes = nodeNames.join("|");
@ -97,6 +91,7 @@ export interface Cluster {
export class Cluster extends KubeObject {
static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
static namespaced = true;
getStatus() {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
@ -107,17 +102,11 @@ export class Cluster extends KubeObject {
}
}
/**
* Only available within kubernetes cluster pages
*/
let clusterApi: ClusterApi;
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
clusterApi = new ClusterApi({
objectConstructor: Cluster,
});
export class ClusterApi extends KubeApi<Cluster> {
constructor(args: SpecificApiOptions<Cluster> = {}) {
super({
...args,
objectConstructor: Cluster,
});
}
}
export {
clusterApi,
};

View File

@ -1,31 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export interface IComponentStatusCondition {
type: string;
status: string;
message: string;
}
export interface ComponentStatus {
conditions: IComponentStatusCondition[];
}
export class ComponentStatus extends KubeObject {
static kind = "ComponentStatus";
static namespaced = false;
static apiBase = "/api/v1/componentstatuses";
getTruthyConditions() {
return this.conditions.filter(c => c.status === "True");
}
}
export const componentStatusApi = new KubeApi({
objectConstructor: ComponentStatus,
});

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { ConfigMapApi } from "./configmap.api";
const configMapApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/configmaps") as ConfigMapApi,
lifecycle: lifecycleEnum.singleton,
});
export default configMapApiInjectable;

View File

@ -5,9 +5,8 @@
import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { autoBind } from "../../utils";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface ConfigMap {
data: {
@ -32,17 +31,11 @@ export class ConfigMap extends KubeObject {
}
}
/**
* Only available within kubernetes cluster pages
*/
let configMapApi: KubeApi<ConfigMap>;
if (isClusterPageContext()) {
configMapApi = new KubeApi({
objectConstructor: ConfigMap,
});
export class ConfigMapApi extends KubeApi<ConfigMap> {
constructor(args: SpecificApiOptions<ConfigMap> = {}) {
super({
...args,
objectConstructor: ConfigMap,
});
}
}
export {
configMapApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { CronJobApi } from "./cron-job.api";
import apiManagerInjectable from "../api-manager.injectable";
const cronJobApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/batch/v1beta1/cronjobs") as CronJobApi,
lifecycle: lifecycleEnum.singleton,
});
export default cronJobApiInjectable;

View File

@ -5,44 +5,11 @@
import moment from "moment";
import { KubeObject } from "../kube-object";
import type { IPodContainer } from "./pods.api";
import type { IPodContainer } from "./pod.api";
import { formatDuration } from "../../utils/formatDuration";
import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class CronJobApi extends KubeApi<CronJob> {
suspend(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
suspend: true,
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
resume(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
suspend: false,
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
}
export interface CronJob {
spec: {
@ -125,17 +92,41 @@ export class CronJob extends KubeObject {
}
}
/**
* Only available within kubernetes cluster pages
*/
let cronJobApi: CronJobApi;
export class CronJobApi extends KubeApi<CronJob> {
constructor(args: SpecificApiOptions<CronJob> = {}) {
super({
...args,
objectConstructor: CronJob,
});
}
if (isClusterPageContext()) {
cronJobApi = new CronJobApi({
objectConstructor: CronJob,
});
suspend(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
suspend: true,
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
resume(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
suspend: false,
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
}
export {
cronJobApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { CustomResourceDefinitionApi } from "./custom-resource-definition.api";
const customResourceDefinitionApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/apiextensions.k8s.io/v1/customresourcedefinitions") as CustomResourceDefinitionApi,
lifecycle: lifecycleEnum.singleton,
});
export default customResourceDefinitionApiInjectable;

View File

@ -4,9 +4,8 @@
*/
import { KubeCreationError, KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { crdResourcesURL } from "../../routes";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { KubeJsonApiData } from "../kube-json-api";
type AdditionalPrinterColumnsCommon = {
@ -210,18 +209,11 @@ export class CustomResourceDefinition extends KubeObject {
}
}
/**
* Only available within kubernetes cluster pages
*/
let crdApi: KubeApi<CustomResourceDefinition>;
if (isClusterPageContext()) {
crdApi = new KubeApi<CustomResourceDefinition>({
objectConstructor: CustomResourceDefinition,
checkPreferredVersion: true,
});
export class CustomResourceDefinitionApi extends KubeApi<CustomResourceDefinition> {
constructor(args: SpecificApiOptions<CustomResourceDefinition> = {}) {
super({
...args,
objectConstructor: CustomResourceDefinition,
});
}
}
export {
crdApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { DaemonSetApi } from "./daemon-set.api";
const daemonSetApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/apps/v1/daemonsets") as DaemonSetApi,
lifecycle: lifecycleEnum.singleton,
});
export default daemonSetApiInjectable;

View File

@ -6,11 +6,10 @@
import get from "lodash/get";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { metricsApi } from "./metrics.api";
import type { KubeJsonApiData } from "../kube-json-api";
import type { IPodContainer, IPodMetrics } from "./pods.api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { IPodContainer, IPodMetrics } from "./pod.api";
import type { LabelSelector } from "../kube-object";
export class DaemonSet extends WorkloadKubeObject {
@ -80,9 +79,6 @@ export class DaemonSet extends WorkloadKubeObject {
}
}
export class DaemonSetApi extends KubeApi<DaemonSet> {
}
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -100,17 +96,11 @@ export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: stri
});
}
/**
* Only available within kubernetes cluster pages
*/
let daemonSetApi: DaemonSetApi;
if (isClusterPageContext()) {
daemonSetApi = new DaemonSetApi({
objectConstructor: DaemonSet,
});
export class DaemonSetApi extends KubeApi<DaemonSet> {
constructor(args: SpecificApiOptions<DaemonSet> = {}) {
super({
...args,
objectConstructor: DaemonSet,
});
}
}
export {
daemonSetApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { DeploymentApi } from "./deployment.api";
const deploymentApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/apps/v1/deployments") as DeploymentApi,
lifecycle: lifecycleEnum.singleton,
});
export default deploymentApiInjectable;

View File

@ -7,59 +7,12 @@ import moment from "moment";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { metricsApi } from "./metrics.api";
import type { IPodMetrics } from "./pods.api";
import type { IPodMetrics } from "./pod.api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { LabelSelector } from "../kube-object";
export class DeploymentApi extends KubeApi<Deployment> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`;
}
getReplicas(params: { namespace: string; name: string }): Promise<number> {
return this.request
.get(this.getScaleApiUrl(params))
.then(({ status }: any) => status?.replicas);
}
scale(params: { namespace: string; name: string }, replicas: number) {
return this.request.patch(this.getScaleApiUrl(params), {
data: {
spec: {
replicas,
},
},
},
{
headers: {
"content-type": "application/merge-patch+json",
},
});
}
restart(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
template: {
metadata: {
annotations: { "kubectl.kubernetes.io/restartedAt" : moment.utc().format() },
},
},
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
}
export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<IPodMetrics> {
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -224,14 +177,61 @@ export class Deployment extends WorkloadKubeObject {
}
}
let deploymentApi: DeploymentApi;
if (isClusterPageContext()) {
deploymentApi = new DeploymentApi({
objectConstructor: Deployment,
});
interface ReplicasStatus {
status?: {
replicas: number;
}
}
export {
deploymentApi,
};
export class DeploymentApi extends KubeApi<Deployment> {
constructor(args: SpecificApiOptions<Deployment> = {}) {
super({
...args,
objectConstructor: Deployment,
});
}
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`;
}
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
const { status } = await this.request.get<ReplicasStatus>(this.getScaleApiUrl(params));
return status?.replicas ?? 0;
}
scale(params: { namespace: string; name: string }, replicas: number) {
return this.request.patch(this.getScaleApiUrl(params), {
data: {
spec: {
replicas,
},
},
},
{
headers: {
"content-type": "application/merge-patch+json",
},
});
}
restart(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
template: {
metadata: {
annotations: { "kubectl.kubernetes.io/restartedAt" : moment.utc().format() },
},
},
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
}

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { EndpointApi } from "./endpoint.api";
const endpointApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/endpoints") as EndpointApi,
lifecycle: lifecycleEnum.singleton,
});
export default endpointApiInjectable;

View File

@ -5,10 +5,9 @@
import { autoBind } from "../../utils";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { get } from "lodash";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface IEndpointPort {
name?: string;
@ -131,17 +130,13 @@ export class Endpoint extends KubeObject {
return "<none>";
}
}
}
let endpointApi: KubeApi<Endpoint>;
if (isClusterPageContext()) {
endpointApi = new KubeApi<Endpoint>({
objectConstructor: Endpoint,
});
export class EndpointApi extends KubeApi<Endpoint> {
constructor(args: SpecificApiOptions<Endpoint> = {}) {
super({
...args,
objectConstructor: Endpoint,
});
}
}
export {
endpointApi,
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { EventApi } from "./event.api";
const eventApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/events") as EventApi,
lifecycle: lifecycleEnum.singleton,
});
export default eventApiInjectable;

View File

@ -6,10 +6,9 @@
import moment from "moment";
import { KubeObject } from "../kube-object";
import { formatDuration } from "../../utils/formatDuration";
import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import { KubeApi, SpecificApiOptions } from "../kube-api";
export interface KubeEvent {
export interface Event {
involvedObject: {
kind: string;
namespace: string;
@ -34,7 +33,7 @@ export interface KubeEvent {
reportingInstance: string;
}
export class KubeEvent extends KubeObject {
export class Event extends KubeObject {
static kind = "Event";
static namespaced = true;
static apiBase = "/api/v1/events";
@ -62,14 +61,11 @@ export class KubeEvent extends KubeObject {
}
}
let eventApi: KubeApi<KubeEvent>;
if (isClusterPageContext()) {
eventApi = new KubeApi<KubeEvent>({
objectConstructor: KubeEvent,
});
export class EventApi extends KubeApi<Event> {
constructor(args: SpecificApiOptions<Event> = {}) {
super({
...args,
objectConstructor: Event,
});
}
}
export {
eventApi,
};

View File

@ -65,7 +65,7 @@ export async function getChartDetails(repo: string, name: string, { version, req
* @param name The name of the chart to request the data of
* @param version The version to get the values from
*/
export async function getChartValues(repo: string, name: string, version: string): Promise<string> {
export function getChartValues(repo: string, name: string, version: string): Promise<string> {
return apiBase.get<string>(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`);
}

View File

@ -4,10 +4,10 @@
*/
import yaml from "js-yaml";
import { autoBind, formatDuration } from "../../utils";
import { formatDuration } from "../../utils";
import capitalize from "lodash/capitalize";
import { apiBase } from "../index";
import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store";
import { helmChartStore } from "../../../renderer/components/+helm-charts/store";
import type { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object";
import type { JsonApiData } from "../json-api";
@ -80,9 +80,9 @@ interface EndpointQuery {
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
const releases = await apiBase.get<HelmReleaseDto[]>(endpoint({ namespace }));
return releases.map(HelmRelease.create);
return releases.map(toHelmRelease);
}
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
@ -96,7 +96,7 @@ export async function getRelease(name: string, namespace: string): Promise<IRele
};
}
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
export function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
const chart = `${repo}/${rawChart}`;
const values = yaml.load(rawValues);
@ -110,7 +110,7 @@ export async function createRelease(payload: IReleaseCreatePayload): Promise<IRe
});
}
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
export function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
const chart = `${repo}/${rawChart}`;
const values = yaml.load(rawValues);
@ -124,27 +124,27 @@ export async function updateRelease(name: string, namespace: string, payload: IR
});
}
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
export function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
const path = endpoint({ name, namespace });
return apiBase.del(path);
}
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
export function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
const route = "values";
const path = endpoint({ name, namespace, route }, { all });
return apiBase.get<string>(path);
}
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
export function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const route = "history";
const path = endpoint({ name, namespace, route });
return apiBase.get(path);
}
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
export function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
const route = "rollback";
const path = endpoint({ name, namespace, route });
const data = { revision };
@ -152,7 +152,7 @@ export async function rollbackRelease(name: string, namespace: string, revision:
return apiBase.put(path, { data });
}
export interface HelmRelease {
interface HelmReleaseDto {
appVersion: string;
name: string;
namespace: string;
@ -162,27 +162,30 @@ export interface HelmRelease {
revision: string;
}
export class HelmRelease implements ItemObject {
constructor(data: any) {
Object.assign(this, data);
autoBind(this);
}
export interface HelmRelease extends HelmReleaseDto, ItemObject {
getNs: () => string
getChart: (withVersion?: boolean) => string
getRevision: () => number
getStatus: () => string
getVersion: () => string
getUpdated: (humanize?: boolean, compact?: boolean) => string | number
getRepo: () => Promise<string>
}
static create(data: any) {
return new HelmRelease(data);
}
const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({
...release,
getId() {
return this.namespace + this.name;
}
},
getName() {
return this.name;
}
},
getNs() {
return this.namespace;
}
},
getChart(withVersion = false) {
let chart = this.chart;
@ -194,24 +197,24 @@ export class HelmRelease implements ItemObject {
}
return chart;
}
},
getRevision() {
return parseInt(this.revision, 10);
}
},
getStatus() {
return capitalize(this.status);
}
},
getVersion() {
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
return versions?.[0] ?? "";
}
},
getUpdated(humanize = true, compact = true) {
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updatedDate = new Date(updated).getTime();
const diff = Date.now() - updatedDate;
@ -220,7 +223,7 @@ export class HelmRelease implements ItemObject {
}
return diff;
}
},
// Helm does not store from what repository the release is installed,
// so we have to try to guess it by searching charts
@ -228,8 +231,10 @@ export class HelmRelease implements ItemObject {
const chartName = this.getChart();
const version = this.getVersion();
const versions = await helmChartStore.getVersions(chartName);
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
const chartVersion = versions.find(
(chartVersion) => chartVersion.version === version,
);
return chartVersion ? chartVersion.repo : "";
}
}
},
});

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import apiManagerInjectable from "../api-manager.injectable";
import type { HorizontalPodAutoscalerApi } from "./horizontal-pod-autoscaler.api";
const horizontalPodAutoscalerApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/autoscaling/v2beta1/horizontalpodautoscalers") as HorizontalPodAutoscalerApi,
lifecycle: lifecycleEnum.singleton,
});
export default horizontalPodAutoscalerApiInjectable;

View File

@ -4,8 +4,7 @@
*/
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import { KubeApi, SpecificApiOptions } from "../kube-api";
export enum HpaMetricType {
Resource = "Resource",
@ -148,14 +147,11 @@ export class HorizontalPodAutoscaler extends KubeObject {
}
}
let hpaApi: KubeApi<HorizontalPodAutoscaler>;
if (isClusterPageContext()) {
hpaApi = new KubeApi<HorizontalPodAutoscaler>({
objectConstructor: HorizontalPodAutoscaler,
});
export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
constructor(args: SpecificApiOptions<HorizontalPodAutoscaler> = {}) {
super({
...args,
objectConstructor: HorizontalPodAutoscaler,
});
}
}
export {
hpaApi,
};

Some files were not shown because too many files have changed in this diff Show More