mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Make base store non Singleton (#6690)
* Remove Singleton from BaseStore to remove global shared state Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove more usages of Singleton Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace use of legacy global execHelm with injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove last use of legacy global execHelm Signed-off-by: Sebastian Malton <sebastian@malton.name> * Extract BaseStore deps into constructor argument Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce method to make store migrations injectable - Use it for ClusterStore Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch HotbarStore to injectable migrations Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch UserStore to injectable migrations Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move migration utils into common/utils/ Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch WeblinkStore to injectable migrations Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove dead code Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error in base-store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove tests that reference lastSeenVersion - That value is not used anywhere in code Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove usage of legacy global .getInstance Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove usage of legacy global ClusterStore.getInstance Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add simple migrations dependency for stores without any preexisting migrations Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix messed up import Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add typing to transient injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Cleanup formatting Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix typing in tests to satisfy requirement to have cacheFile Signed-off-by: Sebastian Malton <sebastian@malton.name> * More consistent use of BaseStore.displayName Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add catching of error while starting main application Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move initializing sentry to runnable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove unneeded appPathsInjectionToken - Only had once impl, which was in common anyway Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add support for multiple "runAfter" runnables - Needed so that several dependencies can be declared Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use multiple runAfter support to fix crash on renderer Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove traces Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override to fix tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix base store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix runManyFor tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix hotbar store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix user store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override for getConfigurationFileModel to fix tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove overrides for configuration stores - Now that there is an override for getConfiguration Signed-off-by: Sebastian Malton <sebastian@malton.name> * Overhaul FS fakes with full in-memory filesystem - This increases our confidence in fs related logic Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove use of global shared Electron.App Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add fake access support Signed-off-by: Sebastian Malton <sebastian@malton.name> * Handle copy as part of fake FS Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add ensureDir/Sync support to fake FS Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use pathExistsSync instead of fsInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add createReadStream to fake FS Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add stat to fake FS Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove dead code Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test failures due to incomplete overrides Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fully injectable-ize BaseStore so that ApplicationBuilder tests work Signed-off-by: Sebastian Malton <sebastian@malton.name> * Consolidate more bootstrapping into startFrame Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move initializing CatalogCategories to runnable in bootstrap Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert contextMenuOpen initializers into runnables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert navigateForExtension init to runnable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make cluster state sync fully injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move init hotbar store into runnables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make LensTheme fully injectable and runnable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Cleanup old code from missed from previous commit Signed-off-by: Sebastian Malton <sebastian@malton.name> * Manually split out terminal color names and fully type LensTheme Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix old imports Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove unnecessart awaits Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove dead code Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fully cherry pick injectablizing custom monaco themes Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix duplicate mock warning Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix incorrectly fully cherry picking new runnable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Complete cherry-pick of current cluster injcetablization Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix override file name Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix injecting before app paths are set up Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix injecting before app paths are set up Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix ordering of runnable and order of injection Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all renderer runnables to late-inject style - To help fix issues around injection time Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix react-beautiful-dnd mocks Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update and fix WriteJson(Sync) to fix error in tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix HotbarStore.load being called twice is being buggy Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update listing-active-helm-repositories-in-preferences snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix sidebar-and-tab-navigation-tests - Move enabling extensions in tests to a proper location - Fix flushing promises Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove props from dnd mock to make snapshot diffs smaller Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix import Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests by overriding things that are no longer overriden by default - NOTE: They are overridden when using ApplicationBuilder Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix hotbar store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix cluster store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension-loader tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension-discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix cluster-role-dialog tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix user store tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix kubeconfig sync tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix sidebar and tab tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove unused code Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix pick paths import type error and simplify signature Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error in legacy ipc registration Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove another use of legacy requestOpenPathPicker Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace use of legacy global PathPicker.Pick Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix usage in light of changed prop names Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix catalog tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix more type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test flakiness by removing side effects from userStore preferences Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix loading Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash Signed-off-by: Sebastian Malton <sebastian@malton.name> * Cherry pick updated startFrameInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add tests to verify runMany behaviour in new possible incorrect configuration Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix init ordering during start frame Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix cluster state sync Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after removing side-effects Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add override for technical test Signed-off-by: Sebastian Malton <sebastian@malton.name> * Correctly mark currentlyInClusterFrame as causedSideEffects Signed-off-by: Sebastian Malton <sebastian@malton.name> * Better formatting Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix behaviour regression Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add better logging Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix BaseStore sync Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update last snapshot Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override for randomBytes Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make startMainApplication not an injection time side effect Signed-off-by: Sebastian Malton <sebastian@malton.name> * Choose better names for start-frame runnable tokens Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove duplication of code in RunManyFor Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add unit tests and fix handling empty runAfter array Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace use of mobx from runManyFor with custom barrier Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add missing test Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
7c5faaaf1d
commit
25f37ac1d1
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export default {};
|
||||
|
||||
export const Uri = {
|
||||
file: (path: string) => path,
|
||||
};
|
||||
|
||||
export const editor = {
|
||||
getModel: () => ({}),
|
||||
create: () => ({}),
|
||||
};
|
||||
@ -7,9 +7,49 @@ import React from "react";
|
||||
import type {
|
||||
DragDropContextProps,
|
||||
DraggableProps,
|
||||
DraggableProvidedDraggableProps,
|
||||
DroppableProps,
|
||||
DroppableProvidedProps,
|
||||
} from "react-beautiful-dnd";
|
||||
|
||||
export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }</>;
|
||||
export const Draggable = ({ children }: DraggableProps) => <>{ children({} as any, {} as any, {} as any) }</>;
|
||||
export const Droppable = ({ children }: DroppableProps) => <>{ children({} as any, {} as any) }</>;
|
||||
export const Draggable = ({ children }: DraggableProps) => (
|
||||
<>
|
||||
{
|
||||
children(
|
||||
{
|
||||
draggableProps: {} as DraggableProvidedDraggableProps,
|
||||
innerRef: () => {},
|
||||
},
|
||||
{
|
||||
isDragging: false,
|
||||
isDropAnimating: false,
|
||||
},
|
||||
{
|
||||
draggableId: "some-mock-draggable-id",
|
||||
mode: "FLUID",
|
||||
source: {
|
||||
droppableId: "some-mock-droppable-id",
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
export const Droppable = ({ children }: DroppableProps) => (
|
||||
<>
|
||||
{
|
||||
children(
|
||||
{
|
||||
droppableProps: {} as DroppableProvidedProps,
|
||||
innerRef: () => {},
|
||||
},
|
||||
{
|
||||
isDraggingOver: false,
|
||||
isUsingPlaceholder: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark";
|
||||
|
||||
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
|
||||
|
||||
const banner = `/*
|
||||
Generated Lens theme CSS-variables, don't edit manually.
|
||||
To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
|
||||
*/`;
|
||||
|
||||
const themeCssVars = Object.entries(defaultBaseLensTheme.colors)
|
||||
.map(([varName, value]) => `--${varName}: ${value};`);
|
||||
|
||||
const content = `
|
||||
${banner}
|
||||
|
||||
:root {
|
||||
${themeCssVars.join("\n")}
|
||||
}
|
||||
`;
|
||||
|
||||
// Run
|
||||
console.info(`"Saving default Lens theme css-variables to "${outputCssFile}""`);
|
||||
fs.ensureFileSync(outputCssFile);
|
||||
fs.writeFile(outputCssFile, content);
|
||||
@ -228,6 +228,7 @@
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"handlebars": "^4.7.7",
|
||||
"history": "^4.10.1",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"immer": "^9.0.16",
|
||||
"joi": "^17.7.0",
|
||||
|
||||
@ -1,148 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import mockFs from "mock-fs";
|
||||
import { BaseStore } from "../base-store";
|
||||
import { action, comparer, makeObservable, observable, toJS } from "mobx";
|
||||
import { readFileSync } from "fs";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
interface TestStoreModel {
|
||||
a: string;
|
||||
b: string;
|
||||
c: string;
|
||||
}
|
||||
|
||||
class TestStore extends BaseStore<TestStoreModel> {
|
||||
@observable a = "";
|
||||
@observable b = "";
|
||||
@observable c = "";
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
configName: "test-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
syncOptions: {
|
||||
equals: comparer.structural,
|
||||
},
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
}
|
||||
|
||||
@action updateAll(data: TestStoreModel) {
|
||||
this.a = data.a;
|
||||
this.b = data.b;
|
||||
this.c = data.c;
|
||||
}
|
||||
|
||||
@action fromStore(data: Partial<TestStoreModel> = {}) {
|
||||
this.a = data.a || "";
|
||||
this.b = data.b || "";
|
||||
this.c = data.c || "";
|
||||
}
|
||||
|
||||
onSync(data: TestStoreModel) {
|
||||
super.onSync(data);
|
||||
}
|
||||
|
||||
async saveToFile(model: TestStoreModel) {
|
||||
return super.saveToFile(model);
|
||||
}
|
||||
|
||||
toJSON(): TestStoreModel {
|
||||
const data: TestStoreModel = {
|
||||
a: this.a,
|
||||
b: this.b,
|
||||
c: this.c,
|
||||
};
|
||||
|
||||
return toJS(data);
|
||||
}
|
||||
}
|
||||
|
||||
describe("BaseStore", () => {
|
||||
let store: TestStore;
|
||||
|
||||
beforeEach(() => {
|
||||
const mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");
|
||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
TestStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
"some-user-data-directory": {
|
||||
"test-store.json": JSON.stringify({}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
store = TestStore.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
store.disableSync();
|
||||
TestStore.resetInstance();
|
||||
});
|
||||
|
||||
describe("persistence", () => {
|
||||
it("persists changes to the filesystem", () => {
|
||||
store.updateAll({
|
||||
a: "foo", b: "bar", c: "hello",
|
||||
});
|
||||
|
||||
const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString());
|
||||
|
||||
expect(data).toEqual({ a: "foo", b: "bar", c: "hello" });
|
||||
});
|
||||
|
||||
it("persists transaction only once", () => {
|
||||
const fileSpy = jest.spyOn(store, "saveToFile");
|
||||
|
||||
store.updateAll({
|
||||
a: "foo", b: "bar", c: "hello",
|
||||
});
|
||||
|
||||
expect(fileSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("persists changes one-by-one without transaction", () => {
|
||||
const fileSpy = jest.spyOn(store, "saveToFile");
|
||||
|
||||
store.a = "a";
|
||||
store.b = "b";
|
||||
|
||||
expect(fileSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString());
|
||||
|
||||
expect(data).toEqual({ a: "a", b: "b", c: "" });
|
||||
});
|
||||
|
||||
it("persists changes coming via onSync (sync from different process)", () => {
|
||||
const fileSpy = jest.spyOn(store, "saveToFile");
|
||||
|
||||
store.onSync({ a: "foo", b: "", c: "bar" });
|
||||
|
||||
expect(store.toJSON()).toEqual({ a: "foo", b: "", c: "bar" });
|
||||
|
||||
expect(fileSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -93,10 +93,9 @@ describe("cluster-store", () => {
|
||||
mainDi.override(normalizedPlatformInjectable, () => "darwin");
|
||||
|
||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
mainDi.permitSideEffects(clusterStoreInjectable);
|
||||
mainDi.permitSideEffects(fsInjectable);
|
||||
mainDi.unoverride(getConfigurationFileModelInjectable);
|
||||
|
||||
mainDi.unoverride(clusterStoreInjectable);
|
||||
mainDi.permitSideEffects(fsInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -107,23 +106,19 @@ describe("cluster-store", () => {
|
||||
let getCustomKubeConfigDirectory: (directoryName: string) => string;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCustomKubeConfigDirectory = mainDi.inject(
|
||||
getCustomKubeConfigDirectoryInjectable,
|
||||
);
|
||||
getCustomKubeConfigDirectory = mainDi.inject(getCustomKubeConfigDirectoryInjectable);
|
||||
|
||||
const mockOpts = {
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
});
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
|
||||
clusterStore.unregisterIpcListener();
|
||||
clusterStore.load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -207,7 +202,7 @@ describe("cluster-store", () => {
|
||||
|
||||
describe("config with existing clusters", () => {
|
||||
beforeEach(() => {
|
||||
const mockOpts = {
|
||||
mockFs({
|
||||
"temp-kube-config": kubeconfig,
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
@ -241,13 +236,12 @@ describe("cluster-store", () => {
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
});
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
clusterStore.load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -297,7 +291,7 @@ users:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
const mockOpts = {
|
||||
mockFs({
|
||||
"invalid-kube-config": invalidKubeconfig,
|
||||
"valid-kube-config": kubeconfig,
|
||||
"some-directory-for-user-data": {
|
||||
@ -325,13 +319,12 @@ users:
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
});
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
clusterStore.load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -347,7 +340,7 @@ users:
|
||||
|
||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
const mockOpts = {
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
__internal__: {
|
||||
@ -368,15 +361,14 @@ users:
|
||||
}),
|
||||
icon_path: testDataIcon,
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
});
|
||||
|
||||
mainDi.override(storeMigrationVersionInjectable, () => "3.6.0");
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
clusterStore.load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@ -19,6 +19,7 @@ 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 fsInjectable from "../fs/fs.injectable";
|
||||
|
||||
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
|
||||
return {
|
||||
@ -46,7 +47,7 @@ describe("HotbarStore", () => {
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
(di as any).unoverride(hotbarStoreInjectable);
|
||||
di.unoverride(hotbarStoreInjectable);
|
||||
|
||||
testCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
@ -112,8 +113,9 @@ describe("HotbarStore", () => {
|
||||
catalogCatalogEntity,
|
||||
]));
|
||||
|
||||
di.permitSideEffects(fsInjectable);
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(hotbarStoreInjectable);
|
||||
di.unoverride(getConfigurationFileModelInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -255,22 +257,12 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("throws if invalid arguments provided", () => {
|
||||
// Prevent writing to stderr during this render.
|
||||
const { error, warn } = console;
|
||||
|
||||
console.error = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
|
||||
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
|
||||
expect(() => hotbarStore.restackItems(2, -1)).toThrow();
|
||||
expect(() => hotbarStore.restackItems(14, 1)).toThrow();
|
||||
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
|
||||
|
||||
// Restore writing to stderr.
|
||||
console.error = error;
|
||||
console.warn = warn;
|
||||
});
|
||||
|
||||
it("checks if entity already pinned to hotbar", () => {
|
||||
@ -284,7 +276,7 @@ describe("HotbarStore", () => {
|
||||
|
||||
describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
|
||||
beforeEach(() => {
|
||||
const configurationToBeMigrated = {
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {
|
||||
"lens-hotbar-store.json": JSON.stringify({
|
||||
__internal__: {
|
||||
@ -344,9 +336,7 @@ describe("HotbarStore", () => {
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(configurationToBeMigrated);
|
||||
});
|
||||
|
||||
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ import getConfigurationFileModelInjectable from "../get-configuration-file-model
|
||||
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
|
||||
import releaseChannelInjectable from "../vars/release-channel.injectable";
|
||||
import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
|
||||
import fsInjectable from "../fs/fs.injectable";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@ -49,8 +50,10 @@ describe("user store tests", () => {
|
||||
|
||||
di.override(writeFileInjectable, () => () => Promise.resolve());
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(userStoreInjectable);
|
||||
di.unoverride(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(fsInjectable);
|
||||
|
||||
di.override(releaseChannelInjectable, () => ({
|
||||
get: () => "latest" as const,
|
||||
@ -58,7 +61,7 @@ describe("user store tests", () => {
|
||||
}));
|
||||
await di.inject(defaultUpdateChannelInjectable).init();
|
||||
|
||||
di.unoverride(userStoreInjectable);
|
||||
userStore = di.inject(userStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -67,17 +70,11 @@ describe("user store tests", () => {
|
||||
|
||||
describe("for an empty config", () => {
|
||||
beforeEach(() => {
|
||||
mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }});
|
||||
mockFs({ "some-directory-for-user-data": { "lens-user-store.json": "{}", "kube_config": "{}" }});
|
||||
|
||||
userStore = di.inject(userStoreInjectable);
|
||||
userStore.load();
|
||||
});
|
||||
|
||||
it("allows setting and retrieving lastSeenAppVersion", () => {
|
||||
userStore.lastSeenAppVersion = "1.2.3";
|
||||
expect(userStore.lastSeenAppVersion).toBe("1.2.3");
|
||||
});
|
||||
|
||||
it("allows setting and getting preferences", () => {
|
||||
userStore.httpsProxy = "abcd://defg";
|
||||
|
||||
@ -99,10 +96,8 @@ describe("user store tests", () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {
|
||||
"config.json": JSON.stringify({
|
||||
user: { username: "foobar" },
|
||||
"lens-user-store.json": JSON.stringify({
|
||||
preferences: { colorTheme: "light" },
|
||||
lastSeenAppVersion: "1.2.3",
|
||||
}),
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
clusters: [
|
||||
@ -127,17 +122,16 @@ describe("user store tests", () => {
|
||||
|
||||
di.override(storeMigrationVersionInjectable, () => "10.0.0");
|
||||
|
||||
userStore = di.inject(userStoreInjectable);
|
||||
userStore.load();
|
||||
});
|
||||
|
||||
it("sets last seen app version to 0.0.0", () => {
|
||||
expect(userStore.lastSeenAppVersion).toBe("0.0.0");
|
||||
});
|
||||
|
||||
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it("allows access to the colorTheme preference", () => {
|
||||
expect(userStore.colorTheme).toBe("light");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,11 +2,6 @@
|
||||
* 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 { PathName } from "./app-path-names";
|
||||
|
||||
export type AppPaths = Record<PathName, string>;
|
||||
|
||||
export const appPathsInjectionToken = getInjectionToken<AppPaths>({ id: "app-paths-token" });
|
||||
|
||||
|
||||
|
||||
@ -3,13 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "./app-path-injection-token";
|
||||
import appPathsStateInjectable from "./app-paths-state.injectable";
|
||||
|
||||
const appPathsInjectable = getInjectable({
|
||||
id: "app-paths",
|
||||
instantiate: (di) => di.inject(appPathsStateInjectable).get(),
|
||||
injectionToken: appPathsInjectionToken,
|
||||
});
|
||||
|
||||
export default appPathsInjectable;
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { AppPaths } from "./app-path-injection-token";
|
||||
import { appPathsInjectionToken } from "./app-path-injection-token";
|
||||
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
|
||||
import type { PathName } from "./app-path-names";
|
||||
import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable";
|
||||
@ -11,6 +10,7 @@ import directoryForIntegrationTestingInjectable from "../../main/app-paths/direc
|
||||
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import appPathsInjectable from "./app-paths.injectable";
|
||||
|
||||
describe("app-paths", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
@ -68,7 +68,7 @@ describe("app-paths", () => {
|
||||
});
|
||||
|
||||
it("given in renderer, when injecting app paths, returns application specific app paths", () => {
|
||||
const actual = windowDi.inject(appPathsInjectionToken);
|
||||
const actual = windowDi.inject(appPathsInjectable);
|
||||
|
||||
expect(actual).toEqual({
|
||||
currentApp: "some-current-app",
|
||||
@ -92,7 +92,7 @@ describe("app-paths", () => {
|
||||
});
|
||||
|
||||
it("given in main, when injecting app paths, returns application specific app paths", () => {
|
||||
const actual = mainDi.inject(appPathsInjectionToken);
|
||||
const actual = mainDi.inject(appPathsInjectable);
|
||||
|
||||
expect(actual).toEqual({
|
||||
currentApp: "some-current-app",
|
||||
@ -133,7 +133,7 @@ describe("app-paths", () => {
|
||||
});
|
||||
|
||||
it("given in renderer, when injecting path for app data, has integration specific app data path", () => {
|
||||
const { appData, userData } = windowDi.inject(appPathsInjectionToken);
|
||||
const { appData, userData } = windowDi.inject(appPathsInjectable);
|
||||
|
||||
expect({ appData, userData }).toEqual({
|
||||
appData: "some-integration-testing-app-data",
|
||||
@ -142,7 +142,7 @@ describe("app-paths", () => {
|
||||
});
|
||||
|
||||
it("given in main, when injecting path for app data, has integration specific app data path", () => {
|
||||
const { appData, userData } = windowDi.inject(appPathsInjectionToken);
|
||||
const { appData, userData } = windowDi.inject(appPathsInjectable);
|
||||
|
||||
expect({ appData, userData }).toEqual({
|
||||
appData: "some-integration-testing-app-data",
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
import appPathsInjectable from "../app-paths.injectable";
|
||||
|
||||
const directoryForDownloadsInjectable = getInjectable({
|
||||
id: "directory-for-downloads",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).downloads,
|
||||
instantiate: (di) => di.inject(appPathsInjectable).downloads,
|
||||
});
|
||||
|
||||
export default directoryForDownloadsInjectable;
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
import appPathsInjectable from "../app-paths.injectable";
|
||||
|
||||
const directoryForExesInjectable = getInjectable({
|
||||
id: "directory-for-exes",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).exe,
|
||||
instantiate: (di) => di.inject(appPathsInjectable).exe,
|
||||
});
|
||||
|
||||
export default directoryForExesInjectable;
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
import appPathsInjectable from "../app-paths.injectable";
|
||||
|
||||
const directoryForTempInjectable = getInjectable({
|
||||
id: "directory-for-temp",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).temp,
|
||||
instantiate: (di) => di.inject(appPathsInjectable).temp,
|
||||
});
|
||||
|
||||
export default directoryForTempInjectable;
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
import appPathsInjectable from "../app-paths.injectable";
|
||||
|
||||
const directoryForUserDataInjectable = getInjectable({
|
||||
id: "directory-for-user-data",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).userData,
|
||||
instantiate: (di) => di.inject(appPathsInjectable).userData,
|
||||
});
|
||||
|
||||
export default directoryForUserDataInjectable;
|
||||
|
||||
@ -1,194 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import type Config from "conf";
|
||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||
import { ipcMain, ipcRenderer } from "electron";
|
||||
import type { IEqualsComparer } from "mobx";
|
||||
import { makeObservable, reaction, runInAction } from "mobx";
|
||||
import type { Disposer } from "./utils";
|
||||
import { Singleton, toJS } from "./utils";
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { isTestEnv } from "./vars";
|
||||
import { kebabCase } from "lodash";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import storeMigrationVersionInjectable from "./vars/store-migration-version.injectable";
|
||||
|
||||
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
||||
syncOptions?: {
|
||||
fireImmediately?: boolean;
|
||||
equals?: IEqualsComparer<T>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: T should only contain base JSON serializable types.
|
||||
*/
|
||||
export abstract class BaseStore<T extends object> extends Singleton {
|
||||
protected storeConfig?: Config<T>;
|
||||
protected syncDisposers: Disposer[] = [];
|
||||
|
||||
readonly displayName: string = this.constructor.name;
|
||||
|
||||
protected constructor(protected params: BaseStoreParams<T>) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
|
||||
if (ipcRenderer) {
|
||||
params.migrations = undefined; // don't run migrations on renderer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This must be called after the last child's constructor is finished (or just before it finishes)
|
||||
*/
|
||||
load() {
|
||||
if (!isTestEnv) {
|
||||
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING from ${this.path} ...`);
|
||||
}
|
||||
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
const getConfigurationFileModel = di.inject(getConfigurationFileModelInjectable);
|
||||
|
||||
this.storeConfig = getConfigurationFileModel({
|
||||
projectName: "lens",
|
||||
projectVersion: di.inject(storeMigrationVersionInjectable),
|
||||
cwd: this.cwd(),
|
||||
...this.params,
|
||||
});
|
||||
|
||||
const res: any = this.fromStore(this.storeConfig.store);
|
||||
|
||||
if (res instanceof Promise || (typeof res === "object" && res && typeof res.then === "function")) {
|
||||
console.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.enableSync();
|
||||
|
||||
if (!isTestEnv) {
|
||||
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADED from ${this.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return path.basename(this.path);
|
||||
}
|
||||
|
||||
protected get syncRendererChannel() {
|
||||
return `store-sync-renderer:${this.path}`;
|
||||
}
|
||||
|
||||
protected get syncMainChannel() {
|
||||
return `store-sync-main:${this.path}`;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.storeConfig?.path || "";
|
||||
}
|
||||
|
||||
protected cwd() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(directoryForUserDataInjectable);
|
||||
}
|
||||
|
||||
protected saveToFile(model: T) {
|
||||
logger.info(`[STORE]: SAVING ${this.path}`);
|
||||
|
||||
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
|
||||
if (this.storeConfig) {
|
||||
for (const [key, value] of Object.entries(model)) {
|
||||
this.storeConfig.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableSync() {
|
||||
this.syncDisposers.push(
|
||||
reaction(
|
||||
() => toJS(this.toJSON()), // unwrap possible observables and react to everything
|
||||
model => this.onModelChange(model),
|
||||
this.params.syncOptions,
|
||||
),
|
||||
);
|
||||
|
||||
if (ipcMain) {
|
||||
this.syncDisposers.push(ipcMainOn(this.syncMainChannel, (event, model: T) => {
|
||||
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||
this.onSync(model);
|
||||
}));
|
||||
}
|
||||
|
||||
if (ipcRenderer) {
|
||||
this.syncDisposers.push(ipcRendererOn(this.syncRendererChannel, (event, model: T) => {
|
||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||
this.onSyncFromMain(model);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected onSyncFromMain(model: T) {
|
||||
this.applyWithoutSync(() => {
|
||||
this.onSync(model);
|
||||
});
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
ipcRenderer?.removeAllListeners(this.syncMainChannel);
|
||||
ipcRenderer?.removeAllListeners(this.syncRendererChannel);
|
||||
}
|
||||
|
||||
disableSync() {
|
||||
this.syncDisposers.forEach(dispose => dispose());
|
||||
this.syncDisposers.length = 0;
|
||||
}
|
||||
|
||||
protected applyWithoutSync(callback: () => void) {
|
||||
this.disableSync();
|
||||
runInAction(callback);
|
||||
this.enableSync();
|
||||
}
|
||||
|
||||
protected onSync(model: T) {
|
||||
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
|
||||
if (!isEqual(this.toJSON(), model)) {
|
||||
this.fromStore(model);
|
||||
}
|
||||
}
|
||||
|
||||
protected onModelChange(model: T) {
|
||||
if (ipcMain) {
|
||||
this.saveToFile(model); // save config file
|
||||
broadcastMessage(this.syncRendererChannel, model);
|
||||
} else {
|
||||
broadcastMessage(this.syncMainChannel, model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
148
src/common/base-store/base-store.ts
Normal file
148
src/common/base-store/base-store.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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, toJS } from "../utils";
|
||||
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 "../utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
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();
|
||||
|
||||
protected constructor(
|
||||
protected readonly dependencies: BaseStoreDependencies,
|
||||
protected readonly params: BaseStoreParams<T>,
|
||||
) {
|
||||
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({
|
||||
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;
|
||||
}
|
||||
11
src/common/base-store/channel-prefix.ts
Normal file
11
src/common/base-store/channel-prefix.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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",
|
||||
});
|
||||
@ -2,9 +2,9 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { Runnable } from "../../common/runnable/run-many-for";
|
||||
|
||||
export const beforeFrameStartsInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "before-frame-starts",
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
|
||||
export const shouldBaseStoreDisableSyncInIpcListenerInjectionToken = getInjectionToken<boolean>({
|
||||
id: "should-base-store-disable-sync-in-ipc-listener-token",
|
||||
});
|
||||
46
src/common/base-store/migrations.injectable.ts
Normal file
46
src/common/base-store/migrations.injectable.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 "../utils";
|
||||
|
||||
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;
|
||||
12
src/common/base-store/save-to-file.ts
Normal file
12
src/common/base-store/save-to-file.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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 Config from "conf";
|
||||
|
||||
export type PersistStateToConfig = <T extends object>(config: Config<T>, state: T) => void;
|
||||
|
||||
export const persistStateToConfigInjectionToken = getInjectionToken<PersistStateToConfig>({
|
||||
id: "persist-state-to-config-token",
|
||||
});
|
||||
@ -5,13 +5,14 @@
|
||||
|
||||
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
|
||||
import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog/catalog-entity";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
|
||||
import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import getClusterByIdInjectable from "../cluster-store/get-by-id.injectable";
|
||||
|
||||
export interface KubernetesClusterPrometheusMetrics {
|
||||
address?: {
|
||||
@ -63,6 +64,8 @@ export function isKubernetesCluster(item: unknown): item is KubernetesCluster {
|
||||
return item instanceof KubernetesCluster;
|
||||
}
|
||||
|
||||
const getClusterById = asLegacyGlobalFunctionForExtensionApi(getClusterByIdInjectable);
|
||||
|
||||
export class KubernetesCluster<
|
||||
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
|
||||
Status extends KubernetesClusterStatus = KubernetesClusterStatus,
|
||||
@ -76,7 +79,7 @@ export class KubernetesCluster<
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (app) {
|
||||
await ClusterStore.getInstance().getById(this.getId())?.activate();
|
||||
await getClusterById(this.getId())?.activate();
|
||||
} else {
|
||||
await requestClusterActivation(this.getId(), false);
|
||||
}
|
||||
@ -84,7 +87,7 @@ export class KubernetesCluster<
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (app) {
|
||||
ClusterStore.getInstance().getById(this.getId())?.disconnect();
|
||||
getClusterById(this.getId())?.disconnect();
|
||||
} else {
|
||||
await requestClusterDisconnection(this.getId(), false);
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } fro
|
||||
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 "../weblink-store.injectable";
|
||||
import weblinkStoreInjectable from "../weblinks-store/weblink-store.injectable";
|
||||
|
||||
export type WebLinkStatusPhase = "available" | "unavailable";
|
||||
|
||||
@ -34,12 +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(Environments.renderer);
|
||||
const productName = di.inject(productNameInjectable);
|
||||
const weblinkStore = di.inject(weblinkStoreInjectable);
|
||||
|
||||
if (this.metadata.source === "local") {
|
||||
context.menuItems.push({
|
||||
title: "Delete",
|
||||
icon: "delete",
|
||||
onClick: async () => di.inject(weblinkStoreInjectable).removeById(this.getId()),
|
||||
onClick: async () => weblinkStore.removeById(this.getId()),
|
||||
confirm: {
|
||||
message: `Remove Web Link "${this.getName()}" from ${productName}?`,
|
||||
},
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(catalogCategoryRegistryInjectable)` instead
|
||||
*/
|
||||
export const catalogCategoryRegistry = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);
|
||||
@ -3,6 +3,5 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./catalog-category-registry";
|
||||
export * from "./category-registry";
|
||||
export * from "./catalog-entity";
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import clusterStoreInjectable from "./cluster-store.injectable";
|
||||
import type { Cluster } from "../cluster/cluster";
|
||||
import type { ClusterStore } from "./cluster-store";
|
||||
|
||||
export default getGlobalOverride(
|
||||
clusterStoreInjectable,
|
||||
() =>
|
||||
({
|
||||
provideInitialFromMain: () => {},
|
||||
getById: (id) => (void id, {}) as Cluster,
|
||||
} as ClusterStore),
|
||||
);
|
||||
@ -7,21 +7,36 @@ import { ClusterStore } from "./cluster-store";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
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 "../utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
const clusterStoreInjectable = getInjectable({
|
||||
id: "cluster-store",
|
||||
|
||||
instantiate: (di) => {
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
return ClusterStore.createInstance({
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
instantiate: (di) => new ClusterStore({
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
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;
|
||||
|
||||
@ -4,17 +4,12 @@
|
||||
*/
|
||||
|
||||
|
||||
import { ipcMain, ipcRenderer, webFrame } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { BaseStore } from "../base-store";
|
||||
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 migrations from "../../migrations/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { ipcMainHandle } from "../ipc";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
|
||||
import { requestInitialClusterStates } from "../../renderer/ipc";
|
||||
import { clusterStates } from "../ipc/cluster";
|
||||
import { toJS } from "../utils";
|
||||
import type { ClusterModel, ClusterId } from "../cluster-types";
|
||||
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
|
||||
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
|
||||
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
|
||||
@ -23,76 +18,25 @@ export interface ClusterStoreModel {
|
||||
clusters?: ClusterModel[];
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
interface Dependencies extends BaseStoreDependencies {
|
||||
createCluster: CreateCluster;
|
||||
readClusterConfigSync: ReadClusterConfigSync;
|
||||
emitAppEvent: EmitAppEvent;
|
||||
}
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
readonly displayName = "ClusterStore";
|
||||
clusters = observable.map<ClusterId, Cluster>();
|
||||
readonly clusters = observable.map<ClusterId, Cluster>();
|
||||
|
||||
protected disposer = disposer();
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
super({
|
||||
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,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
this.pushStateToViewsAutomatically();
|
||||
}
|
||||
|
||||
async loadInitialOnRenderer() {
|
||||
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
||||
|
||||
for (const { id, state } of await requestInitialClusterStates()) {
|
||||
this.getById(id)?.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
provideInitialFromMain() {
|
||||
ipcMainHandle(clusterStates, () => (
|
||||
this.clustersList.map(cluster => ({
|
||||
id: cluster.id,
|
||||
state: cluster.getState(),
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
protected pushStateToViewsAutomatically() {
|
||||
if (ipcMain) {
|
||||
this.disposer.push(
|
||||
reaction(() => this.connectedClustersList, () => this.pushState()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcListener() {
|
||||
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`);
|
||||
const ipc = ipcMain ?? ipcRenderer;
|
||||
|
||||
ipc?.on("cluster:state", (event, clusterId: ClusterId, state: ClusterState) => {
|
||||
this.getById(clusterId)?.setState(state);
|
||||
});
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
super.unregisterIpcListener();
|
||||
this.disposer();
|
||||
}
|
||||
|
||||
pushState() {
|
||||
this.clusters.forEach((c) => {
|
||||
c.pushState();
|
||||
});
|
||||
}
|
||||
|
||||
@computed get clustersList(): Cluster[] {
|
||||
@ -150,7 +94,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
newClusters.set(clusterModel.id, cluster);
|
||||
} catch (error) {
|
||||
logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`);
|
||||
this.dependencies.logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src/common/cluster-store/migration-token.ts
Normal file
11
src/common/cluster-store/migration-token.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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",
|
||||
});
|
||||
@ -316,7 +316,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes
|
||||
|
||||
this.eventsDisposer.push(
|
||||
reaction(() => this.getState(), state => this.pushState(state)),
|
||||
reaction(
|
||||
() => this.prometheusPreferences,
|
||||
prefs => this.contextHandler.setupPrometheus(prefs),
|
||||
@ -349,7 +348,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
@action
|
||||
async activate(force = false) {
|
||||
if (this.activated && !force) {
|
||||
return this.pushState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||
@ -395,7 +394,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
}
|
||||
|
||||
this.activated = true;
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -437,7 +435,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
this.activated = false;
|
||||
this.allowedNamespaces = [];
|
||||
this.resourceAccessStatuses.clear();
|
||||
this.pushState();
|
||||
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id });
|
||||
}
|
||||
|
||||
@ -448,7 +445,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
async refresh() {
|
||||
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
await this.refreshConnectionStatus();
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -614,16 +610,15 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @param state cluster state
|
||||
*/
|
||||
@action setState(state: ClusterState) {
|
||||
Object.assign(this, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param state cluster state
|
||||
*/
|
||||
pushState(state = this.getState()) {
|
||||
this.dependencies.logger.silly(`[CLUSTER]: push-state`, state);
|
||||
this.dependencies.broadcastMessage("cluster:state", this.id, state);
|
||||
this.accessible = state.accessible;
|
||||
this.allowedNamespaces = state.allowedNamespaces;
|
||||
this.allowedResources = state.allowedResources;
|
||||
this.apiUrl = state.apiUrl;
|
||||
this.disconnected = state.disconnected;
|
||||
this.isAdmin = state.isAdmin;
|
||||
this.isGlobalWatchEnabled = state.isGlobalWatchEnabled;
|
||||
this.online = state.online;
|
||||
this.ready = state.ready;
|
||||
}
|
||||
|
||||
// get cluster system meta, e.g. use in "logger"
|
||||
|
||||
11
src/common/cluster/current-cluster-channel.ts
Normal file
11
src/common/cluster/current-cluster-channel.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ClusterId } from "../cluster-types";
|
||||
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
|
||||
|
||||
export const currentClusterMessageChannel: MessageChannel<ClusterId> = {
|
||||
id: "current-visible-cluster",
|
||||
};
|
||||
@ -3,7 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { HttpsProxyAgent } from "hpagent";
|
||||
import type * as FetchModule from "node-fetch";
|
||||
import userStoreInjectable from "../user-store/user-store.injectable";
|
||||
|
||||
const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule };
|
||||
|
||||
@ -14,7 +16,20 @@ export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
const fetchInjectable = getInjectable({
|
||||
id: "fetch",
|
||||
instantiate: (): Fetch => fetch,
|
||||
instantiate: (di): Fetch => {
|
||||
const { httpsProxy, allowUntrustedCAs } = di.inject(userStoreInjectable);
|
||||
const agent = httpsProxy
|
||||
? new HttpsProxyAgent({
|
||||
proxy: httpsProxy,
|
||||
rejectUnauthorized: !allowUntrustedCAs,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return (url, init = {}) => fetch(url, {
|
||||
agent,
|
||||
...init,
|
||||
});
|
||||
},
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import deleteFileInjectable from "./delete-file.injectable";
|
||||
|
||||
export default getGlobalOverride(deleteFileInjectable, () => async () => {
|
||||
throw new Error("tried to delete file without override");
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverrideForFunction } from "../test-utils/get-global-override-for-function";
|
||||
import execFileInjectable from "./exec-file.injectable";
|
||||
|
||||
export default getGlobalOverrideForFunction(execFileInjectable);
|
||||
@ -3,11 +3,61 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { ReadOptions } from "fs-extra";
|
||||
import fse from "fs-extra";
|
||||
|
||||
/**
|
||||
* NOTE: Add corrisponding a corrisponding override of this injecable in `src/test-utils/override-fs-with-fakes.ts`
|
||||
*/
|
||||
const fsInjectable = getInjectable({
|
||||
id: "fs",
|
||||
instantiate: () => fse,
|
||||
instantiate: () => {
|
||||
const {
|
||||
promises: {
|
||||
readFile,
|
||||
writeFile,
|
||||
readdir,
|
||||
lstat,
|
||||
rm,
|
||||
access,
|
||||
stat,
|
||||
},
|
||||
ensureDir,
|
||||
ensureDirSync,
|
||||
readFileSync,
|
||||
readJson,
|
||||
writeJson,
|
||||
readJsonSync,
|
||||
writeFileSync,
|
||||
writeJsonSync,
|
||||
pathExistsSync,
|
||||
pathExists,
|
||||
copy,
|
||||
createReadStream,
|
||||
} = fse;
|
||||
|
||||
return {
|
||||
readFile,
|
||||
readJson: readJson as (file: string, options?: ReadOptions | BufferEncoding) => Promise<any>,
|
||||
writeFile,
|
||||
writeJson,
|
||||
pathExists,
|
||||
readdir,
|
||||
readFileSync,
|
||||
readJsonSync,
|
||||
writeFileSync,
|
||||
writeJsonSync,
|
||||
pathExistsSync,
|
||||
lstat,
|
||||
rm,
|
||||
access,
|
||||
copy: copy as (src: string, dest: string, options?: fse.CopyOptions) => Promise<void>,
|
||||
ensureDir: ensureDir as (path: string, options?: number | fse.EnsureOptions ) => Promise<void>,
|
||||
ensureDirSync,
|
||||
createReadStream,
|
||||
stat,
|
||||
};
|
||||
},
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@ -1,16 +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 { MoveOptions } from "fs-extra";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type Move = (src: string, dest: string, options?: MoveOptions) => Promise<void>;
|
||||
|
||||
const moveInjectable = getInjectable({
|
||||
id: "move",
|
||||
instantiate: (di): Move => di.inject(fsInjectable).move,
|
||||
});
|
||||
|
||||
export default moveInjectable;
|
||||
@ -5,11 +5,9 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type DeleteFile = (filePath: string) => Promise<void>;
|
||||
|
||||
const deleteFileInjectable = getInjectable({
|
||||
id: "delete-file",
|
||||
instantiate: (di): DeleteFile => di.inject(fsInjectable).unlink,
|
||||
const pathExistsSyncInjectable = getInjectable({
|
||||
id: "path-exists-sync",
|
||||
instantiate: (di) => di.inject(fsInjectable).pathExistsSync,
|
||||
});
|
||||
|
||||
export default deleteFileInjectable;
|
||||
export default pathExistsSyncInjectable;
|
||||
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import pathExistsInjectable from "./path-exists.injectable";
|
||||
|
||||
export default getGlobalOverride(pathExistsInjectable, () => async () => {
|
||||
throw new Error("Tried to check if a path exists without override");
|
||||
});
|
||||
@ -14,18 +14,16 @@ export interface ReadDirectory {
|
||||
(
|
||||
path: string,
|
||||
options?:
|
||||
| { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined }
|
||||
| { encoding: BufferEncoding; withFileTypes?: false | undefined }
|
||||
| BufferEncoding
|
||||
| string
|
||||
| null,
|
||||
): Promise<string[]>;
|
||||
(
|
||||
path: string,
|
||||
options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined },
|
||||
options?: { encoding?: BufferEncoding; withFileTypes?: false | undefined },
|
||||
): Promise<string[] | Buffer[]>;
|
||||
(
|
||||
path: string,
|
||||
options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true },
|
||||
options: { encoding?: BufferEncoding; withFileTypes: true },
|
||||
): Promise<Dirent[]>;
|
||||
}
|
||||
|
||||
|
||||
19
src/common/fs/read-file-buffer-sync.injectable.ts
Normal file
19
src/common/fs/read-file-buffer-sync.injectable.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 fsInjectable from "./fs.injectable";
|
||||
|
||||
export type ReadFileBufferSync = (filePath: string) => Buffer;
|
||||
|
||||
const readFileBufferSyncInjectable = getInjectable({
|
||||
id: "read-file-buffer-sync",
|
||||
instantiate: (di): ReadFileBufferSync => {
|
||||
const { readFileSync } = di.inject(fsInjectable);
|
||||
|
||||
return (filePath) => readFileSync(filePath);
|
||||
},
|
||||
});
|
||||
|
||||
export default readFileBufferSyncInjectable;
|
||||
13
src/common/fs/read-json-sync.injectable.ts
Normal file
13
src/common/fs/read-json-sync.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
const readJsonSyncInjectable = getInjectable({
|
||||
id: "read-json-sync",
|
||||
instantiate: (di) => di.inject(fsInjectable).readJsonSync,
|
||||
});
|
||||
|
||||
export default readJsonSyncInjectable;
|
||||
@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import removePathInjectable from "./remove-path.injectable";
|
||||
import removePathInjectable from "./remove.injectable";
|
||||
|
||||
export default getGlobalOverride(removePathInjectable, () => async () => {
|
||||
throw new Error("tried to remove a path without override");
|
||||
throw new Error("tried to remove path without override");
|
||||
});
|
||||
@ -5,11 +5,15 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type RemovePath = (path: string) => Promise<void>;
|
||||
export type RemovePath = (filePath: string) => Promise<void>;
|
||||
|
||||
const removePathInjectable = getInjectable({
|
||||
id: "remove-path",
|
||||
instantiate: (di): RemovePath => di.inject(fsInjectable).remove,
|
||||
instantiate: (di): RemovePath => {
|
||||
const { rm } = di.inject(fsInjectable);
|
||||
|
||||
return (filePath) => rm(filePath, { force: true });
|
||||
},
|
||||
});
|
||||
|
||||
export default removePathInjectable;
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Stats } from "fs";
|
||||
import fsInjectable from "../fs.injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type Stat = (path: string) => Promise<Stats>;
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import statInjectable from "./stat.injectable";
|
||||
import { getGlobalOverride } from "../../test-utils/get-global-override";
|
||||
|
||||
export default getGlobalOverride(statInjectable, () => () => {
|
||||
throw new Error("Tried to call stat without explicit override");
|
||||
});
|
||||
@ -7,7 +7,7 @@ import type { AsyncResult } from "../utils/async-result";
|
||||
import { isErrnoException } from "../utils";
|
||||
import type { Stats } from "fs-extra";
|
||||
import { lowerFirst } from "lodash/fp";
|
||||
import statInjectable from "./stat/stat.injectable";
|
||||
import statInjectable from "./stat.injectable";
|
||||
|
||||
export type ValidateDirectory = (path: string) => Promise<AsyncResult<undefined>>;
|
||||
|
||||
|
||||
29
src/common/fs/write-file-sync.injectable.ts
Normal file
29
src/common/fs/write-file-sync.injectable.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 getDirnameOfPathInjectable from "../path/get-dirname.injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type WriteFileSync = (filePath: string, contents: string) => void;
|
||||
|
||||
const writeFileSyncInjectable = getInjectable({
|
||||
id: "write-file-sync",
|
||||
instantiate: (di): WriteFileSync => {
|
||||
const {
|
||||
writeFileSync,
|
||||
ensureDirSync,
|
||||
} = di.inject(fsInjectable);
|
||||
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
|
||||
|
||||
return (filePath, contents) => {
|
||||
ensureDirSync(getDirnameOfPath(filePath), {
|
||||
mode: 0o755,
|
||||
});
|
||||
writeFileSync(filePath, contents);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default writeFileSyncInjectable;
|
||||
@ -16,15 +16,17 @@ const writeFileInjectable = getInjectable({
|
||||
const { writeFile, ensureDir } = di.inject(fsInjectable);
|
||||
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
|
||||
|
||||
return async (filePath, content, opts) => {
|
||||
return async (filePath, content, opts = {}) => {
|
||||
await ensureDir(getDirnameOfPath(filePath), {
|
||||
mode: 0o755,
|
||||
...(opts ?? {}),
|
||||
...opts,
|
||||
});
|
||||
|
||||
const { encoding = "utf-8", ...options } = opts;
|
||||
|
||||
await writeFile(filePath, content, {
|
||||
encoding: "utf-8",
|
||||
...(opts ?? {}),
|
||||
encoding: encoding as BufferEncoding,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
@ -3,11 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { JsonValue } from "type-fest";
|
||||
import getDirnameOfPathInjectable from "../path/get-dirname.injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type WriteJson = (filePath: string, contents: JsonValue) => Promise<void>;
|
||||
export type WriteJson = (filePath: string, contents: unknown) => Promise<void>;
|
||||
|
||||
const writeJsonFileInjectable = getInjectable({
|
||||
id: "write-json-file",
|
||||
|
||||
31
src/common/fs/write-json-sync.injectable.ts
Normal file
31
src/common/fs/write-json-sync.injectable.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 getDirnameOfPathInjectable from "../path/get-dirname.injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type WriteJsonSync = (filePath: string, contents: unknown) => void;
|
||||
|
||||
const writeJsonSyncInjectable = getInjectable({
|
||||
id: "write-json-sync",
|
||||
instantiate: (di): WriteJsonSync => {
|
||||
const {
|
||||
writeJsonSync,
|
||||
ensureDirSync,
|
||||
} = di.inject(fsInjectable);
|
||||
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
|
||||
|
||||
return (filePath, content) => {
|
||||
ensureDirSync(getDirnameOfPath(filePath), { mode: 0o755 });
|
||||
|
||||
writeJsonSync(filePath, content, {
|
||||
encoding: "utf-8",
|
||||
spaces: 2,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default writeJsonSyncInjectable;
|
||||
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import assert from "assert";
|
||||
import path from "path";
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import getConfigurationFileModelInjectable from "./get-configuration-file-model.injectable";
|
||||
import type Config from "conf";
|
||||
import readJsonSyncInjectable from "../fs/read-json-sync.injectable";
|
||||
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
|
||||
|
||||
export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => {
|
||||
const readJsonSync = di.inject(readJsonSyncInjectable);
|
||||
const writeJsonSync = di.inject(writeJsonSyncInjectable);
|
||||
|
||||
return (options) => {
|
||||
assert(options.cwd, "Missing options.cwd");
|
||||
assert(options.configName, "Missing options.configName");
|
||||
|
||||
const configFilePath = path.posix.join(options.cwd, `${options.configName}.json`);
|
||||
let store: object = {};
|
||||
|
||||
try {
|
||||
store = readJsonSync(configFilePath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return {
|
||||
get store() {
|
||||
return store;
|
||||
},
|
||||
path: configFilePath,
|
||||
set: (key: string, value: unknown) => {
|
||||
let currentState: object;
|
||||
|
||||
try {
|
||||
currentState = readJsonSync(configFilePath);
|
||||
} catch {
|
||||
currentState = {};
|
||||
}
|
||||
|
||||
writeJsonSync(configFilePath, {
|
||||
...currentState,
|
||||
[key]: value,
|
||||
});
|
||||
store = readJsonSync(configFilePath);
|
||||
},
|
||||
} as Partial<Config> as Config<any>;
|
||||
};
|
||||
});
|
||||
@ -4,11 +4,13 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import Config from "conf";
|
||||
import type { BaseStoreParams } from "../base-store";
|
||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||
|
||||
export type GetConfigurationFileModel = <T extends object>(content: ConfOptions<T>) => Config<T>;
|
||||
|
||||
const getConfigurationFileModelInjectable = getInjectable({
|
||||
id: "get-configuration-file-model",
|
||||
instantiate: () => <T extends object>(content: BaseStoreParams<T>) => new Config(content),
|
||||
instantiate: (): GetConfigurationFileModel => (content) => new Config(content),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@ -2,15 +2,15 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type HelmRepo = {
|
||||
|
||||
export interface HelmRepo {
|
||||
name: string;
|
||||
url: string;
|
||||
cacheFilePath?: string;
|
||||
cacheFilePath: string;
|
||||
caFile?: string;
|
||||
certFile?: string;
|
||||
insecureSkipTlsVerify?: boolean;
|
||||
keyFile?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
11
src/common/hotbars/migrations-token.ts
Normal file
11
src/common/hotbars/migrations-token.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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",
|
||||
});
|
||||
@ -6,20 +6,33 @@ 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 "../utils/channel/enlist-message-channel-listener-injection-token";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
|
||||
|
||||
const hotbarStoreInjectable = getInjectable({
|
||||
id: "hotbar-store",
|
||||
|
||||
instantiate: (di) => {
|
||||
HotbarStore.resetInstance();
|
||||
|
||||
return HotbarStore.createInstance({
|
||||
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
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;
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||
import { BaseStore } from "../base-store";
|
||||
import migrations from "../../migrations/hotbar-store";
|
||||
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";
|
||||
@ -21,26 +21,23 @@ export interface HotbarStoreModel {
|
||||
activeHotbarId: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
interface Dependencies extends BaseStoreDependencies {
|
||||
readonly catalogCatalogEntity: GeneralEntity;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
readonly displayName = "HotbarStore";
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@observable private _activeHotbarId!: string;
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
super({
|
||||
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,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@ -99,21 +96,19 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
this.hotbars.forEach(ensureExactHotbarItemLength);
|
||||
|
||||
if (data.activeHotbarId) {
|
||||
this.setActiveHotbar(data.activeHotbarId);
|
||||
this._activeHotbarId = data.activeHotbarId;
|
||||
}
|
||||
|
||||
if (!this.activeHotbarId) {
|
||||
this.setActiveHotbar(0);
|
||||
if (!this._activeHotbarId) {
|
||||
this._activeHotbarId = this.hotbars[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): HotbarStoreModel {
|
||||
const model: HotbarStoreModel = {
|
||||
return toJS({
|
||||
hotbars: this.hotbars,
|
||||
activeHotbarId: this.activeHotbarId,
|
||||
};
|
||||
|
||||
return toJS(model);
|
||||
});
|
||||
}
|
||||
|
||||
getActive(): Hotbar {
|
||||
@ -148,7 +143,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
return void console.warn(
|
||||
return this.dependencies.logger.warn(
|
||||
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
|
||||
{ id },
|
||||
);
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export const openFilePickingDialogChannel = "dialog:open:file-picking";
|
||||
@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
|
||||
export const setNativeThemeChannel = "theme:set-native-theme";
|
||||
export const getNativeThemeChannel = "theme:get-native-theme";
|
||||
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import request from "request";
|
||||
import requestPromise from "request-promise-native";
|
||||
import { UserStore } from "./user-store";
|
||||
|
||||
// todo: get rid of "request" (deprecated)
|
||||
// https://github.com/lensapp/lens/issues/459
|
||||
|
||||
function getDefaultRequestOpts(): Partial<request.Options> {
|
||||
const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance();
|
||||
|
||||
return {
|
||||
proxy: httpsProxy || undefined,
|
||||
rejectUnauthorized: !allowUntrustedCAs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function customRequest(opts: request.Options) {
|
||||
return request.defaults(getDefaultRequestOpts())(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function customRequestPromise(opts: requestPromise.Options) {
|
||||
return requestPromise.defaults(getDefaultRequestOpts())(opts);
|
||||
}
|
||||
@ -8,6 +8,8 @@ import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/i
|
||||
import type { Runnable } from "./run-many-for";
|
||||
import { runManyFor } from "./run-many-for";
|
||||
import { getPromiseStatus } from "../test-utils/get-promise-status";
|
||||
import { runInAction } from "mobx";
|
||||
import { flushPromises } from "../test-utils/flush-promises";
|
||||
|
||||
describe("runManyFor", () => {
|
||||
describe("given no hierarchy, when running many", () => {
|
||||
@ -223,7 +225,68 @@ describe("runManyFor", () => {
|
||||
);
|
||||
|
||||
return expect(() => runMany()).rejects.toThrow(
|
||||
/Tried to get a composite but encountered missing parent ids: "some-runnable-2".\n\nAvailable parent ids are:\n"[0-9a-z-]+",\n"some-runnable-1"/,
|
||||
/Runnable "some-runnable-1" is unreachable for injection token "some-injection-token": run afters "some-runnable-2" are a part of different injection tokens./,
|
||||
);
|
||||
});
|
||||
|
||||
it("given partially incorrect hierarchy, when running runnables, throws", () => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
const runMock = asyncFn<(...args: unknown[]) => void>();
|
||||
|
||||
const someInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const someOtherInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-other-injection-token",
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-runnable-1",
|
||||
|
||||
instantiate: (di) => ({
|
||||
id: "some-runnable-1",
|
||||
run: () => runMock("some-runnable-1"),
|
||||
runAfter: [
|
||||
di.inject(someOtherInjectable),
|
||||
di.inject(someSecondInjectable),
|
||||
],
|
||||
}),
|
||||
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const someSecondInjectable = getInjectable({
|
||||
id: "some-runnable-2",
|
||||
|
||||
instantiate: () => ({
|
||||
id: "some-runnable-2",
|
||||
run: () => runMock("some-runnable-2"),
|
||||
}),
|
||||
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const someOtherInjectable = getInjectable({
|
||||
id: "some-runnable-3",
|
||||
|
||||
instantiate: () => ({
|
||||
id: "some-runnable-3",
|
||||
run: () => runMock("some-runnable-3"),
|
||||
}),
|
||||
|
||||
injectionToken: someOtherInjectionToken,
|
||||
});
|
||||
|
||||
rootDi.register(someInjectable, someOtherInjectable, someSecondInjectable);
|
||||
|
||||
const runMany = runManyFor(rootDi)(
|
||||
someInjectionToken,
|
||||
);
|
||||
|
||||
return expect(() => runMany()).rejects.toThrow(
|
||||
/Runnable "some-runnable-3" is not part of the injection token "some-injection-token"/,
|
||||
);
|
||||
});
|
||||
|
||||
@ -279,4 +342,319 @@ describe("runManyFor", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple runAfters", () => {
|
||||
let runMock: AsyncFnMock<(...args: unknown[]) => void>;
|
||||
let finishingPromise: Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const rootDi = createContainer("irrelevant");
|
||||
|
||||
runMock = asyncFn<(...args: unknown[]) => void>();
|
||||
|
||||
const someInjectionToken = getInjectionToken<Runnable>({
|
||||
id: "some-injection-token",
|
||||
});
|
||||
|
||||
const runnableOneInjectable = getInjectable({
|
||||
id: "runnable-1",
|
||||
instantiate: () => ({
|
||||
id: "runnable-1",
|
||||
run: () => runMock("runnable-1"),
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableTwoInjectable = getInjectable({
|
||||
id: "runnable-2",
|
||||
instantiate: () => ({
|
||||
id: "runnable-2",
|
||||
run: () => runMock("runnable-2"),
|
||||
runAfter: [], // shouldn't block being called
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableThreeInjectable = getInjectable({
|
||||
id: "runnable-3",
|
||||
instantiate: (di) => ({
|
||||
id: "runnable-3",
|
||||
run: () => runMock("runnable-3"),
|
||||
runAfter: di.inject(runnableOneInjectable),
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableFourInjectable = getInjectable({
|
||||
id: "runnable-4",
|
||||
instantiate: (di) => ({
|
||||
id: "runnable-4",
|
||||
run: () => runMock("runnable-4"),
|
||||
runAfter: [di.inject(runnableThreeInjectable)], // should be the same as an single item
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableFiveInjectable = getInjectable({
|
||||
id: "runnable-5",
|
||||
instantiate: (di) => ({
|
||||
id: "runnable-5",
|
||||
run: () => runMock("runnable-5"),
|
||||
runAfter: di.inject(runnableThreeInjectable),
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableSixInjectable = getInjectable({
|
||||
id: "runnable-6",
|
||||
instantiate: (di) => ({
|
||||
id: "runnable-6",
|
||||
run: () => runMock("runnable-6"),
|
||||
runAfter: [
|
||||
di.inject(runnableFourInjectable),
|
||||
di.inject(runnableFiveInjectable),
|
||||
],
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
const runnableSevenInjectable = getInjectable({
|
||||
id: "runnable-7",
|
||||
instantiate: (di) => ({
|
||||
id: "runnable-7",
|
||||
run: () => runMock("runnable-7"),
|
||||
runAfter: [
|
||||
di.inject(runnableFiveInjectable),
|
||||
di.inject(runnableSixInjectable),
|
||||
],
|
||||
}),
|
||||
injectionToken: someInjectionToken,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
rootDi.register(
|
||||
runnableOneInjectable,
|
||||
runnableTwoInjectable,
|
||||
runnableThreeInjectable,
|
||||
runnableFourInjectable,
|
||||
runnableFiveInjectable,
|
||||
runnableSixInjectable,
|
||||
runnableSevenInjectable,
|
||||
);
|
||||
});
|
||||
|
||||
const runMany = runManyFor(rootDi);
|
||||
const runSome = runMany(someInjectionToken);
|
||||
|
||||
finishingPromise = runSome();
|
||||
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("should run 'runnable-1'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-1");
|
||||
});
|
||||
|
||||
it("should run 'runnable-2'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-2");
|
||||
});
|
||||
|
||||
it("should not run 'runnable-3'", () => {
|
||||
expect(runMock).not.toBeCalledWith("runnable-3");
|
||||
});
|
||||
|
||||
describe("when 'runnable-1' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-1"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-3'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-3");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-3' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-3"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-4'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-4");
|
||||
});
|
||||
|
||||
it("should run 'runnable-5'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-5");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-4' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-4"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-5' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-5"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-6'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-6");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-6' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-6"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-7'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-7");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(7);
|
||||
});
|
||||
|
||||
describe("when 'runnable-7' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-7"]);
|
||||
});
|
||||
|
||||
it("should resolve the runMany promise call", async () => {
|
||||
await finishingPromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-5' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-5"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-4' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-4"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-6'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-6");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-6' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-6"]);
|
||||
});
|
||||
|
||||
it("should run 'runnable-7'", () => {
|
||||
expect(runMock).toBeCalledWith("runnable-7");
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(7);
|
||||
});
|
||||
|
||||
describe("when 'runnable-7' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-7"]);
|
||||
});
|
||||
|
||||
it("should resolve the runMany promise call", async () => {
|
||||
await finishingPromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when 'runnable-2' resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await runMock.resolveSpecific(["runnable-2"]);
|
||||
});
|
||||
|
||||
it("shouldn't call any more runnables", () => {
|
||||
expect(runMock).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,46 +3,138 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
|
||||
import type { Composite } from "../utils/composite/get-composite/get-composite";
|
||||
import { getCompositeFor } from "../utils/composite/get-composite/get-composite";
|
||||
import type { SingleOrMany } from "../utils";
|
||||
import { getOrInsert, getOrInsertSetFor, isDefined } from "../utils";
|
||||
import * as uuid from "uuid";
|
||||
import assert from "assert";
|
||||
import type { Asyncify } from "type-fest";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import EventEmitter from "events";
|
||||
|
||||
export interface Runnable<TParameter = void> {
|
||||
id: string;
|
||||
run: Run<TParameter>;
|
||||
runAfter?: Runnable<TParameter>;
|
||||
runAfter?: SingleOrMany<Runnable<TParameter>>;
|
||||
}
|
||||
|
||||
type Run<Param> = (parameter: Param) => Promise<void> | void;
|
||||
|
||||
export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Run<Param>;
|
||||
export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Asyncify<Run<Param>>;
|
||||
|
||||
async function runCompositeRunnables<Param>(param: Param, composite: Composite<Runnable<Param>>) {
|
||||
await composite.value.run(param);
|
||||
await Promise.all(composite.children.map(composite => runCompositeRunnables(param, composite)));
|
||||
const computedNextEdge = (traversed: string[], graph: Map<string, Set<string>>, currentId: string, seenIds: Set<string>) => {
|
||||
seenIds.add(currentId);
|
||||
const currentNode = graph.get(currentId);
|
||||
|
||||
assert(currentNode, `Runnable graph does not contain node with id="${currentId}"`);
|
||||
|
||||
for (const nextId of currentNode.values()) {
|
||||
if (traversed.includes(nextId)) {
|
||||
throw new Error(`Cycle in runnable graph: "${traversed.join(`" -> "`)}" -> "${nextId}"`);
|
||||
}
|
||||
|
||||
computedNextEdge([...traversed, nextId], graph, nextId, seenIds);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyRunnablesAreDAG = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>, runnables: Runnable<Param>[]) => {
|
||||
const rootId = uuid.v4();
|
||||
const runnableGraph = new Map<string, Set<string>>();
|
||||
const seenIds = new Set<string>();
|
||||
const addRunnableId = getOrInsertSetFor(runnableGraph);
|
||||
|
||||
// Build the Directed graph
|
||||
for (const runnable of runnables) {
|
||||
addRunnableId(runnable.id);
|
||||
|
||||
if (!runnable.runAfter || (Array.isArray(runnable.runAfter) && runnable.runAfter.length === 0)) {
|
||||
addRunnableId(rootId).add(runnable.id);
|
||||
} else if (Array.isArray(runnable.runAfter)) {
|
||||
for (const parentRunnable of runnable.runAfter) {
|
||||
addRunnableId(parentRunnable.id).add(runnable.id);
|
||||
}
|
||||
} else {
|
||||
addRunnableId(runnable.runAfter.id).add(runnable.id);
|
||||
}
|
||||
}
|
||||
|
||||
addRunnableId(rootId);
|
||||
|
||||
// Do a DFS to find any cycles
|
||||
computedNextEdge([], runnableGraph, rootId, seenIds);
|
||||
|
||||
for (const id of runnableGraph.keys()) {
|
||||
if (!seenIds.has(id)) {
|
||||
const runnable = runnables.find(runnable => runnable.id === id);
|
||||
|
||||
if (!runnable) {
|
||||
throw new Error(`Runnable "${id}" is not part of the injection token "${injectionToken.id}"`);
|
||||
}
|
||||
|
||||
const runAfters = [runnable.runAfter]
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
.map(runnable => runnable.id)
|
||||
.join('", "');
|
||||
|
||||
throw new Error(`Runnable "${id}" is unreachable for injection token "${injectionToken.id}": run afters "${runAfters}" are a part of different injection tokens.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface BarrierEvent {
|
||||
finish: (id: string) => void;
|
||||
}
|
||||
|
||||
class DynamicBarrier {
|
||||
private readonly finishedIds = new Map<string, Promise<void>>();
|
||||
private readonly events: TypedEventEmitter<BarrierEvent> = new EventEmitter();
|
||||
|
||||
private initFinishingPromise(id: string): Promise<void> {
|
||||
return getOrInsert(this.finishedIds, id, new Promise(resolve => {
|
||||
const handler = (finishedId: string) => {
|
||||
if (finishedId === id) {
|
||||
resolve();
|
||||
this.events.removeListener("finish", handler);
|
||||
}
|
||||
};
|
||||
|
||||
this.events.addListener("finish", handler);
|
||||
}));
|
||||
}
|
||||
|
||||
setFinished(id: string): void {
|
||||
void this.initFinishingPromise(id);
|
||||
|
||||
this.events.emit("finish", id);
|
||||
}
|
||||
|
||||
async blockOn(id: string): Promise<void> {
|
||||
await this.initFinishingPromise(id);
|
||||
}
|
||||
}
|
||||
|
||||
const executeRunnableWith = <Param>(param: Param) => {
|
||||
const barrier = new DynamicBarrier();
|
||||
|
||||
return async (runnable: Runnable<Param>): Promise<void> => {
|
||||
const parentRunnables = [runnable.runAfter].flat().filter(isDefined);
|
||||
|
||||
for (const parentRunnable of parentRunnables) {
|
||||
await barrier.blockOn(parentRunnable.id);
|
||||
}
|
||||
|
||||
await runnable.run(param);
|
||||
barrier.setFinished(runnable.id);
|
||||
};
|
||||
};
|
||||
|
||||
export function runManyFor(di: DiContainerForInjection): RunMany {
|
||||
return <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => async (param: Param) => {
|
||||
const executeRunnable = executeRunnableWith(param);
|
||||
const allRunnables = di.injectMany(injectionToken);
|
||||
const rootId = uuid.v4();
|
||||
const getCompositeRunnables = getCompositeFor<Runnable<Param>>({
|
||||
getId: (runnable) => runnable.id,
|
||||
getParentId: (runnable) => (
|
||||
runnable.id === rootId
|
||||
? undefined
|
||||
: runnable.runAfter?.id ?? rootId
|
||||
),
|
||||
});
|
||||
const composite = getCompositeRunnables([
|
||||
// This is a dummy runnable to conform to the requirements of `getCompositeFor` to only have one root
|
||||
{
|
||||
id: rootId,
|
||||
run: () => {},
|
||||
},
|
||||
...allRunnables,
|
||||
]);
|
||||
|
||||
await runCompositeRunnables(param, composite);
|
||||
verifyRunnablesAreDAG(injectionToken, allRunnables);
|
||||
|
||||
await Promise.all(allRunnables.map(executeRunnable));
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,8 +4,6 @@
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import moveInjectable from "./move.injectable";
|
||||
import currentTimezoneInjectable from "./current-timezone.injectable";
|
||||
|
||||
export default getGlobalOverride(moveInjectable, () => async () => {
|
||||
throw new Error("tried to move without override");
|
||||
});
|
||||
export default getGlobalOverride(currentTimezoneInjectable, () => "Etc/GMT");
|
||||
14
src/common/user-store/current-timezone.injectable.ts
Normal file
14
src/common/user-store/current-timezone.injectable.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
const currentTimezoneInjectable = getInjectable({
|
||||
id: "current-timezone",
|
||||
instantiate: () => moment.tz.guess(true),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default currentTimezoneInjectable;
|
||||
18
src/common/user-store/https-proxy.injectable.ts
Normal file
18
src/common/user-store/https-proxy.injectable.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } 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;
|
||||
@ -7,11 +7,7 @@ import userStoreInjectable from "./user-store.injectable";
|
||||
|
||||
const kubeconfigSyncsInjectable = getInjectable({
|
||||
id: "kubeconfig-syncs",
|
||||
instantiate: (di) => {
|
||||
const store = di.inject(userStoreInjectable);
|
||||
|
||||
return store.syncKubeconfigEntries;
|
||||
},
|
||||
instantiate: (di) => di.inject(userStoreInjectable).syncKubeconfigEntries,
|
||||
});
|
||||
|
||||
export default kubeconfigSyncsInjectable;
|
||||
|
||||
37
src/common/user-store/lens-color-theme.injectable.ts
Normal file
37
src/common/user-store/lens-color-theme.injectable.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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;
|
||||
11
src/common/user-store/migrations-token.ts
Normal file
11
src/common/user-store/migrations-token.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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",
|
||||
});
|
||||
143
src/common/user-store/preference-descriptors.injectable.ts
Normal file
143
src/common/user-store/preference-descriptors.injectable.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -3,14 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment-timezone";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import type { editor } from "monaco-editor";
|
||||
import merge from "lodash/merge";
|
||||
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
|
||||
import type { ObservableMap } from "mobx";
|
||||
import { observable } from "mobx";
|
||||
import { defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
|
||||
import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
|
||||
|
||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||
filePath: string;
|
||||
@ -54,86 +49,8 @@ export interface PreferenceDescription<T, R = T> {
|
||||
toStore(val: R): T | undefined;
|
||||
}
|
||||
|
||||
const httpsProxy: PreferenceDescription<string | undefined> = {
|
||||
fromStore(val) {
|
||||
return val;
|
||||
},
|
||||
toStore(val) {
|
||||
return val || undefined;
|
||||
},
|
||||
};
|
||||
export const getPreferenceDescriptor = <T, R = T>(desc: PreferenceDescription<T, R>) => desc;
|
||||
|
||||
const shell: PreferenceDescription<string | undefined> = {
|
||||
fromStore(val) {
|
||||
return val;
|
||||
},
|
||||
toStore(val) {
|
||||
return val || undefined;
|
||||
},
|
||||
};
|
||||
|
||||
const colorTheme: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return val || defaultThemeId;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val || val === defaultThemeId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const terminalTheme: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return val || "";
|
||||
},
|
||||
toStore(val) {
|
||||
return val || undefined;
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultLocaleTimezone = "UTC";
|
||||
|
||||
const localeTimezone: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return val || moment.tz.guess(true) || defaultLocaleTimezone;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val || val === moment.tz.guess(true) || val === defaultLocaleTimezone) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const allowUntrustedCAs: PreferenceDescription<boolean> = {
|
||||
fromStore(val) {
|
||||
return val ?? false;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const allowErrorReporting: PreferenceDescription<boolean> = {
|
||||
fromStore(val) {
|
||||
return val ?? true;
|
||||
},
|
||||
toStore(val) {
|
||||
if (val === true) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
export interface DownloadMirror {
|
||||
url: string;
|
||||
@ -157,142 +74,6 @@ export const packageMirrors = new Map<string, DownloadMirror>([
|
||||
}],
|
||||
]);
|
||||
|
||||
const downloadMirror: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return !val || !packageMirrors.has(val)
|
||||
? defaultPackageMirror
|
||||
: val;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val || val === defaultPackageMirror) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const downloadKubectlBinaries: PreferenceDescription<boolean> = {
|
||||
fromStore(val) {
|
||||
return val ?? true;
|
||||
},
|
||||
toStore(val) {
|
||||
if (val === true) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const downloadBinariesPath: PreferenceDescription<string | undefined> = {
|
||||
fromStore(val) {
|
||||
return val;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const kubectlBinariesPath: PreferenceDescription<string | undefined> = {
|
||||
fromStore(val) {
|
||||
return val;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const openAtLogin: PreferenceDescription<boolean> = {
|
||||
fromStore(val) {
|
||||
return val ?? false;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const terminalCopyOnSelect: PreferenceDescription<boolean> = {
|
||||
fromStore(val) {
|
||||
return val ?? false;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, Set<string>>> = {
|
||||
fromStore(val) {
|
||||
return 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;
|
||||
},
|
||||
};
|
||||
|
||||
const mainKubeFolder = path.join(os.homedir(), ".kube");
|
||||
|
||||
const syncKubeconfigEntries: PreferenceDescription<KubeconfigSyncEntry[], ObservableMap<string, KubeconfigSyncValue>> = {
|
||||
fromStore(val) {
|
||||
return observable.map(
|
||||
val
|
||||
?.map(({ filePath, ...rest }) => [filePath, rest])
|
||||
?? [[mainKubeFolder, {}]],
|
||||
);
|
||||
},
|
||||
toStore(val) {
|
||||
if (val.size === 1 && val.has(mainKubeFolder)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array.from(val, ([filePath, rest]) => ({ filePath, ...rest }));
|
||||
},
|
||||
};
|
||||
|
||||
const editorConfiguration: PreferenceDescription<Partial<EditorConfiguration> | undefined, EditorConfiguration> = {
|
||||
fromStore(val) {
|
||||
return merge(defaultEditorConfig, val);
|
||||
},
|
||||
toStore(val) {
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const terminalConfig: PreferenceDescription<TerminalConfig, TerminalConfig> = {
|
||||
fromStore(val) {
|
||||
return merge(defaultTerminalConfig, val);
|
||||
},
|
||||
toStore(val) {
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
export type ExtensionRegistryLocation = "default" | "npmrc" | "custom";
|
||||
|
||||
export type ExtensionRegistry = {
|
||||
@ -306,49 +87,13 @@ export type ExtensionRegistry = {
|
||||
export const defaultExtensionRegistryUrlLocation = "default";
|
||||
export const defaultExtensionRegistryUrl = "https://registry.npmjs.org";
|
||||
|
||||
const extensionRegistryUrl: PreferenceDescription<ExtensionRegistry> = {
|
||||
fromStore(val) {
|
||||
return val ?? {
|
||||
location: defaultExtensionRegistryUrlLocation,
|
||||
};
|
||||
},
|
||||
toStore(val) {
|
||||
if (val.location === defaultExtensionRegistryUrlLocation) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
|
||||
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
||||
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 typeof DESCRIPTORS]: UserStoreModelType<field>;
|
||||
[field in keyof PreferenceDescriptors]: UserStoreModelType<field>;
|
||||
};
|
||||
|
||||
export type UserPreferencesModel = {
|
||||
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>;
|
||||
[field in keyof PreferenceDescriptors]: PreferencesModelType<field>;
|
||||
} & { updateChannel: string };
|
||||
|
||||
export const DESCRIPTORS = {
|
||||
httpsProxy,
|
||||
shell,
|
||||
colorTheme,
|
||||
terminalTheme,
|
||||
localeTimezone,
|
||||
allowUntrustedCAs,
|
||||
allowErrorReporting,
|
||||
downloadMirror,
|
||||
downloadKubectlBinaries,
|
||||
downloadBinariesPath,
|
||||
kubectlBinariesPath,
|
||||
openAtLogin,
|
||||
hiddenTableColumns,
|
||||
syncKubeconfigEntries,
|
||||
editorConfiguration,
|
||||
terminalCopyOnSelect,
|
||||
terminalConfig,
|
||||
extensionRegistryUrl,
|
||||
};
|
||||
|
||||
37
src/common/user-store/terminal-theme.injectable.ts
Normal file
37
src/common/user-store/terminal-theme.injectable.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -6,20 +6,37 @@ 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 "../utils/channel/enlist-message-channel-listener-injection-token";
|
||||
import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable";
|
||||
|
||||
const userStoreInjectable = getInjectable({
|
||||
id: "user-store",
|
||||
|
||||
instantiate: (di) => {
|
||||
UserStore.resetInstance();
|
||||
|
||||
return UserStore.createInstance({
|
||||
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
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;
|
||||
|
||||
@ -3,44 +3,37 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { action, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
|
||||
import { BaseStore } from "../base-store";
|
||||
import migrations from "../../migrations/user-store";
|
||||
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, toJS, object } from "../../renderer/utils";
|
||||
import { DESCRIPTORS } from "./preferences-helpers";
|
||||
import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
|
||||
import logger from "../../main/logger";
|
||||
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";
|
||||
|
||||
export interface UserStoreModel {
|
||||
lastSeenAppVersion: string;
|
||||
preferences: UserPreferencesModel;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
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) */ {
|
||||
readonly displayName = "UserStore";
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
super({
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
super(dependencies, {
|
||||
configName: "lens-user-store",
|
||||
migrations,
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@observable lastSeenAppVersion = "0.0.0";
|
||||
|
||||
/**
|
||||
* @deprecated No longer used
|
||||
*/
|
||||
@ -51,58 +44,45 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
*/
|
||||
@observable newContexts = observable.set<string>();
|
||||
|
||||
@observable allowErrorReporting!: StoreType<typeof DESCRIPTORS["allowErrorReporting"]>;
|
||||
@observable allowUntrustedCAs!: StoreType<typeof DESCRIPTORS["allowUntrustedCAs"]>;
|
||||
@observable colorTheme!: StoreType<typeof DESCRIPTORS["colorTheme"]>;
|
||||
@observable terminalTheme!: StoreType<typeof DESCRIPTORS["terminalTheme"]>;
|
||||
@observable localeTimezone!: StoreType<typeof DESCRIPTORS["localeTimezone"]>;
|
||||
@observable downloadMirror!: StoreType<typeof DESCRIPTORS["downloadMirror"]>;
|
||||
@observable httpsProxy!: StoreType<typeof DESCRIPTORS["httpsProxy"]>;
|
||||
@observable shell!: StoreType<typeof DESCRIPTORS["shell"]>;
|
||||
@observable downloadBinariesPath!: StoreType<typeof DESCRIPTORS["downloadBinariesPath"]>;
|
||||
@observable kubectlBinariesPath!: StoreType<typeof DESCRIPTORS["kubectlBinariesPath"]>;
|
||||
@observable terminalCopyOnSelect!: StoreType<typeof DESCRIPTORS["terminalCopyOnSelect"]>;
|
||||
@observable terminalConfig!: StoreType<typeof DESCRIPTORS["terminalConfig"]>;
|
||||
@observable extensionRegistryUrl!: StoreType<typeof DESCRIPTORS["extensionRegistryUrl"]>;
|
||||
@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<typeof DESCRIPTORS["downloadKubectlBinaries"]>;
|
||||
@observable downloadKubectlBinaries!: StoreType<PreferenceDescriptors["downloadKubectlBinaries"]>;
|
||||
|
||||
/**
|
||||
* Whether the application should open itself at login.
|
||||
*/
|
||||
@observable openAtLogin!: StoreType<typeof DESCRIPTORS["openAtLogin"]>;
|
||||
@observable openAtLogin!: StoreType<PreferenceDescriptors["openAtLogin"]>;
|
||||
|
||||
/**
|
||||
* The column IDs under each configurable table ID that have been configured
|
||||
* to not be shown
|
||||
*/
|
||||
@observable hiddenTableColumns!: StoreType<typeof DESCRIPTORS["hiddenTableColumns"]>;
|
||||
@observable hiddenTableColumns!: StoreType<PreferenceDescriptors["hiddenTableColumns"]>;
|
||||
|
||||
/**
|
||||
* Monaco editor configs
|
||||
*/
|
||||
@observable editorConfiguration!: StoreType<typeof DESCRIPTORS["editorConfiguration"]>;
|
||||
@observable editorConfiguration!: StoreType<PreferenceDescriptors["editorConfiguration"]>;
|
||||
|
||||
/**
|
||||
* The set of file/folder paths to be synced
|
||||
*/
|
||||
@observable syncKubeconfigEntries!: StoreType<typeof DESCRIPTORS["syncKubeconfigEntries"]>;
|
||||
|
||||
startMainReactions() {
|
||||
// open at system start-up
|
||||
reaction(() => this.openAtLogin, openAtLogin => {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin,
|
||||
openAsHidden: true,
|
||||
args: ["--hidden"],
|
||||
});
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
}
|
||||
@observable syncKubeconfigEntries!: StoreType<PreferenceDescriptors["syncKubeconfigEntries"]>;
|
||||
|
||||
/**
|
||||
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
|
||||
@ -133,18 +113,14 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
|
||||
@action
|
||||
resetTheme() {
|
||||
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined);
|
||||
this.colorTheme = this.dependencies.preferenceDescriptors.colorTheme.fromStore(undefined);
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore({ lastSeenAppVersion, preferences }: Partial<UserStoreModel> = {}) {
|
||||
logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences });
|
||||
protected fromStore({ preferences }: Partial<UserStoreModel> = {}) {
|
||||
this.dependencies.logger.debug("UserStore.fromStore()", { preferences });
|
||||
|
||||
if (lastSeenAppVersion) {
|
||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||
}
|
||||
|
||||
for (const [key, { fromStore }] of object.entries(DESCRIPTORS)) {
|
||||
for (const [key, { fromStore }] of object.entries(this.dependencies.preferenceDescriptors)) {
|
||||
const curVal = this[key];
|
||||
const newVal = fromStore((preferences)?.[key] as never) as never;
|
||||
|
||||
@ -165,16 +141,13 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
|
||||
toJSON(): UserStoreModel {
|
||||
const preferences = object.fromEntries(
|
||||
object.entries(DESCRIPTORS)
|
||||
object.entries(this.dependencies.preferenceDescriptors)
|
||||
.map(([key, { toStore }]) => [key, toStore(this[key] as never)]),
|
||||
) as UserPreferencesModel;
|
||||
|
||||
return toJS({
|
||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||
|
||||
preferences: {
|
||||
...preferences,
|
||||
|
||||
updateChannel: this.dependencies.selectedUpdateChannel.value.get().id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -48,11 +48,21 @@ export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
|
||||
return getOrInsert(map, key, new Set<SK>());
|
||||
}
|
||||
|
||||
/**
|
||||
* A currying version of {@link getOrInsertSet}
|
||||
*/
|
||||
export function getOrInsertSetFor<K, SK>(map: Map<K, Set<SK>>): (key: K) => Set<SK> {
|
||||
return (key) => getOrInsertSet(map, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but with delayed creation of the item. Which is useful
|
||||
* if it is very expensive to create the initial value.
|
||||
*/
|
||||
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V {
|
||||
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V;
|
||||
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V;
|
||||
|
||||
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, builder());
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import randomBytesInjectable from "./random-bytes.injectable";
|
||||
|
||||
export default getGlobalOverride(randomBytesInjectable, () => async (size) => {
|
||||
const res = Buffer.alloc(size);
|
||||
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
res[i] = i;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
17
src/common/utils/random-bytes.injectable.ts
Normal file
17
src/common/utils/random-bytes.injectable.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 { randomBytes } from "crypto";
|
||||
import { promisify } from "util";
|
||||
|
||||
export type RandomBytes = (size: number) => Promise<Buffer>;
|
||||
|
||||
const randomBytesInjectable = getInjectable({
|
||||
id: "random-bytes",
|
||||
instantiate: (): RandomBytes => promisify(randomBytes),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default randomBytesInjectable;
|
||||
@ -3,10 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
interface StaticThis<T, R extends any[]> { new(...args: R): T }
|
||||
export interface StaticThis<T, R extends any[]> { new(...args: R): T }
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state
|
||||
*/
|
||||
export class Singleton {
|
||||
private static instances = new WeakMap<object, Singleton>();
|
||||
private static readonly instances = new WeakMap<object, Singleton>();
|
||||
private static creating = "";
|
||||
|
||||
constructor() {
|
||||
|
||||
@ -123,6 +123,10 @@ export function isDefined<T>(val: T | undefined | null): val is T {
|
||||
return val != null;
|
||||
}
|
||||
|
||||
export function isFunction(val: unknown): val is (...args: unknown[]) => unknown {
|
||||
return typeof val === "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value in the second position is non-nullable
|
||||
*/
|
||||
@ -146,6 +150,15 @@ export function hasDefiniteField<Field extends keyof T, T>(field: Field): (val:
|
||||
return (val): val is T & { [f in Field]-?: NonNullable<T[Field]> } => val[field] != null;
|
||||
}
|
||||
|
||||
export function isPromiseLike(res: unknown): res is (Promise<unknown> | { then: (fn: (val: unknown) => any) => Promise<unknown> }) {
|
||||
if (res instanceof Promise) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isObject(res)
|
||||
&& hasTypedProperty(res, "then", isFunction);
|
||||
}
|
||||
|
||||
export function isPromiseSettledRejected<T>(result: PromiseSettledResult<T>): result is PromiseRejectedResult {
|
||||
return result.status === "rejected";
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
||||
import type { ThemeId } from "../renderer/themes/store";
|
||||
import type { ThemeId } from "../renderer/themes/lens-theme";
|
||||
|
||||
/**
|
||||
* @deprecated Switch to using isMacInjectable
|
||||
@ -55,12 +55,4 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
|
||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string;
|
||||
export const supportUrl = "https://docs.k8slens.dev/support/" as string;
|
||||
|
||||
export const lensWebsiteWeblinkId = "lens-website-link";
|
||||
export const lensDocumentationWeblinkId = "lens-documentation-link";
|
||||
export const lensSlackWeblinkId = "lens-slack-link";
|
||||
export const lensTwitterWeblinkId = "lens-twitter-link";
|
||||
export const lensBlogWeblinkId = "lens-blog-link";
|
||||
export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link";
|
||||
|
||||
export const docsUrl = "https://docs.k8slens.dev" as string;
|
||||
|
||||
@ -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 { WeblinkStore } from "./weblink-store";
|
||||
|
||||
const weblinkStoreInjectable = getInjectable({
|
||||
id: "weblink-store",
|
||||
|
||||
instantiate: () => {
|
||||
WeblinkStore.resetInstance();
|
||||
|
||||
return WeblinkStore.createInstance();
|
||||
},
|
||||
});
|
||||
|
||||
export default weblinkStoreInjectable;
|
||||
11
src/common/weblinks-store/migration-token.ts
Normal file
11
src/common/weblinks-store/migration-token.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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",
|
||||
});
|
||||
35
src/common/weblinks-store/weblink-store.injectable.ts
Normal file
35
src/common/weblinks-store/weblink-store.injectable.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 "../utils/channel/enlist-message-channel-listener-injection-token";
|
||||
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;
|
||||
@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { action, comparer, observable, makeObservable } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/weblinks-store";
|
||||
import type { BaseStoreDependencies } from "../base-store/base-store";
|
||||
import { BaseStore } from "../base-store/base-store";
|
||||
import * as uuid from "uuid";
|
||||
import { toJS } from "./utils";
|
||||
import { toJS } from "../utils";
|
||||
|
||||
export interface WeblinkData {
|
||||
id: string;
|
||||
@ -27,17 +27,15 @@ export interface WeblinkStoreModel {
|
||||
}
|
||||
|
||||
export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
|
||||
readonly displayName = "WeblinkStore";
|
||||
@observable weblinks: WeblinkData[] = [];
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
constructor(deps: BaseStoreDependencies) {
|
||||
super(deps, {
|
||||
configName: "lens-weblink-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
syncOptions: {
|
||||
equals: comparer.structural,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
@ -14,6 +14,8 @@ import { delay } from "../../renderer/utils";
|
||||
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
|
||||
import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable";
|
||||
import type { IpcRenderer } from "electron";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import currentlyInClusterFrameInjectable from "../../renderer/routes/currently-in-cluster-frame.injectable";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@ -28,6 +30,9 @@ describe("ExtensionLoader", () => {
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
di.override(currentlyInClusterFrameInjectable, () => false);
|
||||
|
||||
di.override(ipcRendererInjectable, () => ({
|
||||
invoke: jest.fn(async (channel: string) => {
|
||||
if (channel === "extension-loader:main:state") {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import userStoreInjectable from "../../common/user-store/user-store.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
export interface UserPreferenceExtensionItems {
|
||||
/**
|
||||
* Get the configured kubectl binaries path.
|
||||
@ -11,6 +12,8 @@ export interface UserPreferenceExtensionItems {
|
||||
getKubectlPath: () => string | undefined;
|
||||
}
|
||||
|
||||
const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable);
|
||||
|
||||
export const Preferences: UserPreferenceExtensionItems = {
|
||||
getKubectlPath: () => UserStore.getInstance().kubectlBinariesPath,
|
||||
getKubectlPath: () => userStore.kubectlBinariesPath,
|
||||
};
|
||||
|
||||
@ -25,7 +25,7 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab
|
||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||
import getRelativePathInjectable from "../../common/path/get-relative-path.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove-path.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||
import applicationInformationInjectable from "../../common/vars/application-information.injectable";
|
||||
import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable";
|
||||
|
||||
@ -15,10 +15,13 @@ import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import watchInjectable from "../../common/fs/watch/watch.injectable";
|
||||
import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove-path.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
|
||||
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
|
||||
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
@ -34,6 +37,9 @@ describe("ExtensionDiscovery", () => {
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
di.override(installExtensionInjectable, () => () => Promise.resolve());
|
||||
di.override(extensionApiVersionInjectable, () => "5.0.0");
|
||||
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
|
||||
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
||||
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||
|
||||
joinPaths = di.inject(joinPathsInjectable);
|
||||
homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
|
||||
|
||||
@ -28,7 +28,7 @@ import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable";
|
||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
|
||||
import type { RemovePath } from "../../common/fs/remove-path.injectable";
|
||||
import type { RemovePath } from "../../common/fs/remove.injectable";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import type { ApplicationInformation } from "../../common/vars/application-information.injectable";
|
||||
|
||||
|
||||
@ -9,12 +9,9 @@ import extensionPackageRootDirectoryInjectable from "./extension-package-root-di
|
||||
const extensionInstallerInjectable = getInjectable({
|
||||
id: "extension-installer",
|
||||
|
||||
instantiate: (di) =>
|
||||
new ExtensionInstaller({
|
||||
extensionPackageRootDirectory: di.inject(
|
||||
extensionPackageRootDirectoryInjectable,
|
||||
),
|
||||
}),
|
||||
instantiate: (di) => new ExtensionInstaller({
|
||||
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionInstallerInjectable;
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
const extensionPackageRootDirectoryInjectable = getInjectable({
|
||||
id: "extension-package-root-directory",
|
||||
|
||||
@ -5,19 +5,38 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { FileSystemProvisionerStore } from "./file-system-provisioner-store";
|
||||
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
|
||||
import ensureDirectoryInjectable from "../../../common/fs/ensure-dir.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import storeMigrationVersionInjectable from "../../../common/vars/store-migration-version.injectable";
|
||||
import { baseStoreIpcChannelPrefixesInjectionToken } from "../../../common/base-store/channel-prefix";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../common/base-store/disable-sync";
|
||||
import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
const fileSystemProvisionerStoreInjectable = getInjectable({
|
||||
id: "file-system-provisioner-store",
|
||||
|
||||
instantiate: (di) => {
|
||||
FileSystemProvisionerStore.resetInstance();
|
||||
|
||||
return FileSystemProvisionerStore.createInstance({
|
||||
directoryForExtensionData: di.inject(directoryForExtensionDataInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
instantiate: (di) => new FileSystemProvisionerStore({
|
||||
directoryForExtensionData: di.inject(directoryForExtensionDataInjectable),
|
||||
ensureDirectory: di.inject(ensureDirectoryInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
randomBytes: di.inject(randomBytesInjectable),
|
||||
directoryForUserData: di.inject(directoryForUserDataInjectable),
|
||||
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
|
||||
migrations: {},
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
|
||||
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
|
||||
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
|
||||
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
|
||||
}),
|
||||
});
|
||||
|
||||
export default fileSystemProvisionerStoreInjectable;
|
||||
|
||||
@ -3,35 +3,37 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { randomBytes } from "crypto";
|
||||
import { SHA256 } from "crypto-js";
|
||||
import fse from "fs-extra";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import path from "path";
|
||||
import { BaseStore } from "../../../common/base-store";
|
||||
import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
|
||||
import { BaseStore } from "../../../common/base-store/base-store";
|
||||
import type { LensExtensionId } from "../../lens-extension";
|
||||
import { getOrInsertWith, toJS } from "../../../common/utils";
|
||||
import { getOrInsertWithAsync, toJS } from "../../../common/utils";
|
||||
import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable";
|
||||
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
|
||||
import type { RandomBytes } from "../../../common/utils/random-bytes.injectable";
|
||||
|
||||
interface FSProvisionModel {
|
||||
extensions: Record<string, string>; // extension names to paths
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
directoryForExtensionData: string;
|
||||
interface Dependencies extends BaseStoreDependencies {
|
||||
readonly directoryForExtensionData: string;
|
||||
ensureDirectory: EnsureDirectory;
|
||||
joinPaths: JoinPaths;
|
||||
randomBytes: RandomBytes;
|
||||
}
|
||||
|
||||
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
||||
readonly displayName = "FilesystemProvisionerStore";
|
||||
registeredExtensions = observable.map<LensExtensionId, string>();
|
||||
readonly registeredExtensions = observable.map<LensExtensionId, string>();
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super({
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
super(dependencies, {
|
||||
configName: "lens-filesystem-provisioner-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,14 +43,14 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
||||
* @returns path to the folder that the extension can safely write files to.
|
||||
*/
|
||||
async requestDirectory(extensionName: string): Promise<string> {
|
||||
const dirPath = getOrInsertWith(this.registeredExtensions, extensionName, () => {
|
||||
const salt = randomBytes(32).toString("hex");
|
||||
const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => {
|
||||
const salt = (await this.dependencies.randomBytes(32)).toString("hex");
|
||||
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
||||
|
||||
return path.resolve(this.dependencies.directoryForExtensionData, hashedName);
|
||||
return this.dependencies.joinPaths(this.dependencies.directoryForExtensionData, hashedName);
|
||||
});
|
||||
|
||||
await fse.ensureDir(dirPath);
|
||||
await this.dependencies.ensureDirectory(dirPath);
|
||||
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
@ -3,13 +3,76 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { BaseStore } from "../common/base-store";
|
||||
import type { BaseStoreParams } from "../common/base-store/base-store";
|
||||
import { BaseStore } from "../common/base-store/base-store";
|
||||
import * as path from "path";
|
||||
import type { LensExtension } from "./lens-extension";
|
||||
import assert from "assert";
|
||||
import type { StaticThis } from "../common/utils";
|
||||
import { getOrInsertWith } from "../common/utils";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import directoryForUserDataInjectable from "../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import loggerInjectable from "../common/logger.injectable";
|
||||
import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable";
|
||||
import type { Migrations } from "conf/dist/source/types";
|
||||
import { baseStoreIpcChannelPrefixesInjectionToken } from "../common/base-store/channel-prefix";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../common/base-store/disable-sync";
|
||||
import { persistStateToConfigInjectionToken } from "../common/base-store/save-to-file";
|
||||
import getBasenameOfPathInjectable from "../common/path/get-basename.injectable";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
export interface ExtensionStoreParams<T extends object> extends BaseStoreParams<T> {
|
||||
migrations?: Migrations<T>;
|
||||
}
|
||||
|
||||
export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
|
||||
readonly displayName = "ExtensionStore<T>";
|
||||
private static readonly instances = new WeakMap<object, ExtensionStore<object>>();
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state. Just call `new Store(...)`
|
||||
*/
|
||||
static createInstance<T extends ExtensionStore<object>, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
||||
return getOrInsertWith(ExtensionStore.instances, this, () => new this(...args)) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state. Just call `new Store(...)`
|
||||
*/
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict?: true): T;
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict: false): T | undefined;
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
|
||||
if (!ExtensionStore.instances.has(this) && strict) {
|
||||
throw new TypeError(`instance of ${this.name} is not created`);
|
||||
}
|
||||
|
||||
return ExtensionStore.instances.get(this) as (T | undefined);
|
||||
}
|
||||
|
||||
constructor({ migrations, ...params }: ExtensionStoreParams<T>) {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
super({
|
||||
directoryForUserData: di.inject(directoryForUserDataInjectable),
|
||||
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
|
||||
migrations: migrations as Migrations<Record<string, unknown>>,
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
|
||||
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
|
||||
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
|
||||
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
|
||||
}, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state. Just call `new Store(...)`
|
||||
*/
|
||||
static resetInstance() {
|
||||
ExtensionStore.instances.delete(this);
|
||||
}
|
||||
|
||||
protected extension?: LensExtension;
|
||||
|
||||
loadExtension(extension: LensExtension) {
|
||||
|
||||
@ -3,18 +3,31 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { baseStoreIpcChannelPrefixesInjectionToken } from "../../common/base-store/channel-prefix";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../common/base-store/disable-sync";
|
||||
import { persistStateToConfigInjectionToken } from "../../common/base-store/save-to-file";
|
||||
import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
|
||||
const extensionsStoreInjectable = getInjectable({
|
||||
id: "extensions-store",
|
||||
|
||||
instantiate: () => {
|
||||
ExtensionsStore.resetInstance();
|
||||
|
||||
return ExtensionsStore.createInstance();
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
instantiate: (di) => new ExtensionsStore({
|
||||
directoryForUserData: di.inject(directoryForUserDataInjectable),
|
||||
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
|
||||
migrations: {},
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
|
||||
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
|
||||
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
|
||||
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionsStoreInjectable;
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
import type { LensExtensionId } from "../lens-extension";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { toJS } from "../../common/utils";
|
||||
import { BaseStore } from "../../common/base-store";
|
||||
import type { BaseStoreDependencies } from "../../common/base-store/base-store";
|
||||
import { BaseStore } from "../../common/base-store/base-store";
|
||||
|
||||
export interface LensExtensionsStoreModel {
|
||||
extensions: Record<LensExtensionId, LensExtensionState>;
|
||||
@ -23,9 +24,8 @@ export interface IsEnabledExtensionDescriptor {
|
||||
}
|
||||
|
||||
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
readonly displayName = "ExtensionsStore";
|
||||
constructor() {
|
||||
super({
|
||||
constructor(deps: BaseStoreDependencies) {
|
||||
super(deps, {
|
||||
configName: "lens-extensions",
|
||||
});
|
||||
makeObservable(this);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import activeThemeInjectable from "../../renderer/themes/active.injectable";
|
||||
import type { LensTheme } from "../../renderer/themes/store";
|
||||
import type { LensTheme } from "../../renderer/themes/lens-theme";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable);
|
||||
|
||||
@ -164,7 +164,23 @@ exports[`extension special characters in page registrations renders 1`] = `
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -231,7 +247,7 @@ exports[`extension special characters in page registrations renders 1`] = `
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -352,7 +368,23 @@ exports[`extension special characters in page registrations when navigating to r
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -419,7 +451,7 @@ exports[`extension special characters in page registrations when navigating to r
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
|
||||
@ -164,7 +164,23 @@ exports[`navigate to extension page renders 1`] = `
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -231,7 +247,7 @@ exports[`navigate to extension page renders 1`] = `
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -352,7 +368,23 @@ exports[`navigate to extension page when extension navigates to child route rend
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -419,7 +451,7 @@ exports[`navigate to extension page when extension navigates to child route rend
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -556,7 +588,23 @@ exports[`navigate to extension page when extension navigates to route with param
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -623,7 +671,7 @@ exports[`navigate to extension page when extension navigates to route with param
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -760,7 +808,23 @@ exports[`navigate to extension page when extension navigates to route without pa
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -827,7 +891,7 @@ exports[`navigate to extension page when extension navigates to route without pa
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -964,7 +1028,23 @@ exports[`navigate to extension page when extension navigates to route without pa
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -1031,7 +1111,7 @@ exports[`navigate to extension page when extension navigates to route without pa
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
|
||||
@ -88,7 +88,23 @@ exports[`navigating between routes given route with optional path parameters whe
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -155,7 +171,7 @@ exports[`navigating between routes given route with optional path parameters whe
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -276,7 +292,23 @@ exports[`navigating between routes given route without path parameters when navi
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -343,7 +375,7 @@ exports[`navigating between routes given route without path parameters when navi
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
|
||||
@ -164,7 +164,23 @@ exports[`add-cluster - navigation using application menu renders 1`] = `
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -231,7 +247,7 @@ exports[`add-cluster - navigation using application menu renders 1`] = `
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
@ -436,7 +452,23 @@ exports[`add-cluster - navigation using application menu when navigating to add
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="0"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style="z-index: 12; position: absolute;"
|
||||
>
|
||||
<div
|
||||
class="HotbarIcon contextMenuAvailable"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded disabled avatar"
|
||||
id="hotbarIcon-hotbar-icon-catalog-entity"
|
||||
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
|
||||
>
|
||||
Ca
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HotbarCell isDraggingOwner animateDown"
|
||||
index="1"
|
||||
@ -503,7 +535,7 @@ exports[`add-cluster - navigation using application menu when navigating to add
|
||||
class="badge Badge small clickable"
|
||||
id="hotbarIndex"
|
||||
>
|
||||
0
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user