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 committed 2022-01-28 18:05:05 -05:00
1 parent 6cba82c491
commit 1747cbf6c8
1345 files changed
+34694 -27702

No files matched your search

+3 -5
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();
});
@@ -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);
});
});
+9 -18
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);
-416
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",
},
});
});
});
});
+6 -6
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);
@@ -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);
});
});
+15 -18
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();
});
@@ -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) =>
@@ -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,
@@ -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,
@@ -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({
@@ -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,
@@ -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,
@@ -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,
+3 -5
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) {
@@ -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",
+1 -16
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());
@@ -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);
+4 -30
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());
+25 -25
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();
+3 -3
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;
}
@@ -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;
@@ -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,
});
@@ -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>();
@@ -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,
});
@@ -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" });
+21 -17
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;
@@ -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>();
@@ -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) =>
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+39
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 }),
]);
@@ -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();
});
});
@@ -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",
},
});
});
});
});
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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),
+121
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);
}
});
}
@@ -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;
@@ -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>();
@@ -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;
@@ -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;
@@ -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;
@@ -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 {
+1 -1
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));
}
+2 -2
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 });
+2
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()));
}
@@ -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);
});
});
@@ -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", () => {
+24 -118
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: {
@@ -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;
+140 -35
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();
@@ -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;
@@ -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,
};
@@ -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;
@@ -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,
};
@@ -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;
+9 -20
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,
};
@@ -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,
});
@@ -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;
+8 -15
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,
};
@@ -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;
+38 -47
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,
};
@@ -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;
@@ -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,
};
@@ -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;
+9 -19
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,
};
@@ -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;
+58 -58
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",
},
});
}
}
@@ -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;
+8 -13
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,
};
@@ -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;
@@ -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,
};
@@ -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 })}`);
}
@@ -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 : "";
}
}
},
});
@@ -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;
@@ -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,
};
+16 -14
View File
@@ -6,36 +6,38 @@
// Kubernetes apis
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/
export * from "./cluster.api";
export * from "./cluster-role.api";
export * from "./cluster-role-binding.api";
export * from "./cluster-role.api";
export * from "./cluster.api";
export * from "./configmap.api";
export * from "./crd.api";
export * from "./cron-job.api";
export * from "./custom-resource-definition.api";
export * from "./daemon-set.api";
export * from "./deployment.api";
export * from "./endpoint.api";
export * from "./events.api";
export * from "./hpa.api";
export * from "./event.api";
export * from "./helm-release.api";
export * from "./helm-chart.api";
export * from "./horizontal-pod-autoscaler.api";
export * from "./ingress.api";
export * from "./job.api";
export * from "./limit-range.api";
export * from "./namespaces.api";
export * from "./namespace.api";
export * from "./network-policy.api";
export * from "./nodes.api";
export * from "./persistent-volume.api";
export * from "./node.api";
export * from "./persistent-volume-claims.api";
export * from "./pods.api";
export * from "./poddisruptionbudget.api";
export * from "./persistent-volume.api";
export * from "./pod-disruption-budget.api";
export * from "./pod-metrics.api";
export * from "./podsecuritypolicy.api";
export * from "./pod-security-policy.api";
export * from "./pod.api";
export * from "./replica-set.api";
export * from "./resource-quota.api";
export * from "./role.api";
export * from "./role-binding.api";
export * from "./role.api";
export * from "./secret.api";
export * from "./selfsubjectrulesreviews.api";
export * from "./self-subject-rules-review.api";
export * from "./service-account.api";
export * from "./service.api";
export * from "./service-accounts.api";
export * from "./stateful-set.api";
export * from "./storage-class.api";
@@ -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 { IngressApi } from "./ingress.api";
const ingressApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/networking.k8s.io/v1/ingresses", "/apis/extensions/v1beta1/ingresses") as IngressApi,
lifecycle: lifecycleEnum.singleton,
});
export default ingressApiInjectable;
+10 -17
View File
@@ -6,14 +6,10 @@
import { KubeObject } from "../kube-object";
import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api";
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";
import type { RequireExactlyOne } from "type-fest";
export class IngressApi extends KubeApi<Ingress> {
}
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
const opts = { category: "ingress", ingress, namespace };
@@ -194,17 +190,14 @@ export class Ingress extends KubeObject {
}
}
let ingressApi: IngressApi;
export class IngressApi extends KubeApi<Ingress> {
constructor(args: SpecificApiOptions<Ingress> = {}) {
args.checkPreferredVersion ??= true;
(args.fallbackApiBases ??= []).push("/apis/extensions/v1beta1/ingresses");
if (isClusterPageContext()) {
ingressApi = new IngressApi({
objectConstructor: Ingress,
// Add fallback for Kubernetes <1.19
checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
});
super({
...args,
objectConstructor: Ingress,
});
}
}
export {
ingressApi,
};
@@ -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 { JobApi } from "./job.api";
const jobApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/batch/v1/jobs") as JobApi,
lifecycle: lifecycleEnum.singleton,
});
export default jobApiInjectable;
+11 -19
View File
@@ -3,14 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import get from "lodash/get";
import { autoBind } from "../../utils";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
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 Job extends WorkloadKubeObject {
@@ -97,15 +95,12 @@ export class Job extends WorkloadKubeObject {
}
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
const { containers = [] } = this.spec?.template?.spec ?? {};
return [...containers].map(container => container.image);
return containers.map(container => container.image);
}
}
export class JobApi extends KubeApi<Job> {
}
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<IPodMetrics> {
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
@@ -123,14 +118,11 @@ export function getMetricsForJobs(jobs: Job[], namespace: string, selector = "")
});
}
let jobApi: JobApi;
if (isClusterPageContext()) {
jobApi = new JobApi({
objectConstructor: Job,
});
export class JobApi extends KubeApi<Job> {
constructor(args: SpecificApiOptions<Job> = {}) {
super({
...args,
objectConstructor: Job,
});
}
}
export {
jobApi,
};
@@ -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 { LimitRangeApi } from "./limit-range.api";
const limitRangeApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/limitranges") as LimitRangeApi,
lifecycle: lifecycleEnum.singleton,
});
export default limitRangeApiInjectable;
@@ -4,10 +4,9 @@
*/
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { autoBind } from "../../utils";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export enum LimitType {
CONTAINER = "Container",
@@ -65,14 +64,11 @@ export class LimitRange extends KubeObject {
}
}
let limitRangeApi: KubeApi<LimitRange>;
if (isClusterPageContext()) {
limitRangeApi = new KubeApi<LimitRange>({
objectConstructor: LimitRange,
});
export class LimitRangeApi extends KubeApi<LimitRange> {
constructor(args: SpecificApiOptions<LimitRange> = {}) {
super({
...args,
objectConstructor: LimitRange,
});
}
}
export {
limitRangeApi,
};
+6 -6
View File
@@ -7,7 +7,7 @@
import moment from "moment";
import { apiBase } from "../index";
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
import type { IMetricsQuery } from "../../../main/routes/metrics/route";
export interface IMetrics {
status: string;
@@ -56,7 +56,7 @@ export interface IResourceMetrics<T extends IMetrics> {
}
export const metricsApi = {
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
const { range = 3600, step = 60, namespace } = reqParams;
let { start, end } = reqParams;
@@ -77,7 +77,7 @@ export const metricsApi = {
});
},
async getMetricProviders(): Promise<MetricProviderInfo[]> {
getMetricProviders(): Promise<MetricProviderInfo[]> {
return apiBase.get("/metrics/providers");
},
};
@@ -141,8 +141,8 @@ export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
}
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | void {
if (!metrics) return;
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | null {
if (!metrics) return null;
const itemMetrics = { ...metrics };
for (const metric in metrics) {
@@ -159,7 +159,7 @@ export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: stri
}
export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
const result: Partial<{ [metric: string]: number }> = {};
const result: Partial<Record<string, number>> = {};
Object.keys(metrics).forEach(metricName => {
try {
@@ -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 { NamespaceApi } from "./namespace.api";
const namespaceApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/namespaces") as NamespaceApi,
lifecycle: lifecycleEnum.singleton,
});
export default namespaceApiInjectable;
@@ -3,13 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { KubeApi } from "../kube-api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import { KubeObject } from "../kube-object";
import { autoBind } from "../../../renderer/utils";
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";
export enum NamespaceStatus {
ACTIVE = "Active",
@@ -37,9 +36,6 @@ export class Namespace extends KubeObject {
}
}
export class NamespaceApi extends KubeApi<Namespace> {
}
export function getMetricsForNamespace(namespace: string, selector = ""): Promise<IPodMetrics> {
const opts = { category: "pods", pods: ".*", namespace, selector };
@@ -56,14 +52,11 @@ export function getMetricsForNamespace(namespace: string, selector = ""): Promis
});
}
let namespacesApi: NamespaceApi;
if (isClusterPageContext()) {
namespacesApi = new NamespaceApi({
objectConstructor: Namespace,
});
export class NamespaceApi extends KubeApi<Namespace> {
constructor(args: SpecificApiOptions<Namespace> = {}) {
super({
...args,
objectConstructor: Namespace,
});
}
}
export {
namespacesApi,
};
@@ -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 { NetworkPolicyApi } from "./network-policy.api";
const networkPolicyApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/apis/networking.k8s.io/v1/networkpolicies") as NetworkPolicyApi,
lifecycle: lifecycleEnum.singleton,
});
export default networkPolicyApiInjectable;
@@ -3,11 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { KubeObject, LabelSelector } from "../kube-object";
import { KubeObject, KubeObjectMetadata, KubeObjectStatus, LabelSelector } from "../kube-object";
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 interface IPolicyIpBlock {
cidr: string;
@@ -100,16 +99,12 @@ export interface NetworkPolicySpec {
egress?: IPolicyEgress[];
}
export interface NetworkPolicy {
spec: NetworkPolicySpec;
}
export class NetworkPolicy extends KubeObject {
export class NetworkPolicy extends KubeObject<KubeObjectMetadata, KubeObjectStatus, NetworkPolicySpec> {
static kind = "NetworkPolicy";
static namespaced = true;
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
constructor(data: KubeJsonApiData) {
constructor(data: KubeJsonApiData<KubeObjectMetadata, KubeObjectStatus, NetworkPolicySpec>) {
super(data);
autoBind(this);
}
@@ -129,14 +124,11 @@ export class NetworkPolicy extends KubeObject {
}
}
let networkPolicyApi: KubeApi<NetworkPolicy>;
if (isClusterPageContext()) {
networkPolicyApi = new KubeApi<NetworkPolicy>({
objectConstructor: NetworkPolicy,
});
export class NetworkPolicyApi extends KubeApi<NetworkPolicy> {
constructor(args: SpecificApiOptions<NetworkPolicy> = {}) {
super({
...args,
objectConstructor: NetworkPolicy,
});
}
}
export {
networkPolicyApi,
};
@@ -0,0 +1,15 @@
/**
* 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 { NodeApi } from "./node.api";
import apiManagerInjectable from "../api-manager.injectable";
const nodeApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/nodes") as NodeApi,
lifecycle: lifecycleEnum.singleton,
});
export default nodeApiInjectable;
@@ -6,12 +6,9 @@
import { KubeObject } from "../kube-object";
import { autoBind, cpuUnitsToNumber, iter, unitsToBytes } from "../../../renderer/utils";
import { IMetrics, metricsApi } from "./metrics.api";
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 NodesApi extends KubeApi<Node> {
}
export function getMetricsForAllNodes(): Promise<INodeMetrics> {
const opts = { category: "nodes" };
@@ -171,7 +168,7 @@ export class Node extends KubeObject {
}
const roleLabels = Object.keys(this.metadata.labels)
.filter(key => key.includes("node-role.kubernetes.io"))
.filter(key => key.startsWith("node-role.kubernetes.io/"))
.map(key => key.match(/([^/]+$)/)[0]); // all after last slash
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
@@ -231,14 +228,11 @@ export class Node extends KubeObject {
}
}
let nodesApi: NodesApi;
if (isClusterPageContext()) {
nodesApi = new NodesApi({
objectConstructor: Node,
});
export class NodeApi extends KubeApi<Node> {
constructor(args: SpecificApiOptions<Node> = {}) {
super({
...args,
objectConstructor: Node,
});
}
}
export {
nodesApi,
};
@@ -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 { PersistentVolumeClaimApi } from "./persistent-volume-claims.api";
const persistentVolumeClaimApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/persistentvolumeclaims") as PersistentVolumeClaimApi,
lifecycle: lifecycleEnum.singleton,
});
export default persistentVolumeClaimApiInjectable;
@@ -6,13 +6,9 @@
import { KubeObject, LabelSelector } from "../kube-object";
import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api";
import type { Pod } from "./pods.api";
import { KubeApi } from "../kube-api";
import type { Pod } from "./pod.api";
import { KubeApi, SpecificApiOptions } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
}
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetrics> {
const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() };
@@ -94,14 +90,11 @@ export class PersistentVolumeClaim extends KubeObject {
}
}
let pvcApi: PersistentVolumeClaimsApi;
if (isClusterPageContext()) {
pvcApi = new PersistentVolumeClaimsApi({
objectConstructor: PersistentVolumeClaim,
});
export class PersistentVolumeClaimApi extends KubeApi<PersistentVolumeClaim> {
constructor(args: SpecificApiOptions<PersistentVolumeClaim> = {}) {
super({
...args,
objectConstructor: PersistentVolumeClaim,
});
}
}
export {
pvcApi,
};
@@ -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 { PersistentVolumeApi } from "./persistent-volume.api";
const persistentVolumeApiInjectable = getInjectable({
instantiate: (di) => di.inject(apiManagerInjectable).getApi("/api/v1/persistentvolumes") as PersistentVolumeApi,
lifecycle: lifecycleEnum.singleton,
});
export default persistentVolumeApiInjectable;
@@ -5,9 +5,8 @@
import { KubeObject } from "../kube-object";
import { autoBind, unitsToBytes } 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 interface PersistentVolume {
spec: {
@@ -86,14 +85,11 @@ export class PersistentVolume extends KubeObject {
}
}
let persistentVolumeApi: KubeApi<PersistentVolume>;
if (isClusterPageContext()) {
persistentVolumeApi = new KubeApi({
objectConstructor: PersistentVolume,
});
export class PersistentVolumeApi extends KubeApi<PersistentVolume> {
constructor(args: SpecificApiOptions<PersistentVolume> = {}) {
super({
...args,
objectConstructor: PersistentVolume,
});
}
}
export {
persistentVolumeApi,
};
Loaded 100 of 1345 files, more files were not shown because too many files have changed in this diff. Show more