Remove BaseStore<T> and replace with composition of dependencies (#7002)

* Rename ExtensionStore -> BaseExtensionStore
- The name was too close to ExtensionsStore

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move ExtensionsStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move ClusterStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move UserStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup types to remove multiple cast locations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move HotbarStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move WeblinkStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move FileSystemProvisionerStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Clean up impl and rename to better describe intent

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix remaining type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the enabled extensions storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the clusters storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the hotbar storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the weblinks storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully split apart the user preferences storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crashing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests and snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix integration test failures

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve typing to prevent errors in the future.

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup @k8slens/messaging and friends

- To fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint issue

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix global override not being complete enough causing tests to fail

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Bump memory for unit tests on CI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Attempt to fix memory issue on CI again

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup test because of new injectables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Upgrade Jest

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests falling over

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Back out jest config change

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove console log

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshot

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by matching equality instead of snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by forcing specific snapshot style

- Ubuntu CI seems to format arrays in snapshots differently than macOS locally

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2023-03-28 17:54:25 +03:00
1 parent ba4a283af9
commit 49f0a1af9c
365 files changed
+6733 -3972

No files matched your search

@@ -1,366 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterStore } from "../cluster-store/cluster-store";
import type { GetCustomKubeConfigFilePath } from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import getCustomKubeConfigFilePathInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import assert from "assert";
import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable";
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import type { WriteJsonSync } from "../fs/write-json-sync.injectable";
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
import type { ReadFileSync } from "../fs/read-file-sync.injectable";
import readFileSyncInjectable from "../fs/read-file-sync.injectable";
import { readFileSync } from "fs";
import type { WriteFileSync } from "../fs/write-file-sync.injectable";
import writeFileSyncInjectable from "../fs/write-file-sync.injectable";
import type { WriteBufferSync } from "../fs/write-buffer-sync.injectable";
import writeBufferSyncInjectable from "../fs/write-buffer-sync.injectable";
import { Cluster } from "../cluster/cluster";
// NOTE: this is intended to read the actual file system
const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png");
const clusterServerUrl = "https://localhost";
const kubeconfig = `
apiVersion: v1
clusters:
- cluster:
server: ${clusterServerUrl}
name: test
contexts:
- context:
cluster: test
user: test
name: foo
- context:
cluster: test
user: test
name: foo2
current-context: test
kind: Config
preferences: {}
users:
- name: test
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
describe("cluster-store", () => {
let di: DiContainer;
let clusterStore: ClusterStore;
let writeJsonSync: WriteJsonSync;
let writeFileSync: WriteFileSync;
let writeBufferSync: WriteBufferSync;
let readFileSync: ReadFileSync;
let getCustomKubeConfigFilePath: GetCustomKubeConfigFilePath;
let writeFileSyncAndReturnPath: (filePath: string, contents: string) => string;
beforeEach(async () => {
di = getDiForUnitTesting();
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "/some-temp-directory");
di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin");
writeJsonSync = di.inject(writeJsonSyncInjectable);
writeFileSync = di.inject(writeFileSyncInjectable);
writeBufferSync = di.inject(writeBufferSyncInjectable);
readFileSync = di.inject(readFileSyncInjectable);
writeFileSyncAndReturnPath = (filePath, contents) => (writeFileSync(filePath, contents), filePath);
});
describe("empty config", () => {
beforeEach(async () => {
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {});
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
describe("with foo cluster added", () => {
beforeEach(() => {
const cluster = new Cluster({
id: "foo",
contextName: "foo",
preferences: {
terminalCWD: "/some-directory-for-user-data",
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube",
},
kubeConfigPath: writeFileSyncAndReturnPath(
getCustomKubeConfigFilePath("foo"),
kubeconfig,
),
}, {
clusterServerUrl,
});
clusterStore.addCluster(cluster);
});
it("adds new cluster to store", async () => {
const storedCluster = clusterStore.getById("foo");
assert(storedCluster);
expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
expect(storedCluster.preferences.icon).toBe(
"data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
);
});
});
describe("with prod and dev clusters added", () => {
beforeEach(() => {
const store = clusterStore;
store.addCluster({
id: "prod",
contextName: "foo",
preferences: {
clusterName: "prod",
},
kubeConfigPath: writeFileSyncAndReturnPath(
getCustomKubeConfigFilePath("prod"),
kubeconfig,
),
});
store.addCluster({
id: "dev",
contextName: "foo2",
preferences: {
clusterName: "dev",
},
kubeConfigPath: writeFileSyncAndReturnPath(
getCustomKubeConfigFilePath("dev"),
kubeconfig,
),
});
});
it("check if store can contain multiple clusters", () => {
expect(clusterStore.hasClusters()).toBeTruthy();
expect(clusterStore.clusters.size).toBe(2);
});
it("check if cluster's kubeconfig file saved", () => {
const file = writeFileSyncAndReturnPath(getCustomKubeConfigFilePath("boo"), "kubeconfig");
expect(readFileSync(file)).toBe("kubeconfig");
});
});
});
describe("config with existing clusters", () => {
beforeEach(() => {
writeFileSync("/temp-kube-config", kubeconfig);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {
__internal__: {
migrations: {
version: "99.99.99",
},
},
clusters: [
{
id: "cluster1",
kubeConfigPath: "/temp-kube-config",
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "default",
},
{
id: "cluster2",
kubeConfigPath: "/temp-kube-config",
contextName: "foo2",
preferences: { terminalCWD: "/foo2" },
},
{
id: "cluster3",
kubeConfigPath: "/temp-kube-config",
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "foo",
ownerRef: "foo",
},
],
});
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1");
assert(storedCluster);
expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
it("allows getting all of the clusters", async () => {
const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1");
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
expect(storedClusters[1].id).toBe("cluster2");
expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2");
expect(storedClusters[2].id).toBe("cluster3");
});
});
describe("config with invalid cluster kubeconfig", () => {
beforeEach(() => {
writeFileSync("/invalid-kube-config", invalidKubeconfig);
writeFileSync("/valid-kube-config", kubeconfig);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {
__internal__: {
migrations: {
version: "99.99.99",
},
},
clusters: [
{
id: "cluster1",
kubeConfigPath: "/invalid-kube-config",
contextName: "test",
preferences: { terminalCWD: "/foo" },
workspace: "foo",
},
{
id: "cluster2",
kubeConfigPath: "/valid-kube-config",
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "default",
},
],
});
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(1);
});
});
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => {
di.override(storeMigrationVersionInjectable, () => "3.6.0");
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {
__internal__: {
migrations: {
version: "3.5.0",
},
},
clusters: [
{
id: "cluster1",
kubeConfig: minimalValidKubeConfig,
contextName: "cluster",
preferences: {
icon: "store://icon_path",
},
},
],
});
writeBufferSync("/some-directory-for-user-data/icon_path", testDataIcon);
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
it("migrates to modern format with kubeconfig in a file", async () => {
const configPath = clusterStore.clustersList[0].kubeConfigPath.get();
expect(readFileSync(configPath)).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", async () => {
expect(clusterStore.clustersList[0].preferences.icon).toMatch(/data:;base64,/);
});
});
});
const invalidKubeconfig = JSON.stringify({
apiVersion: "v1",
clusters: [{
cluster: {
server: "https://localhost",
},
name: "test2",
}],
contexts: [{
context: {
cluster: "test",
user: "test",
},
name: "test",
}],
"current-context": "test",
kind: "Config",
preferences: {},
users: [{
user: {
token: "kubeconfig-user-q4lm4:xxxyyyy",
},
name: "test",
}],
});
const minimalValidKubeConfig = JSON.stringify({
apiVersion: "v1",
clusters: [
{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
},
],
"current-context": "minikube",
contexts: [
{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
},
],
users: [
{
name: "minikube",
user: {
"client-certificate": "/Users/foo/.minikube/client.crt",
"client-key": "/Users/foo/.minikube/client.key",
},
},
],
kind: "Config",
preferences: {},
});
@@ -1,356 +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 type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { DiContainer } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../hotbars/store.injectable";
import type { HotbarStore } from "../hotbars/store";
import catalogEntityRegistryInjectable from "../../main/catalog/entity-registry.injectable";
import { computed } from "mobx";
import hasCategoryForEntityInjectable from "../catalog/has-category-for-entity.injectable";
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return {
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: {},
...data,
} as CatalogEntity;
}
describe("HotbarStore", () => {
let di: DiContainer;
let hotbarStore: HotbarStore;
let testCluster: CatalogEntity;
let minikubeCluster: CatalogEntity;
let awsCluster: CatalogEntity;
let loggerMock: jest.Mocked<Logger>;
beforeEach(async () => {
di = getDiForUnitTesting();
testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-test-id",
name: "my-test-cluster",
source: "local",
labels: {},
},
});
minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-minikube-id",
name: "my-minikube-cluster",
source: "local",
labels: {},
},
});
awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-aws-id",
name: "my-aws-cluster",
source: "local",
labels: {},
},
});
di.override(hasCategoryForEntityInjectable, () => () => true);
loggerMock = {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
silly: jest.fn(),
};
di.override(loggerInjectable, () => loggerMock);
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
catalogEntityRegistry.addComputedSource("some-id", computed(() => [
testCluster,
minikubeCluster,
awsCluster,
catalogCatalogEntity,
]));
});
describe("given no previous data in store, running all migrations", () => {
beforeEach(() => {
hotbarStore = di.inject(hotbarStoreInjectable);
hotbarStore.load();
});
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");
});
it("adds items", () => {
hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("removes items", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("some-test-id");
hotbarStore.removeFromHotbar("catalog-entity");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items).toStrictEqual([]);
});
it("does nothing if removing with invalid uid", () => {
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", () => {
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("some-test-id");
});
it("moves items down", () => {
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(["some-aws-id", "catalog-entity", "some-test-id", "some-minikube-id"]);
});
it("moves items up", () => {
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", "some-minikube-id", "some-aws-id", "some-test-id"]);
});
it("logs an error if cellIndex is out of bounds", () => {
hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.setActiveHotbar("hottest");
hotbarStore.addToHotbar(testCluster, -1);
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 12);
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 13);
expect(loggerMock.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(() => 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", () => {
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", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1);
expect(hotbarStore.getActive().items[1]?.entity.uid).toEqual("some-test-id");
});
it("new items takes first empty cell", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikubeCluster);
expect(hotbarStore.getActive().items[0]?.entity.uid).toEqual("some-minikube-id");
});
it("throws if invalid arguments provided", () => {
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();
});
it("checks if entity already pinned to hotbar", () => {
hotbarStore.addToHotbar(testCluster);
expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy();
expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy();
});
});
});
describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
beforeEach(() => {
const writeJsonSync = di.inject(writeJsonSyncInjectable);
writeJsonSync("/some-directory-for-user-data/lens-hotbar-store.json", {
__internal__: {
migrations: {
version: "5.0.0-beta.3",
},
},
hotbars: [
{
id: "3caac17f-aec2-4723-9694-ad204465d935",
name: "myhotbar",
items: [
{
entity: {
uid: "some-aws-id",
},
},
{
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,
],
},
],
});
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
hotbarStore = di.inject(hotbarStoreInjectable);
hotbarStore.load();
});
it("allows to retrieve a hotbar", () => {
const hotbar = hotbarStore.findById("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: "my-aws-cluster",
source: "local",
uid: "some-aws-id",
},
});
});
});
});
@@ -2,11 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { UserStore } from "../user-store";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } 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 { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
@@ -15,9 +12,16 @@ import releaseChannelInjectable from "../vars/release-channel.injectable";
import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
import writeFileSyncInjectable from "../fs/write-file-sync.injectable";
import type { UserPreferencesState } from "../../features/user-preferences/common/state.injectable";
import userPreferencesStateInjectable from "../../features/user-preferences/common/state.injectable";
import userPreferencesPersistentStorageInjectable from "../../features/user-preferences/common/storage.injectable";
import type { ResetTheme } from "../../features/user-preferences/common/reset-theme.injectable";
import resetThemeInjectable from "../../features/user-preferences/common/reset-theme.injectable";
import type { ClusterStoreModel } from "../../features/cluster/storage/common/storage.injectable";
describe("user store tests", () => {
let userStore: UserStore;
let state: UserPreferencesState;
let resetTheme: ResetTheme;
let di: DiContainer;
beforeEach(async () => {
@@ -33,6 +37,8 @@ describe("user store tests", () => {
await di.inject(defaultUpdateChannelInjectable).init();
state = di.inject(userPreferencesStateInjectable);
resetTheme = di.inject(resetThemeInjectable);
});
describe("for an empty config", () => {
@@ -42,25 +48,23 @@ describe("user store tests", () => {
writeJsonSync("/some-directory-for-user-data/lens-user-store.json", {});
writeJsonSync("/some-directory-for-user-data/kube_config", {});
userStore = di.inject(userStoreInjectable);
userStore.load();
di.inject(userPreferencesPersistentStorageInjectable).loadAndStartSyncing();
});
it("allows setting and getting preferences", () => {
userStore.httpsProxy = "abcd://defg";
state.httpsProxy = "abcd://defg";
expect(userStore.httpsProxy).toBe("abcd://defg");
expect(userStore.colorTheme).toBe(defaultThemeId);
expect(state.httpsProxy).toBe("abcd://defg");
expect(state.colorTheme).toBe(defaultThemeId);
userStore.colorTheme = "light";
expect(userStore.colorTheme).toBe("light");
state.colorTheme = "light";
expect(state.colorTheme).toBe("light");
});
it("correctly resets theme to default value", async () => {
userStore.colorTheme = "some other theme";
userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultThemeId);
state.colorTheme = "some other theme";
resetTheme();
expect(state.colorTheme).toBe(defaultThemeId);
});
});
@@ -92,18 +96,16 @@ describe("user store tests", () => {
di.override(storeMigrationVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable);
userStore.load();
di.inject(userPreferencesPersistentStorageInjectable).loadAndStartSyncing();
});
it("skips clusters for adding to kube-sync with files under extension_data/", () => {
expect(userStore.syncKubeconfigEntries.has("/some-directory-for-user-data/extension_data/foo/bar")).toBe(false);
expect(userStore.syncKubeconfigEntries.has("/some/other/path")).toBe(true);
expect(state.syncKubeconfigEntries.has("/some-directory-for-user-data/extension_data/foo/bar")).toBe(false);
expect(state.syncKubeconfigEntries.has("/some/other/path")).toBe(true);
});
it("allows access to the colorTheme preference", () => {
expect(userStore.colorTheme).toBe("light");
expect(state.colorTheme).toBe("light");
});
});
});
@@ -1,156 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type Config from "conf";
import type { Migrations, Options as ConfOptions } from "conf/dist/source/types";
import type { IEqualsComparer } from "mobx";
import { makeObservable, reaction } from "mobx";
import { disposer, isPromiseLike } from "@k8slens/utilities";
import { broadcastMessage } from "../ipc";
import isEqual from "lodash/isEqual";
import { kebabCase } from "lodash";
import type { GetConfigurationFileModel } from "../get-configuration-file-model/get-configuration-file-model.injectable";
import type { Logger } from "../logger";
import type { PersistStateToConfig } from "./save-to-file";
import type { GetBasenameOfPath } from "../path/get-basename.injectable";
import type { EnlistMessageChannelListener } from "@k8slens/messaging";
import { toJS } from "../utils";
export interface BaseStoreParams<T> extends Omit<ConfOptions<T>, "migrations"> {
syncOptions?: {
fireImmediately?: boolean;
equals?: IEqualsComparer<T>;
};
configName: string;
}
export interface IpcChannelPrefixes {
local: string;
remote: string;
}
export interface BaseStoreDependencies {
readonly logger: Logger;
readonly storeMigrationVersion: string;
readonly directoryForUserData: string;
readonly migrations: Migrations<Record<string, unknown>>;
readonly ipcChannelPrefixes: IpcChannelPrefixes;
readonly shouldDisableSyncInListener: boolean;
getConfigurationFileModel: GetConfigurationFileModel;
persistStateToConfig: PersistStateToConfig;
getBasenameOfPath: GetBasenameOfPath;
enlistMessageChannelListener: EnlistMessageChannelListener;
}
/**
* Note: T should only contain base JSON serializable types.
*/
export abstract class BaseStore<T extends object> {
private readonly syncDisposers = disposer();
readonly displayName = kebabCase(this.params.configName).toUpperCase();
/**
* @ignore
*/
protected readonly dependencies: BaseStoreDependencies;
protected constructor(
dependencies: BaseStoreDependencies,
protected readonly params: BaseStoreParams<T>,
) {
this.dependencies = dependencies;
makeObservable(this);
}
/**
* This must be called after the last child's constructor is finished (or just before it finishes)
*/
load() {
this.dependencies.logger.info(`[${this.displayName}]: LOADING ...`);
const config = this.dependencies.getConfigurationFileModel({
projectName: "lens",
projectVersion: this.dependencies.storeMigrationVersion,
cwd: this.cwd(),
...this.params,
migrations: this.dependencies.migrations as Migrations<T>,
});
const res = this.fromStore(config.store);
if (isPromiseLike(res)) {
this.dependencies.logger.error(`${this.displayName} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
}
this.startSyncing(config);
this.dependencies.logger.info(`[${this.displayName}]: LOADED from ${config.path}`);
}
protected cwd() {
return this.dependencies.directoryForUserData;
}
private startSyncing(config: Config<T>) {
const name = this.dependencies.getBasenameOfPath(config.path);
const disableSync = () => this.syncDisposers();
const enableSync = () => {
this.syncDisposers.push(
reaction(
() => toJS(this.toJSON()), // unwrap possible observables and react to everything
model => {
this.dependencies.persistStateToConfig(config, model);
broadcastMessage(`${this.dependencies.ipcChannelPrefixes.remote}:${config.path}`, model);
},
this.params.syncOptions,
),
this.dependencies.enlistMessageChannelListener({
id: this.displayName,
channel: {
id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`,
},
handler: (model) => {
this.dependencies.logger.silly(`[${this.displayName}]: syncing ${name}`, { model });
if (this.dependencies.shouldDisableSyncInListener) {
disableSync();
}
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
if (!isEqual(this.toJSON(), model)) {
this.fromStore(model as T);
}
if (this.dependencies.shouldDisableSyncInListener) {
enableSync();
}
},
}),
);
};
enableSync();
}
/**
* fromStore is called internally when a child class syncs with the file
* system.
*
* Note: This function **must** be synchronous.
*
* @param data the parsed information read from the stored JSON file
*/
protected abstract fromStore(data: T): void;
/**
* toJSON is called when syncing the store to the filesystem. It should
* produce a JSON serializable object representation of the current state.
*
* It is recommended that a round trip is valid. Namely, calling
* `this.fromStore(this.toJSON())` shouldn't change the state.
*/
abstract toJSON(): T;
}
@@ -1,11 +0,0 @@
/**
* 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 { IpcChannelPrefixes } from "./base-store";
export const baseStoreIpcChannelPrefixesInjectionToken = getInjectionToken<IpcChannelPrefixes>({
id: "base-store-ipc-channel-prefix-token",
});
@@ -1,46 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InjectionToken } from "@ogre-tools/injectable";
import { lifecycleEnum, getInjectable } from "@ogre-tools/injectable";
import type Conf from "conf/dist/source";
import type { Migrations } from "conf/dist/source/types";
import loggerInjectable from "../logger.injectable";
import { getOrInsert, iter } from "@k8slens/utilities";
export interface MigrationDeclaration {
version: string;
run(store: Conf<Partial<Record<string, unknown>>>): void;
}
const storeMigrationsInjectable = getInjectable({
id: "store-migrations",
instantiate: (di, token): Migrations<Record<string, unknown>> => {
const logger = di.inject(loggerInjectable);
const declarations = di.injectMany(token);
const migrations = new Map<string, MigrationDeclaration["run"][]>();
for (const decl of declarations) {
getOrInsert(migrations, decl.version, []).push(decl.run);
}
return Object.fromEntries(
iter.map(
migrations,
([v, fns]) => [v, (store) => {
logger.info(`Running ${v} migration for ${store.path}`);
for (const fn of fns) {
fn(store);
}
}],
),
);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, token: InjectionToken<MigrationDeclaration, void>) => token.id,
}),
});
export default storeMigrationsInjectable;
@@ -4,10 +4,10 @@
*/
import { getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import removeWeblinkInjectable from "../../features/weblinks/common/remove.injectable";
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import productNameInjectable from "../vars/product-name.injectable";
import weblinkStoreInjectable from "../weblinks-store/weblink-store.injectable";
export type WebLinkStatusPhase = "available" | "unavailable";
@@ -34,13 +34,13 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
// NOTE: this is safe because `onContextMenuOpen` is only supposed to be called in the renderer
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi("renderer");
const productName = di.inject(productNameInjectable);
const weblinkStore = di.inject(weblinkStoreInjectable);
const removeWeblink = di.inject(removeWeblinkInjectable);
if (this.metadata.source === "local") {
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: async () => weblinkStore.removeById(this.getId()),
onClick: () => removeWeblink(this.getId()),
confirm: {
message: `Remove Web Link "${this.getName()}" from ${productName}?`,
},
@@ -0,0 +1,83 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { CatalogEntity } from "./catalog-entity";
import GraphemeSplitter from "grapheme-splitter";
import { hasOwnProperty, hasTypedProperty, isObject, isString, iter } from "@k8slens/utilities";
function getNameParts(name: string): string[] {
const byWhitespace = name.split(/\s+/);
if (byWhitespace.length > 1) {
return byWhitespace;
}
const byDashes = name.split(/[-_]+/);
if (byDashes.length > 1) {
return byDashes;
}
return name.split(/@+/);
}
export function limitGraphemeLengthOf(src: string, count: number): string {
const splitter = new GraphemeSplitter();
return iter
.chain(splitter.iterateGraphemes(src))
.take(count)
.join("");
}
export function computeDefaultShortName(name: string) {
if (!name || typeof name !== "string") {
return "??";
}
const [rawFirst, rawSecond, rawThird] = getNameParts(name);
const splitter = new GraphemeSplitter();
const first = splitter.iterateGraphemes(rawFirst);
const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first;
const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty<string>();
return iter.chain(iter.take(first, 1))
.concat(iter.take(second, 1))
.concat(iter.take(third, 1))
.join("");
}
export function getShortName(entity: CatalogEntity): string {
return entity.metadata.shortName || computeDefaultShortName(entity.getName());
}
export function getIconColourHash(entity: CatalogEntity): string {
return `${entity.metadata.name}-${entity.metadata.source}`;
}
export function getIconBackground(entity: CatalogEntity): string | undefined {
if (isObject(entity.spec.icon)) {
if (hasTypedProperty(entity.spec.icon, "background", isString)) {
return entity.spec.icon.background;
}
return hasOwnProperty(entity.spec.icon, "src")
? "transparent"
: undefined;
}
return undefined;
}
export function getIconMaterial(entity: CatalogEntity): string | undefined {
if (
isObject(entity.spec.icon)
&& hasTypedProperty(entity.spec.icon, "material", isString)
) {
return entity.spec.icon.material;
}
return undefined;
}
@@ -1,12 +0,0 @@
/**
* 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 { IComputedValue } from "mobx";
import type { KubeApiResourceDescriptor } from "../rbac";
export const shouldShowResourceInjectionToken = getInjectionToken<IComputedValue<boolean>, KubeApiResourceDescriptor>({
id: "should-show-resource",
});
@@ -1,40 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { clusterStoreMigrationInjectionToken } from "./migration-token";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
const clusterStoreInjectable = getInjectable({
id: "cluster-store",
instantiate: (di) => new ClusterStore({
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(storeMigrationsInjectable, clusterStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
});
export default clusterStoreInjectable;
@@ -1,107 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, comparer, computed, makeObservable, observable } from "mobx";
import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store/base-store";
import { Cluster } from "../cluster/cluster";
import { toJS } from "../utils";
import type { ClusterModel, ClusterId } from "../cluster-types";
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
export interface ClusterStoreModel {
clusters?: ClusterModel[];
}
interface Dependencies extends BaseStoreDependencies {
readClusterConfigSync: ReadClusterConfigSync;
emitAppEvent: EmitAppEvent;
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
readonly clusters = observable.map<ClusterId, Cluster>();
constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
});
makeObservable(this);
}
@computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values());
}
@computed get connectedClustersList(): Cluster[] {
return this.clustersList.filter((c) => !c.disconnected);
}
hasClusters() {
return this.clusters.size > 0;
}
getById(id: ClusterId | undefined): Cluster | undefined {
if (id) {
return this.clusters.get(id);
}
return undefined;
}
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
this.dependencies.emitAppEvent({ name: "cluster", action: "add" });
const cluster = clusterOrModel instanceof Cluster
? clusterOrModel
: new Cluster(
clusterOrModel,
this.dependencies.readClusterConfigSync(clusterOrModel),
);
this.clusters.set(cluster.id, cluster);
return cluster;
}
@action
protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = new Map(this.clusters);
const newClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) {
try {
let cluster = currentClusters.get(clusterModel.id);
if (cluster) {
cluster.updateModel(clusterModel);
} else {
cluster = new Cluster(
clusterModel,
this.dependencies.readClusterConfigSync(clusterModel),
);
}
newClusters.set(clusterModel.id, cluster);
} catch (error) {
this.dependencies.logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`);
}
}
this.clusters.replace(newClusters);
}
toJSON(): ClusterStoreModel {
return toJS({
clusters: this.clustersList.map(cluster => cluster.toJSON()),
});
}
}
@@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../cluster-types";
import type { Cluster } from "../cluster/cluster";
import clusterStoreInjectable from "./cluster-store.injectable";
export type GetClusterById = (id: ClusterId) => Cluster | undefined;
const getClusterByIdInjectable = getInjectable({
id: "get-cluster-by-id",
instantiate: (di): GetClusterById => {
const store = di.inject(clusterStoreInjectable);
return (id) => store.getById(id);
},
});
export default getClusterByIdInjectable;
@@ -1,11 +0,0 @@
/**
* 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 { MigrationDeclaration } from "../base-store/migrations.injectable";
export const clusterStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "cluster-store-migration",
});
@@ -1,31 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterConfigData, ClusterModel } from "../cluster-types";
import readFileSyncInjectable from "../fs/read-file-sync.injectable";
import { loadConfigFromString, validateKubeConfig } from "../kube-helpers";
export type ReadClusterConfigSync = (model: ClusterModel) => ClusterConfigData;
const readClusterConfigSyncInjectable = getInjectable({
id: "read-cluster-config-sync",
instantiate: (di): ReadClusterConfigSync => {
const readFileSync = di.inject(readFileSyncInjectable);
return ({ kubeConfigPath, contextName }) => {
const kubeConfigData = readFileSync(kubeConfigPath);
const { config } = loadConfigFromString(kubeConfigData);
const result = validateKubeConfig(config, contextName);
if (result.error) {
throw result.error;
}
return { clusterServerUrl: result.cluster.server };
};
},
});
export default readClusterConfigSyncInjectable;
@@ -9,7 +9,7 @@ import isProductionInjectable from "../vars/is-production.injectable";
import sentryDataSourceNameInjectable from "../vars/sentry-dsn-url.injectable";
import { Dedupe, Offline } from "@sentry/integrations";
import { inspect } from "util";
import userStoreInjectable from "../user-store/user-store.injectable";
import userPreferencesStateInjectable from "../../features/user-preferences/common/state.injectable";
export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void;
@@ -20,7 +20,7 @@ const initializeSentryReportingWithInjectable = getInjectable({
instantiate: (di): InitializeSentryReportingWith => {
const sentryDataSourceName = di.inject(sentryDataSourceNameInjectable);
const isProduction = di.inject(isProductionInjectable);
const userStore = di.inject(userStoreInjectable);
const state = di.inject(userPreferencesStateInjectable);
if (!sentryDataSourceName) {
return () => {};
@@ -28,7 +28,7 @@ const initializeSentryReportingWithInjectable = getInjectable({
return (initSentry) => initSentry({
beforeSend: (event) => {
if (userStore.allowErrorReporting) {
if (state.allowErrorReporting) {
return event;
}
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { HttpsProxyAgent } from "hpagent";
import userStoreInjectable from "../user-store/user-store.injectable";
import userPreferencesStateInjectable from "../../features/user-preferences/common/state.injectable";
import type { Fetch } from "./fetch.injectable";
import fetchInjectable from "./fetch.injectable";
@@ -12,7 +12,7 @@ const proxyFetchInjectable = getInjectable({
id: "proxy-fetch",
instantiate: (di): Fetch => {
const fetch = di.inject(fetchInjectable);
const { httpsProxy, allowUntrustedCAs } = di.inject(userStoreInjectable);
const { httpsProxy, allowUntrustedCAs } = di.inject(userPreferencesStateInjectable);
const agent = httpsProxy
? new HttpsProxyAgent({
proxy: httpsProxy,
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const configMapsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const horizontalPodAutoscalersRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const leasesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const limitRangesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const podDisruptionBudgetsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const priorityClassesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const resourceQuotasRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const runtimeClassesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const secretsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const verticalPodAutoscalersRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const eventsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const namespacesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const endpointsRouteInjectable = getInjectable({
@@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
import {
shouldShowResourceInjectionToken,
} from "../../../../../cluster-store/allowed-resources-injection-token";
} from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
const ingressClassesesRouteInjectable = getInjectable({
id: "ingress-classes-route",
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { computedOr } from "@k8slens/utilities";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const networkPoliciesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const servicesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const nodesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const clusterOverviewRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const persistentVolumeClaimsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const persistentVolumesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const storageClassesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const clusterRoleBindingsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const clusterRolesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const podSecurityPoliciesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const roleBindingsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const rolesRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const serviceAccountsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const cronJobsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const daemonsetsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const deploymentsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const jobsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const podsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const replicasetsRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const replicationControllersRouteInjectable = getInjectable({
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const statefulsetsRouteInjectable = getInjectable({
@@ -6,22 +6,14 @@ import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import { frontEndRouteInjectionToken } from "./front-end-route-injection-token";
import { filter, map } from "lodash/fp";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../cluster-store/cluster-store";
import { pipeline } from "@ogre-tools/fp";
describe("verify-that-all-routes-have-component", () => {
it("verify that routes have route component", () => {
const rendererDi = getDiForUnitTesting();
rendererDi.override(clusterStoreInjectable, () => ({
getById: () => null,
} as unknown as ClusterStore));
const routes = rendererDi.injectMany(frontEndRouteInjectionToken);
const routeComponents = rendererDi.injectMany(
routeSpecificComponentInjectionToken,
);
const routeComponents = rendererDi.injectMany(routeSpecificComponentInjectionToken);
const routesMissingComponent = pipeline(
routes,
@@ -22,6 +22,7 @@ const fsInjectable = getInjectable({
access,
stat,
unlink,
rename,
},
ensureDir,
ensureDirSync,
@@ -58,6 +59,7 @@ const fsInjectable = getInjectable({
createReadStream,
stat,
unlink,
rename,
};
},
causesSideEffects: true,
@@ -10,7 +10,7 @@ import getConfigurationFileModelInjectable from "./get-configuration-file-model.
import type Config from "conf";
import readJsonSyncInjectable from "../fs/read-json-sync.injectable";
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
import { get, set } from "lodash";
import { get, has, set } from "lodash";
import semver from "semver";
const MIGRATION_KEY = `__internal__.migrations.version`;
@@ -64,7 +64,7 @@ export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => {
path: configFilePath,
get: (key: string) => get(store, key),
set: (key: string, value: unknown) => {
let currentState: object;
let currentState: Partial<Record<string, unknown>>;
try {
currentState = readJsonSync(configFilePath);
@@ -78,6 +78,25 @@ export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => {
});
store = readJsonSync(configFilePath);
},
delete: (key: string) => {
let currentState: Partial<Record<string, unknown>>;
try {
currentState = readJsonSync(configFilePath);
} catch {
currentState = {};
}
delete currentState[key];
writeJsonSync(configFilePath, currentState);
store = readJsonSync(configFilePath);
},
has: (key: string) => has(store, key),
clear: () => {
writeJsonSync(configFilePath, {});
store = readJsonSync(configFilePath);
},
} as Partial<Config> as Config<any>;
// Migrate
@@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "./store.injectable";
import type { CreateHotbarData, CreateHotbarOptions } from "./types";
export type AddHotbar = (data: CreateHotbarData, opts?: CreateHotbarOptions) => void;
const addHotbarInjectable = getInjectable({
id: "add-hotbar",
instantiate: (di): AddHotbar => {
const store = di.inject(hotbarStoreInjectable);
return (data, opts) => store.add(data, opts);
},
});
export default addHotbarInjectable;
@@ -1,11 +0,0 @@
/**
* 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 { MigrationDeclaration } from "../base-store/migrations.injectable";
export const hotbarStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "hotbar-store-migration-token",
});
@@ -1,38 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { HotbarStore } from "./store";
import loggerInjectable from "../logger.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { hotbarStoreMigrationInjectionToken } from "./migrations-token";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
const hotbarStoreInjectable = getInjectable({
id: "hotbar-store",
instantiate: (di) => new HotbarStore({
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
logger: di.inject(loggerInjectable),
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(storeMigrationsInjectable, hotbarStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
});
export default hotbarStoreInjectable;
-351
View File
@@ -1,351 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, comparer, observable, makeObservable, computed } from "mobx";
import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store/base-store";
import { toJS } from "../utils";
import type { CatalogEntity } from "../catalog";
import { broadcastMessage } from "../ipc";
import type { Hotbar, CreateHotbarData, CreateHotbarOptions } from "./types";
import { defaultHotbarCells, getEmptyHotbar } from "./types";
import { hotbarTooManyItemsChannel } from "../ipc/hotbar";
import type { GeneralEntity } from "../catalog-entities";
import type { Logger } from "../logger";
import assert from "assert";
export interface HotbarStoreModel {
hotbars: Hotbar[];
activeHotbarId: string;
}
interface Dependencies extends BaseStoreDependencies {
readonly catalogCatalogEntity: GeneralEntity;
readonly logger: Logger;
}
export class HotbarStore extends BaseStore<HotbarStoreModel> {
@observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId!: string;
constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
});
makeObservable(this);
}
@computed get activeHotbarId() {
return this._activeHotbarId;
}
/**
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
* @param hotbar The hotbar instance, or the index, or its ID
*/
setActiveHotbar(hotbar: Hotbar | number | string) {
if (typeof hotbar === "number") {
if (hotbar >= 0 && hotbar < this.hotbars.length) {
this._activeHotbarId = this.hotbars[hotbar].id;
}
} else if (typeof hotbar === "string") {
if (this.findById(hotbar)) {
this._activeHotbarId = hotbar;
}
} else {
if (this.hotbars.indexOf(hotbar) >= 0) {
this._activeHotbarId = hotbar.id;
}
}
}
private hotbarIndexById(id: string) {
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
}
private hotbarIndex(hotbar: Hotbar) {
return this.hotbars.indexOf(hotbar);
}
@computed get activeHotbarIndex() {
return this.hotbarIndexById(this.activeHotbarId);
}
@action
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
if (!data.hotbars || !data.hotbars.length) {
const hotbar = getEmptyHotbar("Default");
const {
metadata: { uid, name, source },
} = this.dependencies.catalogCatalogEntity;
const initialItem = { entity: { uid, name, source }};
hotbar.items[0] = initialItem;
this.hotbars = [hotbar];
} else {
this.hotbars = data.hotbars;
}
this.hotbars.forEach(ensureExactHotbarItemLength);
if (data.activeHotbarId) {
this._activeHotbarId = data.activeHotbarId;
}
if (!this._activeHotbarId) {
this._activeHotbarId = this.hotbars[0].id;
}
}
toJSON(): HotbarStoreModel {
return toJS({
hotbars: this.hotbars,
activeHotbarId: this.activeHotbarId,
});
}
getActive(): Hotbar {
const hotbar = this.findById(this.activeHotbarId);
assert(hotbar, "There MUST always be an active hotbar");
return hotbar;
}
findByName(name: string) {
return this.hotbars.find((hotbar) => hotbar.name === name);
}
findById(id: string) {
return this.hotbars.find((hotbar) => hotbar.id === id);
}
@action
add(data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) {
const hotbar = getEmptyHotbar(data.name, data.id);
this.hotbars.push(hotbar);
if (setActive) {
this._activeHotbarId = hotbar.id;
}
}
@action
setHotbarName(id: string, name: string): void {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) {
return this.dependencies.logger.warn(
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
{ id },
);
}
this.hotbars[index].name = name;
}
@action
remove(hotbar: Hotbar) {
assert(this.hotbars.length >= 2, "Cannot remove the last hotbar");
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
if (this.activeHotbarId === hotbar.id) {
this.setActiveHotbar(0);
}
}
@action
addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive();
const uid = item.getId();
const name = item.getName();
if (typeof uid !== "string") {
throw new TypeError("CatalogEntity's ID must be a string");
}
if (typeof name !== "string") {
throw new TypeError("CatalogEntity's NAME must be a string");
}
if (this.isAddedToActive(item)) {
return;
}
const entity = {
uid,
name,
source: item.metadata.source,
};
const newItem = { entity };
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = hotbar.items.indexOf(null);
if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem;
} else {
broadcastMessage(hotbarTooManyItemsChannel);
}
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem;
} else {
this.dependencies.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) {
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
hotbar.items[index] = null;
}
}
}
findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
const hotbar = this.getActive();
while (hotbar.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() {
let index = this.activeHotbarIndex - 1;
if (index < 0) {
index = this.hotbars.length - 1;
}
this.setActiveHotbar(index);
}
switchToNext() {
let index = this.activeHotbarIndex + 1;
if (index >= this.hotbars.length) {
index = 0;
}
this.setActiveHotbar(index);
}
/**
* Checks if entity already pinned to the active hotbar
*/
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
if (!entity) {
return false;
}
const indexInActiveHotbar = this.getActive().items.findIndex(item => item?.entity.uid === entity.getId());
return indexInActiveHotbar >= 0;
}
getDisplayLabel(hotbar: Hotbar): string {
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
}
getDisplayIndex(hotbar: Hotbar): string {
const index = this.hotbarIndex(hotbar);
if (index < 0) {
return "??";
}
return `${index + 1}`;
}
}
/**
* This function ensures that there are always exactly `defaultHotbarCells`
* worth of items in the hotbar.
* @param hotbar The hotbar to modify
*/
function ensureExactHotbarItemLength(hotbar: Hotbar) {
// if there are not enough items
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
// if for some reason the hotbar was overfilled before, remove as many entries
// as needed, but prefer empty slots and items at the end first.
while (hotbar.items.length > defaultHotbarCells) {
const lastNull = hotbar.items.lastIndexOf(null);
if (lastNull >= 0) {
hotbar.items.splice(lastNull, 1);
} else {
hotbar.items.length = defaultHotbarCells;
}
}
}
-41
View File
@@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as uuid from "uuid";
import type { Tuple } from "@k8slens/utilities";
import { tuple } from "@k8slens/utilities";
export interface HotbarItem {
entity: {
uid: string;
name: string;
source?: string;
};
params?: {
[key: string]: string;
};
}
export type Hotbar = Required<CreateHotbarData>;
export interface CreateHotbarData {
id?: string;
name: string;
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
}
export interface CreateHotbarOptions {
setActive?: boolean;
}
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
return {
id,
items: tuple.filled(defaultHotbarCells, null),
name,
};
}
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import userInfoInjectable from "../user-store/user-info.injectable";
import userInfoInjectable from "../vars/user-info.injectable";
const homeDirectoryPathInjectable = getInjectable({
id: "home-directory-path",
@@ -0,0 +1,15 @@
/**
* 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";
export interface IpcChannelPrefixes {
readonly local: string;
readonly remote: string;
}
export const persistentStorageIpcChannelPrefixesInjectionToken = getInjectionToken<IpcChannelPrefixes>({
id: "persistent-storage-ipc-channel-prefix-token",
});
@@ -0,0 +1,158 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { disposer, isPromiseLike } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import type { Options } from "conf/dist/source";
import { isEqual, kebabCase } from "lodash";
import type { IEqualsComparer } from "mobx";
import { reaction } from "mobx";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken, sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import type { MessageChannel } from "@k8slens/messaging";
import { persistentStorageIpcChannelPrefixesInjectionToken } from "./channel-prefix";
import { shouldPersistentStorageDisableSyncInIpcListenerInjectionToken } from "./disable-sync";
import { persistStateToConfigInjectionToken } from "./save-to-file";
import type { Migrations } from "./migrations.injectable";
import { nextTick } from "process";
export interface PersistentStorage {
/**
* This method does the initial synchronous load from disk and then starts writing the state
* back to disk whenever it changes.
*/
loadAndStartSyncing: () => void;
}
export interface PersistentStorageParams<T extends object> extends Omit<Options<T>, "migrations"> {
readonly syncOptions?: {
readonly fireImmediately?: boolean;
equals?: IEqualsComparer<T>;
};
readonly configName: string;
readonly migrations?: Migrations;
/**
* fromStore is called internally when a child class syncs with the file
* system.
*
* Note: This function **must** be synchronous.
*
* @param data the parsed information read from the stored JSON file
*/
fromStore(data: Partial<T>): void;
/**
* toJSON is called when syncing the store to the filesystem. It should
* produce a JSON serializable object representation of the current state.
*
* It is recommended that a round trip is valid. Namely, calling
* `this.fromStore(this.toJSON())` shouldn't change the state.
*/
toJSON(): T;
}
export type CreatePersistentStorage = <T extends object>(params: PersistentStorageParams<T>) => PersistentStorage;
const createPersistentStorageInjectable = getInjectable({
id: "create-persistent-storage",
instantiate: (di): CreatePersistentStorage => {
const directoryForUserData = di.inject(directoryForUserDataInjectable);
const getConfigurationFileModel = di.inject(getConfigurationFileModelInjectable);
const logger = di.inject(loggerInjectable);
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
const ipcChannelPrefixes = di.inject(persistentStorageIpcChannelPrefixesInjectionToken);
const persistStateToConfig = di.inject(persistStateToConfigInjectionToken);
const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
const shouldDisableSyncInListener = di.inject(shouldPersistentStorageDisableSyncInIpcListenerInjectionToken);
const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
return <T extends object>(rawParams: PersistentStorageParams<T>) => {
const {
fromStore,
toJSON,
syncOptions,
migrations,
cwd = directoryForUserData,
...params
} = rawParams;
const displayName = kebabCase(params.configName).toUpperCase();
const loadAndStartSyncing = () => {
logger.info(`[${displayName}]: LOADING ...`);
const config = getConfigurationFileModel({
projectName: "lens",
cwd,
migrations: migrations as Options<T>["migrations"],
...params,
});
const res = fromStore(config.store);
if (isPromiseLike(res)) {
logger.error(`${displayName} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
}
logger.info(`[${displayName}]: LOADED from ${config.path}`);
const syncDisposers = disposer();
const sendChannel: MessageChannel<T> = {
id: `${ipcChannelPrefixes.remote}:${config.path}`,
};
const receiveChannel: MessageChannel<T> = {
id: `${ipcChannelPrefixes.local}:${config.path}`,
};
const name = getBasenameOfPath(config.path);
const disableSync = () => syncDisposers();
const enableSync = () => {
syncDisposers.push(
reaction(
() => toJSON(),
model => {
persistStateToConfig(config, model);
sendMessageToChannel(sendChannel, model);
},
syncOptions,
),
enlistMessageChannelListener({
id: "persistent-storage-sync",
channel: receiveChannel,
handler: (model) => {
logger.silly(`[${displayName}]: syncing ${name}`, { model });
if (shouldDisableSyncInListener) {
disableSync();
}
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
if (!isEqual(toJSON(), model)) {
fromStore(model);
}
if (shouldDisableSyncInListener) {
nextTick(() => {
enableSync();
});
}
},
}),
);
};
enableSync();
};
return {
loadAndStartSyncing,
};
};
},
});
export default createPersistentStorageInjectable;
@@ -5,6 +5,6 @@
import { getInjectionToken } from "@ogre-tools/injectable";
export const shouldBaseStoreDisableSyncInIpcListenerInjectionToken = getInjectionToken<boolean>({
id: "should-base-store-disable-sync-in-ipc-listener-token",
export const shouldPersistentStorageDisableSyncInIpcListenerInjectionToken = getInjectionToken<boolean>({
id: "should-persistent-storage-disable-sync-in-ipc-listener-token",
});
@@ -0,0 +1,58 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InjectionToken } from "@ogre-tools/injectable";
import { lifecycleEnum, getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable";
import { getOrInsert, iter, object } from "@k8slens/utilities";
export type AllowedSetValue<T> = T extends (...args: any[]) => any
? never
: T extends undefined | symbol
? never
: T;
export interface MigrationStore {
readonly path: string;
get(key: string): unknown;
delete(key: string): void;
has(key: string): boolean;
clear(): void;
set<T>(key: string, value: AllowedSetValue<T>): void;
}
export type Migrations = Partial<Record<string, (store: MigrationStore) => void>>;
export interface MigrationDeclaration {
version: string;
run(store: MigrationStore): void;
}
const persistentStorageMigrationsInjectable = getInjectable({
id: "persistent-storage-migrations",
instantiate: (di, token) => {
const logger = di.inject(loggerInjectable);
const declarations = di.injectMany(token);
const migrations = new Map<string, MigrationDeclaration["run"][]>();
for (const decl of declarations) {
getOrInsert(migrations, decl.version, []).push(decl.run);
}
return iter.chain(migrations.entries())
.map(([v, fns]) => [v, (store: MigrationStore) => {
logger.info(`Running ${v} migration for ${store.path}`);
for (const fn of fns) {
fn(store);
}
}] as const)
.collect(object.fromEntries);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, token: InjectionToken<MigrationDeclaration, void>) => token.id,
}),
});
export default persistentStorageMigrationsInjectable;
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InjectionToken } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import * as semver from "semver";
import type { MigrationDeclaration } from "./migrations.injectable";
/**
* NOTE: not all stores can use this computed version, namely if any migration uses a range for
* the version selector.
*/
const storageMigrationVersionInjectable = getInjectable({
id: "storage-migration-version",
instantiate: (di, token) => {
const declarations = di.injectMany(token);
return declarations.reduce((version, decl) => {
if (semver.gte(decl.version, version)) {
return decl.version;
}
return version;
}, "1.0.0");
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, token: InjectionToken<MigrationDeclaration, void>) => token.id,
}),
});
export default storageMigrationVersionInjectable;
@@ -10,13 +10,13 @@ import { isDefined, iter } from "@k8slens/utilities";
import { pathToRegexp } from "path-to-regexp";
import type Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error";
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
import type { ExtensionLoader } from "../../extensions/extension-loader";
import type { LensExtension } from "../../extensions/lens-extension";
import type { RouteHandler, RouteParams } from "./registration";
import { when } from "mobx";
import { ipcRenderer } from "electron";
import type { Logger } from "../logger";
import type { IsExtensionEnabled } from "../../features/extensions/enabled/common/is-enabled.injectable";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const ProtocolHandlerIpcPrefix = "protocol-handler";
@@ -65,8 +65,8 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
export interface LensProtocolRouterDependencies {
readonly extensionLoader: ExtensionLoader;
readonly extensionsStore: ExtensionsStore;
readonly logger: Logger;
isExtensionEnabled: IsExtensionEnabled;
}
export abstract class LensProtocolRouter {
@@ -209,7 +209,7 @@ export abstract class LensProtocolRouter {
return name;
}
if (!this.dependencies.extensionsStore.isEnabled(extension)) {
if (!this.dependencies.isExtensionEnabled(extension)) {
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
return name;
@@ -1,9 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
export default getGlobalOverride(userStoreFileNameMigrationInjectable, () => async () => {});
@@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import fse from "fs-extra";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { isErrnoException } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
export type UserStoreFileNameMigration = () => Promise<void>;
const userStoreFileNameMigrationInjectable = getInjectable({
id: "user-store-file-name-migration",
instantiate: (di): UserStoreFileNameMigration => {
const userDataPath = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const configJsonPath = joinPaths(userDataPath, "config.json");
const lensUserStoreJsonPath = joinPaths(userDataPath, "lens-user-store.json");
return async () => {
try {
await fse.move(configJsonPath, lensUserStoreJsonPath);
} catch (error) {
if (error instanceof Error && error.message === "dest already exists.") {
await fse.remove(configJsonPath);
} else if (isErrnoException(error) && error.code === "ENOENT" && error.path === configJsonPath) {
// (No such file or directory)
return; // file already moved
} else {
// pass other errors along
throw error;
}
}
};
},
causesSideEffects: true,
});
export default userStoreFileNameMigrationInjectable;
@@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
const httpsProxyConfigurationInjectable = getInjectable({
id: "https-proxy-configuration",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
return computed(() => userStore.httpsProxy);
},
});
export default httpsProxyConfigurationInjectable;
@@ -1,7 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./user-store";
export type { KubeconfigSyncEntry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
@@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import userStoreInjectable from "./user-store.injectable";
const kubeconfigSyncsInjectable = getInjectable({
id: "kubeconfig-syncs",
instantiate: (di) => di.inject(userStoreInjectable).syncKubeconfigEntries,
});
export default kubeconfigSyncsInjectable;
@@ -1,37 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
export type LensColorThemePreference = {
useSystemTheme: true;
} | {
useSystemTheme: false;
lensThemeId: string;
};
const lensColorThemePreferenceInjectable = getInjectable({
id: "lens-color-theme-preference",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
return computed((): LensColorThemePreference => {
// TODO: remove magic strings
if (userStore.colorTheme === "system") {
return {
useSystemTheme: true,
};
}
return {
useSystemTheme: false,
lensThemeId: userStore.colorTheme,
};
});
},
});
export default lensColorThemePreferenceInjectable;
@@ -1,11 +0,0 @@
/**
* 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 { MigrationDeclaration } from "../base-store/migrations.injectable";
export const userStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "user-store-migration-token",
});
@@ -1,143 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { merge } from "lodash";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
import homeDirectoryPathInjectable from "../os/home-directory-path.injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
import { defaultThemeId } from "../vars";
import currentTimezoneInjectable from "./current-timezone.injectable";
import type { EditorConfiguration, ExtensionRegistry, KubeconfigSyncEntry, KubeconfigSyncValue, TerminalConfig } from "./preferences-helpers";
import { defaultExtensionRegistryUrlLocation, defaultEditorConfig, defaultTerminalConfig, defaultPackageMirror, getPreferenceDescriptor, packageMirrors } from "./preferences-helpers";
export type PreferenceDescriptors = ReturnType<typeof userStorePreferenceDescriptorsInjectable["instantiate"]>;
const userStorePreferenceDescriptorsInjectable = getInjectable({
id: "user-store-preference-descriptors",
instantiate: (di) => {
const currentTimezone = di.inject(currentTimezoneInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
const mainKubeFolderPath = joinPaths(homeDirectoryPath, ".kube");
return ({
httpsProxy: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
shell: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
colorTheme: getPreferenceDescriptor<string>({
fromStore: val => val || defaultThemeId,
toStore: val => !val || val === defaultThemeId
? undefined
: val,
}),
terminalTheme: getPreferenceDescriptor<string>({
fromStore: val => val || "",
toStore: val => val || undefined,
}),
localeTimezone: getPreferenceDescriptor<string>({
fromStore: val => val || currentTimezone,
toStore: val => !val || val === currentTimezone
? undefined
: val,
}),
allowUntrustedCAs: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
allowErrorReporting: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? true,
toStore: val => val
? undefined
: val,
}),
downloadMirror: getPreferenceDescriptor<string>({
fromStore: val => !val || !packageMirrors.has(val)
? defaultPackageMirror
: val,
toStore: val => val === defaultPackageMirror
? undefined
: val,
}),
downloadKubectlBinaries: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? true,
toStore: val => val
? undefined
: val,
}),
downloadBinariesPath: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
kubectlBinariesPath: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
openAtLogin: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
terminalCopyOnSelect: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
hiddenTableColumns: getPreferenceDescriptor<[string, string[]][], Map<string, Set<string>>>({
fromStore: (val = []) => new Map(
val.map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
),
toStore: (val) => {
const res: [string, string[]][] = [];
for (const [table, columns] of val) {
if (columns.size) {
res.push([table, Array.from(columns)]);
}
}
return res.length ? res : undefined;
},
}),
syncKubeconfigEntries: getPreferenceDescriptor<KubeconfigSyncEntry[], ObservableMap<string, KubeconfigSyncValue>>({
fromStore: val => observable.map(
val?.map(({ filePath, ...rest }) => [filePath, rest])
?? [[mainKubeFolderPath, {}]],
),
toStore: val => val.size === 1 && val.has(mainKubeFolderPath)
? undefined
: Array.from(val, ([filePath, rest]) => ({ filePath, ...rest })),
}),
editorConfiguration: getPreferenceDescriptor<Partial<EditorConfiguration>, EditorConfiguration>({
fromStore: val => merge(defaultEditorConfig, val),
toStore: val => val,
}),
terminalConfig: getPreferenceDescriptor<Partial<TerminalConfig>, TerminalConfig>({
fromStore: val => merge(defaultTerminalConfig, val),
toStore: val => val,
}),
extensionRegistryUrl: getPreferenceDescriptor<ExtensionRegistry>({
fromStore: val => val ?? {
location: defaultExtensionRegistryUrlLocation,
},
toStore: val => val.location === defaultExtensionRegistryUrlLocation
? undefined
: val,
}),
}) as const;
},
});
export default userStorePreferenceDescriptorsInjectable;
@@ -1,99 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { editor } from "monaco-editor";
import { defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string;
}
export interface KubeconfigSyncValue {
}
export interface TerminalConfig {
fontSize: number;
fontFamily: string;
}
export const defaultTerminalConfig: TerminalConfig = {
fontSize: defaultFontSize,
fontFamily: defaultTerminalFontFamily,
};
export interface BaseEditorConfiguration extends Required<Pick<editor.IStandaloneEditorConstructionOptions, "minimap" | "tabSize" | "fontSize" | "fontFamily">> {
lineNumbers: NonNullable<Exclude<editor.IStandaloneEditorConstructionOptions["lineNumbers"], Function>>;
}
export type EditorConfiguration = Required<BaseEditorConfiguration>;
export const defaultEditorConfig: EditorConfiguration = {
tabSize: 2,
lineNumbers: "on",
fontSize: defaultFontSize,
fontFamily: defaultEditorFontFamily,
minimap: {
enabled: true,
side: "right",
},
};
export type StoreType<P> = P extends PreferenceDescription<unknown, infer Store>
? Store
: never;
export interface PreferenceDescription<T, R = T> {
fromStore(val: T | undefined): R;
toStore(val: R): T | undefined;
}
export const getPreferenceDescriptor = <T, R = T>(desc: PreferenceDescription<T, R>) => desc;
export interface DownloadMirror {
url: string;
label: string;
platforms: Set<NodeJS.Platform>;
}
export const defaultPackageMirror = "default";
const defaultDownloadMirrorData: DownloadMirror = {
url: "https://storage.googleapis.com/kubernetes-release/release",
label: "Default (Google)",
platforms: new Set(["darwin", "win32", "linux"]),
};
export const packageMirrors = new Map<string, DownloadMirror>([
[defaultPackageMirror, defaultDownloadMirrorData],
["china", {
url: "https://mirror.azure.cn/kubernetes/kubectl",
label: "China (Azure)",
platforms: new Set(["win32", "linux"]),
}],
]);
export type ExtensionRegistryLocation = "default" | "npmrc" | "custom";
export type ExtensionRegistry = {
location: "default" | "npmrc";
customUrl?: undefined;
} | {
location: "custom";
customUrl: string;
};
export const defaultExtensionRegistryUrlLocation = "default";
export const defaultExtensionRegistryUrl = "https://registry.npmjs.org";
type PreferencesModelType<field extends keyof PreferenceDescriptors> = PreferenceDescriptors[field] extends PreferenceDescription<infer T, any> ? T : never;
type UserStoreModelType<field extends keyof PreferenceDescriptors> = PreferenceDescriptors[field] extends PreferenceDescription<any, infer T> ? T : never;
export type UserStoreFlatModel = {
[field in keyof PreferenceDescriptors]: UserStoreModelType<field>;
};
export type UserPreferencesModel = {
[field in keyof PreferenceDescriptors]: PreferencesModelType<field>;
} & { updateChannel: string };
@@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import userInfoInjectable from "./user-info.injectable";
import userStoreInjectable from "./user-store.injectable";
const userShellSettingInjectable = getInjectable({
id: "user-shell-setting",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
const userInfo = di.inject(userInfoInjectable);
return computed(() => userStore.shell || userInfo.shell);
},
});
export default userShellSettingInjectable;
@@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { toJS } from "../utils";
import userStoreInjectable from "./user-store.injectable";
const terminalConfigInjectable = getInjectable({
id: "terminal-config",
instantiate: (di) => {
const store = di.inject(userStoreInjectable);
return computed(() => toJS(store.terminalConfig));
},
});
export default terminalConfigInjectable;
@@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
const terminalCopyOnSelectInjectable = getInjectable({
id: "terminal-copy-on-select",
instantiate: (di) => {
const store = di.inject(userStoreInjectable);
return computed(() => store.terminalCopyOnSelect);
},
});
export default terminalCopyOnSelectInjectable;
@@ -1,37 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
export type TerminalThemePreference = {
matchLensTheme: true;
} | {
matchLensTheme: false;
themeId: string;
};
const terminalThemePreferenceInjectable = getInjectable({
id: "terminal-theme-preference",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
return computed((): TerminalThemePreference => {
// NOTE: remove use of magic strings
if (!userStore.terminalTheme) {
return {
matchLensTheme: true,
};
}
return {
matchLensTheme: false,
themeId: userStore.terminalTheme,
};
});
},
});
export default terminalThemePreferenceInjectable;
@@ -1,42 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { userStoreMigrationInjectionToken } from "./migrations-token";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable";
const userStoreInjectable = getInjectable({
id: "user-store",
instantiate: (di) => new UserStore({
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(storeMigrationsInjectable, userStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
preferenceDescriptors: di.inject(userStorePreferenceDescriptorsInjectable),
}),
});
export default userStoreInjectable;
@@ -1,156 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, observable, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store/base-store";
import { getOrInsertSet, toggle, object } from "@k8slens/utilities";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
// TODO: Remove coupling with Feature
import type { SelectedUpdateChannel } from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
import type { ReleaseChannel } from "../../features/application-update/common/update-channels";
import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
import { toJS } from "../utils";
export interface UserStoreModel {
preferences: UserPreferencesModel;
}
interface Dependencies extends BaseStoreDependencies {
readonly selectedUpdateChannel: SelectedUpdateChannel;
readonly preferenceDescriptors: PreferenceDescriptors;
emitAppEvent: EmitAppEvent;
}
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
configName: "lens-user-store",
});
makeObservable(this);
}
/**
* @deprecated No longer used
*/
@observable seenContexts = observable.set<string>();
/**
* @deprecated No longer used
*/
@observable newContexts = observable.set<string>();
@observable allowErrorReporting!: StoreType<PreferenceDescriptors["allowErrorReporting"]>;
@observable allowUntrustedCAs!: StoreType<PreferenceDescriptors["allowUntrustedCAs"]>;
@observable colorTheme!: StoreType<PreferenceDescriptors["colorTheme"]>;
@observable terminalTheme!: StoreType<PreferenceDescriptors["terminalTheme"]>;
@observable localeTimezone!: StoreType<PreferenceDescriptors["localeTimezone"]>;
@observable downloadMirror!: StoreType<PreferenceDescriptors["downloadMirror"]>;
@observable httpsProxy!: StoreType<PreferenceDescriptors["httpsProxy"]>;
@observable shell!: StoreType<PreferenceDescriptors["shell"]>;
@observable downloadBinariesPath!: StoreType<PreferenceDescriptors["downloadBinariesPath"]>;
@observable kubectlBinariesPath!: StoreType<PreferenceDescriptors["kubectlBinariesPath"]>;
@observable terminalCopyOnSelect!: StoreType<PreferenceDescriptors["terminalCopyOnSelect"]>;
@observable terminalConfig!: StoreType<PreferenceDescriptors["terminalConfig"]>;
@observable extensionRegistryUrl!: StoreType<PreferenceDescriptors["extensionRegistryUrl"]>;
/**
* Download kubectl binaries matching cluster version
*/
@observable downloadKubectlBinaries!: StoreType<PreferenceDescriptors["downloadKubectlBinaries"]>;
/**
* Whether the application should open itself at login.
*/
@observable openAtLogin!: StoreType<PreferenceDescriptors["openAtLogin"]>;
/**
* The column IDs under each configurable table ID that have been configured
* to not be shown
*/
@observable hiddenTableColumns!: StoreType<PreferenceDescriptors["hiddenTableColumns"]>;
/**
* Monaco editor configs
*/
@observable editorConfiguration!: StoreType<PreferenceDescriptors["editorConfiguration"]>;
/**
* The set of file/folder paths to be synced
*/
@observable syncKubeconfigEntries!: StoreType<PreferenceDescriptors["syncKubeconfigEntries"]>;
/**
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
* @param tableId The ID of the table to be checked against
* @param columnIds The list of IDs the check if one is hidden
* @returns true if at least one column under the table is set to hidden
*/
isTableColumnHidden(tableId: string, ...columnIds: (string | undefined)[]): boolean {
if (columnIds.length === 0) {
return false;
}
const config = this.hiddenTableColumns.get(tableId);
if (!config) {
return false;
}
return columnIds.some(columnId => columnId && config.has(columnId));
}
/**
* Toggles the hidden configuration of a table's column
*/
toggleTableColumnVisibility(tableId: string, columnId: string) {
toggle(getOrInsertSet(this.hiddenTableColumns, tableId), columnId);
}
@action
resetTheme() {
this.colorTheme = this.dependencies.preferenceDescriptors.colorTheme.fromStore(undefined);
}
@action
protected fromStore({ preferences }: Partial<UserStoreModel> = {}) {
this.dependencies.logger.debug("UserStore.fromStore()", { preferences });
for (const [key, { fromStore }] of object.entries(this.dependencies.preferenceDescriptors)) {
const curVal = this[key];
const newVal = fromStore((preferences)?.[key] as never) as never;
if (isObservableArray(curVal)) {
curVal.replace(newVal);
} else if (isObservableSet(curVal) || isObservableMap(curVal)) {
curVal.replace(newVal);
} else {
this[key] = newVal;
}
}
// TODO: Switch to action-based saving instead saving stores by reaction
if (preferences?.updateChannel) {
this.dependencies.selectedUpdateChannel.setValue(preferences?.updateChannel as ReleaseChannel);
}
}
toJSON(): UserStoreModel {
const preferences = object.fromEntries(
object.entries(this.dependencies.preferenceDescriptors)
.map(([key, { toStore }]) => [key, toStore(this[key] as never)]),
) as UserPreferencesModel;
return toJS({
preferences: {
...preferences,
updateChannel: this.dependencies.selectedUpdateChannel.value.get().id,
},
});
}
}
@@ -1,11 +0,0 @@
/**
* 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 { MigrationDeclaration } from "../base-store/migrations.injectable";
export const weblinkStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "weblink-store-migration-token",
});
@@ -1,35 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import { weblinkStoreMigrationInjectionToken } from "./migration-token";
import { WeblinkStore } from "./weblink-store";
const weblinkStoreInjectable = getInjectable({
id: "weblink-store",
instantiate: (di) => new WeblinkStore({
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(storeMigrationsInjectable, weblinkStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
});
export default weblinkStoreInjectable;
@@ -1,74 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, comparer, observable, makeObservable } from "mobx";
import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store/base-store";
import * as uuid from "uuid";
import { toJS } from "../utils";
export interface WeblinkData {
id: string;
name: string;
url: string;
}
export interface WeblinkCreateOptions {
id?: string;
name: string;
url: string;
}
export interface WeblinkStoreModel {
weblinks: WeblinkData[];
}
export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
@observable weblinks: WeblinkData[] = [];
constructor(deps: BaseStoreDependencies) {
super(deps, {
configName: "lens-weblink-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
});
makeObservable(this);
this.load();
}
@action
protected fromStore(data: Partial<WeblinkStoreModel> = {}) {
this.weblinks = data.weblinks || [];
}
add(data: WeblinkCreateOptions) {
const {
id = uuid.v4(),
name,
url,
} = data;
const weblink: WeblinkData = { id, name, url };
this.weblinks.push(weblink);
return weblink;
}
@action
removeById(id: string) {
this.weblinks = this.weblinks.filter((w) => w.id !== id);
}
toJSON(): WeblinkStoreModel {
const model: WeblinkStoreModel = {
weblinks: this.weblinks,
};
return toJS(model);
}
}