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

Merge branch 'master' into consitent-tooltips

This commit is contained in:
Alex Andreev 2021-06-28 10:40:05 +03:00
commit c8684bd737
53 changed files with 430 additions and 527 deletions

View File

@ -70,7 +70,7 @@ integration-win: binaries/client build-extension-types build-extensions
.PHONY: build .PHONY: build
build: node_modules binaries/client build: node_modules binaries/client
yarn run npm:fix-build-version yarn run npm:fix-build-version
$(MAKE) build-extensions $(MAKE) build-extensions -B
yarn run compile yarn run compile
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"
yarn run electron-builder --publish onTag --x64 --ia32 yarn run electron-builder --publish onTag --x64 --ia32

View File

@ -51,4 +51,4 @@ To provide feedback on the documentation or issues with the Lens Extension API,
## Downloading Lens ## Downloading Lens
[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux. [Download Lens](https://k8slens.dev/) for macOS, Windows, or Linux.

View File

@ -78,7 +78,7 @@ npm run dev
You must restart Lens for the extension to load. You must restart Lens for the extension to load.
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds. After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/latest/clusters/adding-clusters) for details on how to add a cluster in Lens IDE. With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE.
You will see the "Hello World" page in the left-side cluster menu. You will see the "Hello World" page in the left-side cluster menu.
## Develop the Extension ## Develop the Extension

View File

@ -43,10 +43,6 @@ describe("Lens cluster pages", () => {
const ready = minikubeReady(TEST_NAMESPACE); const ready = minikubeReady(TEST_NAMESPACE);
let clusterAdded = false; let clusterAdded = false;
afterEach(() => {
clusterAdded = false;
});
utils.describeIf(ready)("test common pages", () => { utils.describeIf(ready)("test common pages", () => {
const addCluster = async () => { const addCluster = async () => {
await waitForMinikubeDashboard(app); await waitForMinikubeDashboard(app);
@ -54,29 +50,29 @@ describe("Lens cluster pages", () => {
await app.client.waitUntilTextExists("div.TableCell", "Ready"); await app.client.waitUntilTextExists("div.TableCell", "Ready");
}; };
describe("cluster add", () => {
utils.beforeAllWrapped(async () => {
app = await utils.appStart();
});
utils.afterAllWrapped(async () => {
if (app?.isRunning()) {
return utils.tearDown(app);
}
});
it("allows to add a cluster", async () => {
await addCluster();
clusterAdded = true;
});
});
const appStartAddCluster = async () => { const appStartAddCluster = async () => {
app = await utils.appStart(); app = await utils.appStart();
await addCluster(); await addCluster();
clusterAdded = true; clusterAdded = true;
}; };
const tearDown = async () => {
await utils.tearDown(app);
clusterAdded = false;
};
describe("cluster add", () => {
utils.beforeAllWrapped(async () => {
app = await utils.appStart();
});
utils.afterAllWrapped(tearDown);
it("allows to add a cluster", async () => {
await addCluster();
});
});
function getSidebarSelectors(itemId: string) { function getSidebarSelectors(itemId: string) {
const root = `.SidebarItem[data-test-id="${itemId}"]`; const root = `.SidebarItem[data-test-id="${itemId}"]`;
@ -88,12 +84,7 @@ describe("Lens cluster pages", () => {
describe("cluster pages", () => { describe("cluster pages", () => {
utils.beforeAllWrapped(appStartAddCluster); utils.beforeAllWrapped(appStartAddCluster);
utils.afterAllWrapped(tearDown);
utils.afterAllWrapped(async () => {
if (app?.isRunning()) {
return utils.tearDown(app);
}
});
const tests: { const tests: {
drawer?: string drawer?: string
@ -378,12 +369,7 @@ describe("Lens cluster pages", () => {
describe("viewing pod logs", () => { describe("viewing pod logs", () => {
utils.beforeEachWrapped(appStartAddCluster); utils.beforeEachWrapped(appStartAddCluster);
utils.afterEachWrapped(tearDown);
utils.afterEachWrapped(async () => {
if (app?.isRunning()) {
return utils.tearDown(app);
}
});
it(`shows a log for a pod`, async () => { it(`shows a log for a pod`, async () => {
expect(clusterAdded).toBe(true); expect(clusterAdded).toBe(true);
@ -433,12 +419,7 @@ describe("Lens cluster pages", () => {
describe("cluster operations", () => { describe("cluster operations", () => {
utils.beforeEachWrapped(appStartAddCluster); utils.beforeEachWrapped(appStartAddCluster);
utils.afterEachWrapped(tearDown);
utils.afterEachWrapped(async () => {
if (app?.isRunning()) {
return utils.tearDown(app);
}
});
it("shows default namespace", async () => { it("shows default namespace", async () => {
expect(clusterAdded).toBe(true); expect(clusterAdded).toBe(true);

View File

@ -37,27 +37,27 @@ interface DoneCallback {
* This is necessary because Jest doesn't do this correctly. * This is necessary because Jest doesn't do this correctly.
* @param fn The function to call * @param fn The function to call
*/ */
export function wrapJestLifecycle(fn: () => Promise<void>): (done: DoneCallback) => void { export function wrapJestLifecycle(fn: () => Promise<void> | void): (done: DoneCallback) => void {
return function (done: DoneCallback) { return function (done: DoneCallback) {
fn() (async () => fn())()
.then(() => done()) .then(() => done())
.catch(error => done.fail(error)); .catch(error => done.fail(error));
}; };
} }
export function beforeAllWrapped(fn: () => Promise<void>): void { export function beforeAllWrapped(fn: () => Promise<void> | void): void {
beforeAll(wrapJestLifecycle(fn)); beforeAll(wrapJestLifecycle(fn));
} }
export function beforeEachWrapped(fn: () => Promise<void>): void { export function beforeEachWrapped(fn: () => Promise<void> | void): void {
beforeEach(wrapJestLifecycle(fn)); beforeEach(wrapJestLifecycle(fn));
} }
export function afterAllWrapped(fn: () => Promise<void>): void { export function afterAllWrapped(fn: () => Promise<void> | void): void {
afterAll(wrapJestLifecycle(fn)); afterAll(wrapJestLifecycle(fn));
} }
export function afterEachWrapped(fn: () => Promise<void>): void { export function afterEachWrapped(fn: () => Promise<void> | void): void {
afterEach(wrapJestLifecycle(fn)); afterEach(wrapJestLifecycle(fn));
} }
@ -105,7 +105,11 @@ export async function showCatalog(app: Application) {
type AsyncPidGetter = () => Promise<number>; type AsyncPidGetter = () => Promise<number>;
export async function tearDown(app: Application) { export async function tearDown(app?: Application) {
if (!app?.isRunning()) {
return;
}
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
await app.stop(); await app.stop();

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.0.0-beta.10", "version": "5.0.0-beta.12",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",

View File

@ -95,7 +95,7 @@ describe("empty config", () => {
mockFs(mockOpts); mockFs(mockOpts);
await ClusterStore.createInstance().load(); ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -203,7 +203,7 @@ describe("config with existing clusters", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -285,7 +285,7 @@ users:
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -344,7 +344,7 @@ describe("pre 2.0 config with an existing cluster", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -414,7 +414,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -456,7 +456,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -495,7 +495,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -531,7 +531,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance().load(); return ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {

View File

@ -23,7 +23,7 @@ import mockFs from "mock-fs";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store"; import { HotbarStore } from "../hotbar-store";
jest.mock("../../renderer/api/catalog-entity-registry", () => ({ jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: { catalogEntityRegistry: {
items: [ items: [
{ {
@ -39,7 +39,14 @@ jest.mock("../../renderer/api/catalog-entity-registry", () => ({
name: "my_shiny_cluster", name: "my_shiny_cluster",
source: "remote" source: "remote"
} }
} },
{
metadata: {
uid: "catalog-entity",
name: "Catalog",
source: "app"
},
},
] ]
} }
})); }));
@ -120,29 +127,31 @@ jest.mock("electron", () => {
describe("HotbarStore", () => { describe("HotbarStore", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance(); mockFs({
"tmp": {
"lens-hotbar-store.json": JSON.stringify({})
}
});
ClusterStore.createInstance(); ClusterStore.createInstance();
HotbarStore.createInstance();
HotbarStore.resetInstance();
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
}); });
afterEach(() => { afterEach(() => {
ClusterStore.resetInstance();
HotbarStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });
describe("load", () => { describe("load", () => {
it("loads one hotbar by default", () => { it("loads one hotbar by default", () => {
HotbarStore.createInstance().load();
expect(HotbarStore.getInstance().hotbars.length).toEqual(1); expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
}); });
}); });
describe("add", () => { describe("add", () => {
it("adds a hotbar", () => { it("adds a hotbar", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.add({ name: "hottest" }); hotbarStore.add({ name: "hottest" });
expect(hotbarStore.hotbars.length).toEqual(2); expect(hotbarStore.hotbars.length).toEqual(2);
}); });
@ -150,23 +159,20 @@ describe("HotbarStore", () => {
describe("hotbar items", () => { describe("hotbar items", () => {
it("initially creates 12 empty cells", () => { it("initially creates 12 empty cells", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
expect(hotbarStore.getActive().items.length).toEqual(12); expect(hotbarStore.getActive().items.length).toEqual(12);
}); });
it("initially adds catalog entity as first item", () => { it("initially adds catalog entity as first item", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
}); });
it("adds items", () => { it("adds items", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean); const items = hotbarStore.getActive().items.filter(Boolean);
@ -174,9 +180,8 @@ describe("HotbarStore", () => {
}); });
it("removes items", () => { it("removes items", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("test"); hotbarStore.removeFromHotbar("test");
hotbarStore.removeFromHotbar("catalog-entity"); hotbarStore.removeFromHotbar("catalog-entity");
@ -186,9 +191,8 @@ describe("HotbarStore", () => {
}); });
it("does nothing if removing with invalid uid", () => { it("does nothing if removing with invalid uid", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("invalid uid"); hotbarStore.removeFromHotbar("invalid uid");
const items = hotbarStore.getActive().items.filter(Boolean); const items = hotbarStore.getActive().items.filter(Boolean);
@ -197,9 +201,8 @@ describe("HotbarStore", () => {
}); });
it("moves item to empty cell", () => { it("moves item to empty cell", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster); hotbarStore.addToHotbar(awsCluster);
@ -213,9 +216,8 @@ describe("HotbarStore", () => {
}); });
it("moves items down", () => { it("moves items down", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster); hotbarStore.addToHotbar(awsCluster);
@ -229,9 +231,8 @@ describe("HotbarStore", () => {
}); });
it("moves items up", () => { it("moves items up", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster); hotbarStore.addToHotbar(awsCluster);
@ -245,9 +246,8 @@ describe("HotbarStore", () => {
}); });
it("does nothing when item moved to same cell", () => { it("does nothing when item moved to same cell", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1); hotbarStore.restackItems(1, 1);
@ -255,9 +255,8 @@ describe("HotbarStore", () => {
}); });
it("new items takes first empty cell", () => { it("new items takes first empty cell", () => {
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster); hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3); hotbarStore.restackItems(0, 3);
@ -268,13 +267,13 @@ describe("HotbarStore", () => {
it("throws if invalid arguments provided", () => { it("throws if invalid arguments provided", () => {
// Prevent writing to stderr during this render. // Prevent writing to stderr during this render.
const err = console.error; const { error, warn } = console;
console.error = jest.fn(); console.error = jest.fn();
console.warn = jest.fn();
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.getInstance();
hotbarStore.load();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
@ -283,7 +282,8 @@ describe("HotbarStore", () => {
expect(() => hotbarStore.restackItems(11, 112)).toThrow(); expect(() => hotbarStore.restackItems(11, 112)).toThrow();
// Restore writing to stderr. // Restore writing to stderr.
console.error = err; console.error = error;
console.warn = warn;
}); });
}); });
@ -354,7 +354,7 @@ describe("HotbarStore", () => {
mockFs(mockOpts); mockFs(mockOpts);
return HotbarStore.createInstance().load(); HotbarStore.createInstance();
}); });
afterEach(() => { afterEach(() => {

View File

@ -52,7 +52,7 @@ describe("user store tests", () => {
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); (UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
return UserStore.getInstance().load(); UserStore.getInstance();
}); });
afterEach(() => { afterEach(() => {
@ -81,10 +81,8 @@ describe("user store tests", () => {
it("correctly resets theme to default value", async () => { it("correctly resets theme to default value", async () => {
const us = UserStore.getInstance(); const us = UserStore.getInstance();
us.isLoaded = true;
us.colorTheme = "some other theme"; us.colorTheme = "some other theme";
await us.resetTheme(); us.resetTheme();
expect(us.colorTheme).toBe(UserStore.defaultTheme); expect(us.colorTheme).toBe(UserStore.defaultTheme);
}); });
@ -111,7 +109,7 @@ describe("user store tests", () => {
} }
}); });
return UserStore.createInstance().load(); UserStore.createInstance();
}); });
afterEach(() => { afterEach(() => {

View File

@ -23,41 +23,42 @@ import path from "path";
import Config from "conf"; import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types"; import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, ipcRenderer, remote } from "electron"; import { app, ipcMain, ipcRenderer, remote } from "electron";
import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx"; import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
import { getAppVersion, Singleton, toJS, Disposer } from "./utils"; import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
export interface BaseStoreParams<T = any> extends ConfOptions<T> { export interface BaseStoreParams<T> extends ConfOptions<T> {
autoLoad?: boolean;
syncEnabled?: boolean;
syncOptions?: IReactionOptions; syncOptions?: IReactionOptions;
} }
/** /**
* Note: T should only contain base JSON serializable types. * Note: T should only contain base JSON serializable types.
*/ */
export abstract class BaseStore<T = any> extends Singleton { export abstract class BaseStore<T> extends Singleton {
protected storeConfig?: Config<T>; protected storeConfig?: Config<T>;
protected syncDisposers: Disposer[] = []; protected syncDisposers: Disposer[] = [];
@observable isLoaded = false; protected constructor(protected params: BaseStoreParams<T>) {
get whenLoaded() {
return when(() => this.isLoaded);
}
protected constructor(protected params: BaseStoreParams) {
super(); super();
makeObservable(this); makeObservable(this);
}
this.params = { /**
autoLoad: false, * This must be called after the last child's constructor is finished (or just before it finishes)
syncEnabled: true, */
...params, load() {
}; this.storeConfig = new Config({
this.init(); ...this.params,
projectName: "lens",
projectVersion: getAppVersion(),
cwd: this.cwd(),
});
logger.info(`[STORE]: LOADED from ${this.path}`);
this.fromStore(this.storeConfig.store);
this.enableSync();
} }
get name() { get name() {
@ -76,31 +77,6 @@ export abstract class BaseStore<T = any> extends Singleton {
return this.storeConfig?.path || ""; return this.storeConfig?.path || "";
} }
protected async init() {
if (this.params.autoLoad) {
await this.load();
}
if (this.params.syncEnabled) {
await this.whenLoaded;
this.enableSync();
}
}
async load() {
const { autoLoad, syncEnabled, ...confOptions } = this.params;
this.storeConfig = new Config({
...confOptions,
projectName: "lens",
projectVersion: getAppVersion(),
cwd: this.cwd(),
});
logger.info(`[STORE]: LOADED from ${this.path}`);
this.fromStore(this.storeConfig.store);
this.isLoaded = true;
}
protected cwd() { protected cwd() {
return (app || remote.app).getPath("userData"); return (app || remote.app).getPath("userData");
} }
@ -159,10 +135,7 @@ export abstract class BaseStore<T = any> extends Singleton {
protected applyWithoutSync(callback: () => void) { protected applyWithoutSync(callback: () => void) {
this.disableSync(); this.disableSync();
runInAction(callback); runInAction(callback);
this.enableSync();
if (this.params.syncEnabled) {
this.enableSync();
}
} }
protected onSync(model: T) { protected onSync(model: T) {

View File

@ -110,6 +110,8 @@ export interface ClusterPrometheusPreferences {
}; };
} }
const initialStates = "cluster:states";
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state"; private static StateChannel = "cluster:state";
@ -121,8 +123,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId); return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
} }
@observable clusters = observable.map<ClusterId, Cluster>(); clusters = observable.map<ClusterId, Cluster>();
@observable removedClusters = observable.map<ClusterId, Cluster>(); removedClusters = observable.map<ClusterId, Cluster>();
protected disposer = disposer(); protected disposer = disposer();
@ -137,31 +139,27 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}); });
makeObservable(this); makeObservable(this);
this.load();
this.pushStateToViewsAutomatically(); this.pushStateToViewsAutomatically();
} }
async load() { async loadInitialOnRenderer() {
const initialStates = "cluster:states"; logger.info("[CLUSTER-STORE] requesting initial state sync");
await super.load(); for (const { id, state } of await requestMain(initialStates)) {
this.getById(id)?.setState(state);
if (ipcRenderer) {
logger.info("[CLUSTER-STORE] requesting initial state sync");
for (const { id, state } of await requestMain(initialStates)) {
this.getById(id)?.setState(state);
}
} else if (ipcMain) {
ipcMainHandle(initialStates, () => {
return this.clustersList.map(cluster => ({
id: cluster.id,
state: cluster.getState(),
}));
});
} }
} }
provideInitialFromMain() {
ipcMainHandle(initialStates, () => {
return this.clustersList.map(cluster => ({
id: cluster.id,
state: cluster.getState(),
}));
});
}
protected pushStateToViewsAutomatically() { protected pushStateToViewsAutomatically() {
if (ipcMain) { if (ipcMain) {
this.disposer.push( this.disposer.push(

View File

@ -26,7 +26,6 @@ import * as uuid from "uuid";
import isNull from "lodash/isNull"; import isNull from "lodash/isNull";
import { toJS } from "./utils"; import { toJS } from "./utils";
import { CatalogEntity } from "./catalog"; import { CatalogEntity } from "./catalog";
import { catalogEntity } from "../main/catalog-sources/general";
export interface HotbarItem { export interface HotbarItem {
entity: { entity: {
@ -39,16 +38,12 @@ export interface HotbarItem {
} }
} }
export interface Hotbar { export type Hotbar = Required<HotbarCreateOptions>;
id: string;
name: string;
items: HotbarItem[];
}
export interface HotbarCreateOptions { export interface HotbarCreateOptions {
id?: string; id?: string;
name: string; name: string;
items?: HotbarItem[]; items?: (HotbarItem | null)[];
} }
export interface HotbarStoreModel { export interface HotbarStoreModel {
@ -72,6 +67,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
migrations, migrations,
}); });
makeObservable(this); makeObservable(this);
this.load();
} }
get activeHotbarId() { get activeHotbarId() {
@ -92,30 +88,13 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return this.hotbarIndex(this.activeHotbarId); return this.hotbarIndex(this.activeHotbarId);
} }
get defaultHotbarInitialItems() { static getInitialItems() {
const { metadata: { uid, name, source } } = catalogEntity;
const initialItem = { entity: { uid, name, source }};
return [
initialItem,
...Array.from(Array(defaultHotbarCells - 1).fill(null))
];
}
get initialItems() {
return [...Array.from(Array(defaultHotbarCells).fill(null))]; return [...Array.from(Array(defaultHotbarCells).fill(null))];
} }
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) { @action
if (data.hotbars?.length === 0) { protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
this.hotbars = [{ this.hotbars = data.hotbars;
id: uuid.v4(),
name: "Default",
items: this.defaultHotbarInitialItems,
}];
} else {
this.hotbars = data.hotbars;
}
if (data.activeHotbarId) { if (data.activeHotbarId) {
if (this.getById(data.activeHotbarId)) { if (this.getById(data.activeHotbarId)) {
@ -140,18 +119,19 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return this.hotbars.find((hotbar) => hotbar.id === id); return this.hotbars.find((hotbar) => hotbar.id === id);
} }
add(data: HotbarCreateOptions) { @action
add(data: HotbarCreateOptions, { setActive = false } = {}) {
const { const {
id = uuid.v4(), id = uuid.v4(),
items = this.initialItems, items = HotbarStore.getInitialItems(),
name, name,
} = data; } = data;
const hotbar = { id, name, items }; this.hotbars.push({ id, name, items });
this.hotbars.push(hotbar as Hotbar); if (setActive) {
this._activeHotbarId = id;
return hotbar as Hotbar; }
} }
@action @action

View File

@ -31,6 +31,7 @@ import { ExtensionLoader } from "../../extensions/extension-loader";
import type { LensExtension } from "../../extensions/lens-extension"; import type { LensExtension } from "../../extensions/lens-extension";
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler"; import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
import { when } from "mobx"; import { when } from "mobx";
import { ipcRenderer } from "electron";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel. // IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const ProtocolHandlerIpcPrefix = "protocol-handler"; export const ProtocolHandlerIpcPrefix = "protocol-handler";
@ -182,7 +183,10 @@ export abstract class LensProtocolRouter extends Singleton {
const extensionLoader = ExtensionLoader.getInstance(); const extensionLoader = ExtensionLoader.getInstance();
try { try {
await when(() => !!extensionLoader.getInstanceByName(name), { timeout: 5_000 }); /**
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
*/
await when(() => extensionLoader.getInstanceByName(name) !== (void 0), { timeout: 5_000 });
} catch(error) { } catch(error) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`); logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`);
@ -191,7 +195,13 @@ export abstract class LensProtocolRouter extends Singleton {
const extension = extensionLoader.getInstanceByName(name); const extension = extensionLoader.getInstanceByName(name);
if (!ExtensionsStore.getInstance().isEnabled(extension.id)) { if (!extension) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
return name;
}
if (!ExtensionsStore.getInstance().isEnabled(extension)) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
return name; return name;

View File

@ -68,7 +68,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
configName: "lens-user-store", configName: "lens-user-store",
migrations, migrations,
}); });
makeObservable(this); makeObservable(this);
fileNameMigration();
this.load();
} }
@observable lastSeenAppVersion = "0.0.0"; @observable lastSeenAppVersion = "0.0.0";
@ -101,33 +104,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
[path.join(os.homedir(), ".kube"), {}] [path.join(os.homedir(), ".kube"), {}]
]); ]);
async load(): Promise<void> {
/**
* This has to be here before the call to `new Config` in `super.load()`
* as we have to make sure that file is in the expected place for that call
*/
await fileNameMigration();
await super.load();
if (app) {
// track telemetry availability
reaction(() => this.allowTelemetry, allowed => {
appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
});
// open at system start-up
reaction(() => this.openAtLogin, openAtLogin => {
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
args: ["--hidden"]
});
}, {
fireImmediately: true,
});
}
}
@computed get isNewVersion() { @computed get isNewVersion() {
return semver.gt(getAppVersion(), this.lastSeenAppVersion); return semver.gt(getAppVersion(), this.lastSeenAppVersion);
} }
@ -136,6 +112,24 @@ export class UserStore extends BaseStore<UserStoreModel> {
return this.shell || process.env.SHELL || process.env.PTYSHELL; return this.shell || process.env.SHELL || process.env.PTYSHELL;
} }
startMainReactions() {
// track telemetry availability
reaction(() => this.allowTelemetry, allowed => {
appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
});
// open at system start-up
reaction(() => this.openAtLogin, openAtLogin => {
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
args: ["--hidden"]
});
}, {
fireImmediately: true,
});
}
/** /**
* Checks if a column (by ID) for a table (by ID) is configured to be hidden * Checks if a column (by ID) for a table (by ID) is configured to be hidden
* @param tableId The ID of the table to be checked against * @param tableId The ID of the table to be checked against
@ -165,8 +159,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
} }
@action @action
async resetTheme() { resetTheme() {
await this.whenLoaded;
this.colorTheme = UserStore.defaultTheme; this.colorTheme = UserStore.defaultTheme;
} }

View File

@ -55,6 +55,7 @@ export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
migrations, migrations,
}); });
makeObservable(this); makeObservable(this);
this.load();
} }
@action protected async fromStore(data: Partial<WeblinkStoreModel> = {}) { @action protected async fromStore(data: Partial<WeblinkStoreModel> = {}) {

View File

@ -39,6 +39,12 @@ jest.mock("../extension-installer", () => ({
installPackage: jest.fn() installPackage: jest.fn()
} }
})); }));
jest.mock("electron", () => ({
app: {
getPath: () => "tmp",
setLoginItemSettings: jest.fn(),
},
}));
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction<typeof watch>; const mockedWatch = watch as jest.MockedFunction<typeof watch>;

View File

@ -357,8 +357,8 @@ export class ExtensionDiscovery extends Singleton {
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> { protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
try { try {
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest; const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
const installedManifestPath = this.getInstalledManifestPath(manifest.name); const id = this.getInstalledManifestPath(manifest.name);
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath); const isEnabled = ExtensionsStore.getInstance().isEnabled({ id, isBundled });
const extensionDir = path.dirname(manifestPath); const extensionDir = path.dirname(manifestPath);
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`); const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir; const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
@ -369,9 +369,9 @@ export class ExtensionDiscovery extends Singleton {
} }
return { return {
id: installedManifestPath, id,
absolutePath, absolutePath,
manifestPath: installedManifestPath, manifestPath: id,
manifest, manifest,
isBundled, isBundled,
isEnabled, isEnabled,

View File

@ -48,6 +48,13 @@ export class ExtensionLoader extends Singleton {
protected extensions = observable.map<LensExtensionId, InstalledExtension>(); protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>(); protected instances = observable.map<LensExtensionId, LensExtension>();
/**
* This is the set of extensions that don't come with either
* - Main.LensExtension when running in the main process
* - Renderer.LensExtension when running in the renderer process
*/
protected nonInstancesByName = observable.set<string>();
/** /**
* This is updated by the `observe` in the constructor. DO NOT write directly to it * This is updated by the `observe` in the constructor. DO NOT write directly to it
*/ */
@ -101,7 +108,20 @@ export class ExtensionLoader extends Singleton {
return extensions; return extensions;
} }
getInstanceByName(name: string): LensExtension | undefined { /**
* Get the extension instance by its manifest name
* @param name The name of the extension
* @returns one of the following:
* - the instance of `Main.LensExtension` on the main process if created
* - the instance of `Renderer.LensExtension` on the renderer process if created
* - `null` if no class definition is provided for the current process
* - `undefined` if the name is not known about
*/
getInstanceByName(name: string): LensExtension | null | undefined {
if (this.nonInstancesByName.has(name)) {
return null;
}
return this.instancesByName.get(name); return this.instancesByName.get(name);
} }
@ -124,7 +144,7 @@ export class ExtensionLoader extends Singleton {
await this.initMain(); await this.initMain();
} }
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]); await Promise.all([this.whenLoaded]);
// broadcasting extensions between main/renderer processes // broadcasting extensions between main/renderer processes
reaction(() => this.toJSON(), () => this.broadcastExtensions(), { reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
@ -145,6 +165,7 @@ export class ExtensionLoader extends Singleton {
this.extensions.set(extension.id, extension); this.extensions.set(extension.id, extension);
} }
@action
removeInstance(lensExtensionId: LensExtensionId) { removeInstance(lensExtensionId: LensExtensionId) {
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`); logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
const instance = this.instances.get(lensExtensionId); const instance = this.instances.get(lensExtensionId);
@ -157,6 +178,7 @@ export class ExtensionLoader extends Singleton {
instance.disable(); instance.disable();
this.events.emit("remove", instance); this.events.emit("remove", instance);
this.instances.delete(lensExtensionId); this.instances.delete(lensExtensionId);
this.nonInstancesByName.delete(instance.name);
} catch (error) { } catch (error) {
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
} }
@ -302,13 +324,14 @@ export class ExtensionLoader extends Singleton {
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) { protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
return reaction(() => this.toJSON(), installedExtensions => { return reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, extension] of installedExtensions) { for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId); const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
if (extension.isCompatible && extension.isEnabled && !alreadyInit) { if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
try { try {
const LensExtensionClass = this.requireExtension(extension); const LensExtensionClass = this.requireExtension(extension);
if (!LensExtensionClass) { if (!LensExtensionClass) {
this.nonInstancesByName.add(extension.manifest.name);
continue; continue;
} }

View File

@ -26,13 +26,13 @@ import type { LensExtension } from "./lens-extension";
export abstract class ExtensionStore<T> extends BaseStore<T> { export abstract class ExtensionStore<T> extends BaseStore<T> {
protected extension: LensExtension; protected extension: LensExtension;
async loadExtension(extension: LensExtension) { loadExtension(extension: LensExtension) {
this.extension = extension; this.extension = extension;
return super.load(); return super.load();
} }
async load() { load() {
if (!this.extension) { return; } if (!this.extension) { return; }
return super.load(); return super.load();

View File

@ -39,6 +39,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
configName: "lens-extensions", configName: "lens-extensions",
}); });
makeObservable(this); makeObservable(this);
this.load();
} }
@computed @computed
@ -50,12 +51,10 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
protected state = observable.map<LensExtensionId, LensExtensionState>(); protected state = observable.map<LensExtensionId, LensExtensionState>();
isEnabled(extId: LensExtensionId): boolean { isEnabled({ id, isBundled }: { id: string, isBundled: boolean }): boolean {
const state = this.state.get(extId);
// By default false, so that copied extensions are disabled by default. // By default false, so that copied extensions are disabled by default.
// If user installs the extension from the UI, the Extensions component will specifically enable it. // If user installs the extension from the UI, the Extensions component will specifically enable it.
return Boolean(state?.enabled); return isBundled || Boolean(this.state.get(id)?.enabled);
} }
@action @action

View File

@ -22,6 +22,14 @@
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { ContextHandler } from "../context-handler"; import { ContextHandler } from "../context-handler";
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus"; import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
import mockFs from "mock-fs";
jest.mock("electron", () => ({
app: {
getPath: () => "tmp",
setLoginItemSettings: jest.fn(),
},
}));
enum ServiceResult { enum ServiceResult {
Success, Success,
@ -70,6 +78,10 @@ function getHandler() {
describe("ContextHandler", () => { describe("ContextHandler", () => {
beforeEach(() => { beforeEach(() => {
mockFs({
"tmp": {}
});
PrometheusProviderRegistry.createInstance(); PrometheusProviderRegistry.createInstance();
UserStore.createInstance(); UserStore.createInstance();
}); });
@ -77,6 +89,7 @@ describe("ContextHandler", () => {
afterEach(() => { afterEach(() => {
PrometheusProviderRegistry.resetInstance(); PrometheusProviderRegistry.resetInstance();
UserStore.resetInstance(); UserStore.resetInstance();
mockFs.restore();
}); });
describe("getPrometheusService", () => { describe("getPrometheusService", () => {

View File

@ -46,7 +46,8 @@ jest.mock("winston", () => ({
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
getPath: () => "/foo", getPath: () => "tmp",
setLoginItemSettings: jest.fn(),
}, },
})); }));
@ -77,8 +78,6 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilU
describe("kube auth proxy tests", () => { describe("kube auth proxy tests", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
UserStore.resetInstance();
UserStore.createInstance();
const mockMinikubeConfig = { const mockMinikubeConfig = {
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({
@ -102,13 +101,16 @@ describe("kube auth proxy tests", () => {
}], }],
kind: "Config", kind: "Config",
preferences: {}, preferences: {},
}) }),
"tmp": {},
}; };
mockFs(mockMinikubeConfig); mockFs(mockMinikubeConfig);
UserStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
UserStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });

View File

@ -67,6 +67,6 @@ const generalEntities = observable([
preferencesEntity preferencesEntity
]); ]);
export function initializeGeneralEntities() { export function syncGeneralEntities() {
catalogEntityRegistry.addObservableSource("lens:general", generalEntities); catalogEntityRegistry.addObservableSource("lens:general", generalEntities);
} }

View File

@ -19,6 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export { initializeWeblinks } from "./weblinks"; export { syncWeblinks } from "./weblinks";
export { KubeconfigSyncManager } from "./kubeconfig-sync"; export { KubeconfigSyncManager } from "./kubeconfig-sync";
export { initializeGeneralEntities } from "./general"; export { syncGeneralEntities } from "./general";

View File

@ -69,7 +69,7 @@ async function validateLink(link: WebLink) {
} }
export function initializeWeblinks() { export function syncWeblinks() {
const weblinkStore = WeblinkStore.getInstance(); const weblinkStore = WeblinkStore.getInstance();
const weblinks = observable.array(defaultLinks); const weblinks = observable.array(defaultLinks);

View File

@ -104,6 +104,7 @@ export class ClusterManager extends Singleton {
this.updateEntityStatus(entity, cluster); this.updateEntityStatus(entity, cluster);
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels); entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
entity.metadata.labels.distro = cluster.distribution;
if (cluster.preferences?.clusterName) { if (cluster.preferences?.clusterName) {
entity.metadata.name = cluster.preferences.clusterName; entity.metadata.name = cluster.preferences.clusterName;

View File

@ -42,6 +42,7 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
}); });
makeObservable(this); makeObservable(this);
this.load();
} }
/** /**

View File

@ -48,11 +48,17 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { pushCatalogToRenderer } from "./catalog-pusher"; import { pushCatalogToRenderer } from "./catalog-pusher";
import { catalogEntityRegistry } from "./catalog"; import { catalogEntityRegistry } from "./catalog";
import { HelmRepoManager } from "./helm/helm-repo-manager"; import { HelmRepoManager } from "./helm/helm-repo-manager";
import { KubeconfigSyncManager } from "./catalog-sources"; import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade"; import { handleWsUpgrade } from "./proxy/ws-upgrade";
import configurePackages from "../common/configure-packages"; import configurePackages from "../common/configure-packages";
import { PrometheusProviderRegistry } from "./prometheus"; import { PrometheusProviderRegistry } from "./prometheus";
import * as initializers from "./initializers"; import * as initializers from "./initializers";
import { ClusterStore } from "../common/cluster-store";
import { HotbarStore } from "../common/hotbar-store";
import { UserStore } from "../common/user-store";
import { WeblinkStore } from "../common/weblink-store";
import { ExtensionsStore } from "../extensions/extensions-store";
import { FilesystemProvisionerStore } from "./extension-filesystem";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer(); const cleanup = disposer();
@ -123,8 +129,23 @@ app.on("ready", async () => {
PrometheusProviderRegistry.createInstance(); PrometheusProviderRegistry.createInstance();
initializers.initPrometheusProviderRegistry(); initializers.initPrometheusProviderRegistry();
await initializers.initializeStores(); /**
initializers.initializeWeblinks(); * The following sync MUST be done before HotbarStore creation, because that
* store has migrations that will remove items that previous migrations add
* if this is not presant
*/
syncGeneralEntities();
logger.info("💾 Loading stores");
UserStore.createInstance().startMainReactions();
ClusterStore.createInstance().provideInitialFromMain();
HotbarStore.createInstance();
ExtensionsStore.createInstance();
FilesystemProvisionerStore.createInstance();
WeblinkStore.createInstance();
syncWeblinks();
HelmRepoManager.createInstance(); // create the instance HelmRepoManager.createInstance(); // create the instance
@ -182,7 +203,6 @@ app.on("ready", async () => {
ipcMainOn(IpcRendererNavigationEvents.LOADED, () => { ipcMainOn(IpcRendererNavigationEvents.LOADED, () => {
cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); cleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
KubeconfigSyncManager.getInstance().startSync(); KubeconfigSyncManager.getInstance().startSync();
initializers.initializeGeneralEntities();
startUpdateChecking(); startUpdateChecking();
LensProtocolRouterMain.getInstance().rendererLoaded = true; LensProtocolRouterMain.getInstance().rendererLoaded = true;
}); });

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { initializeGeneralEntities } from "../catalog-sources";

View File

@ -22,6 +22,3 @@
export * from "./registries"; export * from "./registries";
export * from "./metrics-providers"; export * from "./metrics-providers";
export * from "./ipc"; export * from "./ipc";
export * from "./weblinks";
export * from "./stores";
export * from "./general-entities";

View File

@ -1,48 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { HotbarStore } from "../../common/hotbar-store";
import { ClusterStore } from "../../common/cluster-store";
import { UserStore } from "../../common/user-store";
import { ExtensionsStore } from "../../extensions/extensions-store";
import { FilesystemProvisionerStore } from "../extension-filesystem";
import { WeblinkStore } from "../../common/weblink-store";
import logger from "../logger";
export async function initializeStores() {
const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance();
const hotbarStore = HotbarStore.createInstance();
const extensionsStore = ExtensionsStore.createInstance();
const filesystemStore = FilesystemProvisionerStore.createInstance();
const weblinkStore = WeblinkStore.createInstance();
logger.info("💾 Loading stores");
// preload
await Promise.all([
userStore.load(),
clusterStore.load(),
hotbarStore.load(),
extensionsStore.load(),
filesystemStore.load(),
weblinkStore.load()
]);
}

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { initializeWeblinks } from "../catalog-sources";

View File

@ -28,9 +28,17 @@ import { LensExtension } from "../../../extensions/main-api";
import { ExtensionLoader } from "../../../extensions/extension-loader"; import { ExtensionLoader } from "../../../extensions/extension-loader";
import { ExtensionsStore } from "../../../extensions/extensions-store"; import { ExtensionsStore } from "../../../extensions/extensions-store";
import { LensProtocolRouterMain } from "../router"; import { LensProtocolRouterMain } from "../router";
import mockFs from "mock-fs";
jest.mock("../../../common/ipc"); jest.mock("../../../common/ipc");
jest.mock("electron", () => ({
app: {
getPath: () => "tmp",
setLoginItemSettings: jest.fn(),
},
}));
function throwIfDefined(val: any): void { function throwIfDefined(val: any): void {
if (val != null) { if (val != null) {
throw val; throw val;
@ -39,6 +47,9 @@ function throwIfDefined(val: any): void {
describe("protocol router tests", () => { describe("protocol router tests", () => {
beforeEach(() => { beforeEach(() => {
mockFs({
"tmp": {}
});
ExtensionsStore.createInstance(); ExtensionsStore.createInstance();
ExtensionLoader.createInstance(); ExtensionLoader.createInstance();
@ -53,23 +64,24 @@ describe("protocol router tests", () => {
ExtensionsStore.resetInstance(); ExtensionsStore.resetInstance();
ExtensionLoader.resetInstance(); ExtensionLoader.resetInstance();
LensProtocolRouterMain.resetInstance(); LensProtocolRouterMain.resetInstance();
mockFs.restore();
}); });
it("should throw on non-lens URLS", () => { it("should throw on non-lens URLS", async () => {
try { try {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
expect(lpr.route("https://google.ca")).toBeUndefined(); expect(await lpr.route("https://google.ca")).toBeUndefined();
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
} }
}); });
it("should throw when host not internal or extension", () => { it("should throw when host not internal or extension", async () => {
try { try {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
expect(lpr.route("lens://foobar")).toBeUndefined(); expect(await lpr.route("lens://foobar")).toBeUndefined();
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
} }
@ -102,13 +114,13 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/", noop); lpr.addInternalHandler("/", noop);
try { try {
expect(lpr.route("lens://app")).toBeUndefined(); expect(await lpr.route("lens://app")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
try { try {
expect(lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined(); expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
@ -118,14 +130,14 @@ describe("protocol router tests", () => {
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched"); expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
}); });
it("should call handler if matches", () => { it("should call handler if matches", async () => {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
let called = false; let called = false;
lpr.addInternalHandler("/page", () => { called = true; }); lpr.addInternalHandler("/page", () => { called = true; });
try { try {
expect(lpr.route("lens://app/page")).toBeUndefined(); expect(await lpr.route("lens://app/page")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
@ -134,7 +146,7 @@ describe("protocol router tests", () => {
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched"); expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
}); });
it("should call most exact handler", () => { it("should call most exact handler", async () => {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
@ -142,7 +154,7 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; }); lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
try { try {
expect(lpr.route("lens://app/page/foo")).toBeUndefined(); expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
@ -182,7 +194,7 @@ describe("protocol router tests", () => {
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
try { try {
expect(lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined(); expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
@ -250,7 +262,7 @@ describe("protocol router tests", () => {
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" }); (ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
try { try {
expect(lpr.route("lens://extension/icecream/page")).toBeUndefined(); expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
@ -267,7 +279,7 @@ describe("protocol router tests", () => {
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError(); expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
}); });
it("should call most exact handler with 3 found handlers", () => { it("should call most exact handler with 3 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
@ -277,7 +289,7 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/page/bar", () => { called = 4; }); lpr.addInternalHandler("/page/bar", () => { called = 4; });
try { try {
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined(); expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }
@ -286,7 +298,7 @@ describe("protocol router tests", () => {
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
}); });
it("should call most exact handler with 2 found handlers", () => { it("should call most exact handler with 2 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
@ -295,7 +307,7 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/page/bar", () => { called = 4; }); lpr.addInternalHandler("/page/bar", () => { called = 4; });
try { try {
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined(); expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
} catch (error) { } catch (error) {
expect(throwIfDefined(error)).not.toThrow(); expect(throwIfDefined(error)).not.toThrow();
} }

View File

@ -73,7 +73,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
* This will send an IPC message to the renderer router to do the same * This will send an IPC message to the renderer router to do the same
* in the renderer. * in the renderer.
*/ */
public route(rawUrl: string) { public async route(rawUrl: string) {
try { try {
const url = new URLParse(rawUrl, true); const url = new URLParse(rawUrl, true);
@ -89,7 +89,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
if (routeInternally) { if (routeInternally) {
this._routeToInternal(url); this._routeToInternal(url);
} else { } else {
this._routeToExtension(url); await this._routeToExtension(url);
} }
} catch (error) { } catch (error) {
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl); broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);

View File

@ -23,7 +23,7 @@ import path from "path";
import { app } from "electron"; import { app } from "electron";
import fse from "fs-extra"; import fse from "fs-extra";
import type { ClusterModel } from "../../common/cluster-store"; import type { ClusterModel } from "../../common/cluster-store";
import { MigrationDeclaration, migrationLog } from "../helpers"; import type { MigrationDeclaration } from "../helpers";
interface Pre500WorkspaceStoreModel { interface Pre500WorkspaceStoreModel {
workspaces: { workspaces: {
@ -45,8 +45,6 @@ export default {
workspaces.set(id, name); workspaces.set(id, name);
} }
migrationLog("workspaces", JSON.stringify([...workspaces.entries()]));
const clusters: ClusterModel[] = store.get("clusters"); const clusters: ClusterModel[] = store.get("clusters");
for (const cluster of clusters) { for (const cluster of clusters) {
@ -58,8 +56,6 @@ export default {
store.set("clusters", clusters); store.set("clusters", clusters);
} catch (error) { } catch (error) {
migrationLog("error", error.path);
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) { if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
// ignore lens-workspace-store.json being missing // ignore lens-workspace-store.json being missing
throw error; throw error;

View File

@ -20,34 +20,24 @@
*/ */
// Cleans up a store that had the state related data stored // Cleans up a store that had the state related data stored
import type { Hotbar } from "../../common/hotbar-store"; import { Hotbar, HotbarStore } from "../../common/hotbar-store";
import { ClusterStore } from "../../common/cluster-store";
import * as uuid from "uuid"; import * as uuid from "uuid";
import type { MigrationDeclaration } from "../helpers"; import type { MigrationDeclaration } from "../helpers";
import { catalogEntity } from "../../main/catalog-sources/general";
export default { export default {
version: "5.0.0-alpha.0", version: "5.0.0-alpha.0",
run(store) { run(store) {
const hotbars: Hotbar[] = []; const hotbar: Hotbar = {
id: uuid.v4(),
name: "default",
items: HotbarStore.getInitialItems(),
};
ClusterStore.getInstance().clustersList.forEach((cluster: any) => { const { metadata: { uid, name, source } } = catalogEntity;
const name = cluster.workspace;
if (!name) return; hotbar.items[0] = { entity: { uid, name, source } };
let hotbar = hotbars.find((h) => h.name === name); store.set("hotbars", [hotbar]);
if (!hotbar) {
hotbar = { id: uuid.v4(), name, items: [] };
hotbars.push(hotbar);
}
hotbar.items.push({
entity: { uid: cluster.id },
params: {}
});
});
store.set("hotbars", hotbars);
} }
} as MigrationDeclaration; } as MigrationDeclaration;

View File

@ -19,12 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { createHash } from "crypto";
import { app } from "electron"; import { app } from "electron";
import fse from "fs-extra"; import fse from "fs-extra";
import { isNull } from "lodash";
import path from "path"; import path from "path";
import * as uuid from "uuid"; import * as uuid from "uuid";
import type { ClusterStoreModel } from "../../common/cluster-store"; import type { ClusterStoreModel } from "../../common/cluster-store";
import { defaultHotbarCells, Hotbar } from "../../common/hotbar-store"; import { defaultHotbarCells, Hotbar, HotbarStore } from "../../common/hotbar-store";
import { catalogEntity } from "../../main/catalog-sources/general";
import type { MigrationDeclaration } from "../helpers"; import type { MigrationDeclaration } from "../helpers";
interface Pre500WorkspaceStoreModel { interface Pre500WorkspaceStoreModel {
@ -49,7 +52,19 @@ export default {
workspaceHotbars.set(id, { workspaceHotbars.set(id, {
id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs
items: [], items: [],
name: `Workspace: ${name}`,
});
}
{
// grab the default named hotbar or the first.
const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
workspaceHotbars.set("default", {
name, name,
id,
items: items.filter(Boolean),
}); });
} }
@ -59,7 +74,7 @@ export default {
if (workspaceHotbar?.items.length < defaultHotbarCells) { if (workspaceHotbar?.items.length < defaultHotbarCells) {
workspaceHotbar.items.push({ workspaceHotbar.items.push({
entity: { entity: {
uid: cluster.id, uid: createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex"),
name: cluster.preferences.clusterName || cluster.contextName, name: cluster.preferences.clusterName || cluster.contextName,
} }
}); });
@ -74,6 +89,46 @@ export default {
hotbars.push(hotbar); hotbars.push(hotbar);
} }
/**
* Finally, make sure that the catalog entity hotbar item is in place.
* Just in case something else removed it.
*
* if every hotbar has elements that all not the `catalog-entity` item
*/
if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) {
// note, we will add a new whole hotbar here called "default" if that was previously removed
const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default");
const { metadata: { uid, name, source } } = catalogEntity;
if (defaultHotbar) {
const freeIndex = defaultHotbar.items.findIndex(isNull);
if (freeIndex === -1) {
// making a new hotbar is less destructive if the first hotbar
// called "default" is full than overriding a hotbar item
const hotbar = {
id: uuid.v4(),
name: "initial",
items: HotbarStore.getInitialItems(),
};
hotbar.items[0] = { entity: { uid, name, source } };
hotbars.unshift(hotbar);
} else {
defaultHotbar.items[freeIndex] = { entity: { uid, name, source } };
}
} else {
const hotbar = {
id: uuid.v4(),
name: "default",
items: HotbarStore.getInitialItems(),
};
hotbar.items[0] = { entity: { uid, name, source } };
hotbars.unshift(hotbar);
}
}
store.set("hotbars", hotbars); store.set("hotbars", hotbars);
} catch (error) { } catch (error) {
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) { if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {

View File

@ -20,7 +20,7 @@
*/ */
import type { Hotbar } from "../../common/hotbar-store"; import type { Hotbar } from "../../common/hotbar-store";
import { catalogEntityRegistry } from "../../renderer/api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../main/catalog";
import type { MigrationDeclaration } from "../helpers"; import type { MigrationDeclaration } from "../helpers";
export default { export default {

View File

@ -23,18 +23,18 @@ import fse from "fs-extra";
import { app, remote } from "electron"; import { app, remote } from "electron";
import path from "path"; import path from "path";
export async function fileNameMigration() { export function fileNameMigration() {
const userDataPath = (app || remote.app).getPath("userData"); const userDataPath = (app || remote.app).getPath("userData");
const configJsonPath = path.join(userDataPath, "config.json"); const configJsonPath = path.join(userDataPath, "config.json");
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
try { try {
await fse.move(configJsonPath, lensUserStoreJsonPath); fse.moveSync(configJsonPath, lensUserStoreJsonPath);
} catch (error) { } catch (error) {
if (error.code === "ENOENT" && error.path === configJsonPath) { // (No such file or directory) if (error.code === "ENOENT" && error.path === configJsonPath) { // (No such file or directory)
return; // file already moved return; // file already moved
} else if (error.message === "dest already exists.") { } else if (error.message === "dest already exists.") {
await fse.remove(configJsonPath); fse.removeSync(configJsonPath);
} else { } else {
// pass other errors along // pass other errors along
throw error; throw error;

View File

@ -42,6 +42,11 @@ import { ExtensionInstallationStateStore } from "./components/+extensions/extens
import { DefaultProps } from "./mui-base-theme"; import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages"; import configurePackages from "../common/configure-packages";
import * as initializers from "./initializers"; import * as initializers from "./initializers";
import { HotbarStore } from "../common/hotbar-store";
import { WeblinkStore } from "../common/weblink-store";
import { ExtensionsStore } from "../extensions/extensions-store";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import { ThemeStore } from "./theme.store";
configurePackages(); configurePackages();
@ -79,7 +84,13 @@ export async function bootstrap(App: AppComponent) {
ExtensionLoader.createInstance().init(); ExtensionLoader.createInstance().init();
ExtensionDiscovery.createInstance().init(); ExtensionDiscovery.createInstance().init();
await initializers.initStores(); UserStore.createInstance();
await ClusterStore.createInstance().loadInitialOnRenderer();
HotbarStore.createInstance();
ExtensionsStore.createInstance();
FilesystemProvisionerStore.createInstance();
ThemeStore.createInstance();
WeblinkStore.createInstance();
ExtensionInstallationStateStore.bindIpcListeners(); ExtensionInstallationStateStore.bindIpcListeners();
HelmRepoManager.createInstance(); // initialize the manager HelmRepoManager.createInstance(); // initialize the manager

View File

@ -112,7 +112,7 @@ export class AddCluster extends React.Component {
<h2>Add Clusters from Kubeconfig</h2> <h2>Add Clusters from Kubeconfig</h2>
<p> <p>
Clusters added here are <b>not</b> merged into the <code>~/.kube/config</code> file. Clusters added here are <b>not</b> merged into the <code>~/.kube/config</code> file.
Read more about adding clusters <a href={`${docsUrl}/clusters/adding-clusters/`} rel="noreferrer" target="_blank">here</a>. Read more about adding clusters <a href={`${docsUrl}/catalog/add-clusters/`} rel="noreferrer" target="_blank">here</a>.
</p> </p>
<div className="flex column"> <div className="flex column">
<AceEditor <AceEditor

View File

@ -30,6 +30,7 @@ import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { HotbarIcon } from "../hotbar/hotbar-icon"; import { HotbarIcon } from "../hotbar/hotbar-icon";
import type { CatalogEntityItem } from "./catalog-entity.store"; import type { CatalogEntityItem } from "./catalog-entity.store";
import { isDevelopment } from "../../../common/vars";
interface Props<T extends CatalogEntity> { interface Props<T extends CatalogEntity> {
item: CatalogEntityItem<T> | null | undefined; item: CatalogEntityItem<T> | null | undefined;
@ -91,6 +92,11 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
<DrawerItem name="Labels"> <DrawerItem name="Labels">
{...item.getLabelBadges(this.props.hideDetails)} {...item.getLabelBadges(this.props.hideDetails)}
</DrawerItem> </DrawerItem>
{isDevelopment && (
<DrawerItem name="Id">
{item.getId()}
</DrawerItem>
)}
</div> </div>
</div> </div>
)} )}

View File

@ -173,7 +173,7 @@ export class Catalog extends React.Component<Props> {
renderIcon(item: CatalogEntityItem<CatalogEntity>) { renderIcon(item: CatalogEntityItem<CatalogEntity>) {
return ( return (
<HotbarIcon <HotbarIcon
uid={item.getId()} uid={`catalog-icon-${item.getId()}`}
title={item.getName()} title={item.getName()}
source={item.source} source={item.source}
src={item.entity.spec.icon?.src} src={item.entity.spec.icon?.src}

View File

@ -61,7 +61,7 @@ describe("Extensions", () => {
ExtensionInstallationStateStore.reset(); ExtensionInstallationStateStore.reset();
UserStore.resetInstance(); UserStore.resetInstance();
await UserStore.createInstance().load(); UserStore.createInstance();
ExtensionDiscovery.resetInstance(); ExtensionDiscovery.resetInstance();
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve()); ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());

View File

@ -30,10 +30,11 @@ import type { LogTabData } from "../log-tab.store";
import { dockerPod, deploymentPod1 } from "./pod.mock"; import { dockerPod, deploymentPod1 } from "./pod.mock";
import { ThemeStore } from "../../../theme.store"; import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store"; import { UserStore } from "../../../../common/user-store";
import mockFs from "mock-fs";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
getPath: () => "/foo", getPath: () => "tmp",
}, },
})); }));
@ -71,6 +72,9 @@ const getFewPodsTabData = (): LogTabData => {
describe("<LogResourceSelector />", () => { describe("<LogResourceSelector />", () => {
beforeEach(() => { beforeEach(() => {
mockFs({
"tmp": {}
});
UserStore.createInstance(); UserStore.createInstance();
ThemeStore.createInstance(); ThemeStore.createInstance();
}); });
@ -78,6 +82,7 @@ describe("<LogResourceSelector />", () => {
afterEach(() => { afterEach(() => {
UserStore.resetInstance(); UserStore.resetInstance();
ThemeStore.resetInstance(); ThemeStore.resetInstance();
mockFs.restore();
}); });
it("renders w/o errors", () => { it("renders w/o errors", () => {

View File

@ -26,6 +26,14 @@ import React from "react";
import { ThemeStore } from "../../../theme.store"; import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store"; import { UserStore } from "../../../../common/user-store";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import mockFs from "mock-fs";
jest.mock("electron", () => ({
app: {
getPath: () => "tmp",
setLoginItemSettings: jest.fn(),
},
}));
const mockHotbars: {[id: string]: any} = { const mockHotbars: {[id: string]: any} = {
"1": { "1": {
@ -48,6 +56,9 @@ jest.mock("../../../../common/hotbar-store", () => ({
describe("<HotbarRemoveCommand />", () => { describe("<HotbarRemoveCommand />", () => {
beforeEach(() => { beforeEach(() => {
mockFs({
"tmp": {}
});
UserStore.createInstance(); UserStore.createInstance();
ThemeStore.createInstance(); ThemeStore.createInstance();
}); });
@ -55,6 +66,7 @@ describe("<HotbarRemoveCommand />", () => {
afterEach(() => { afterEach(() => {
UserStore.resetInstance(); UserStore.resetInstance();
ThemeStore.resetInstance(); ThemeStore.resetInstance();
mockFs.restore();
}); });
it("renders w/o errors", () => { it("renders w/o errors", () => {
@ -73,4 +85,3 @@ describe("<HotbarRemoveCommand />", () => {
spy.mockRestore(); spy.mockRestore();
}); });
}); });

View File

@ -33,22 +33,14 @@ const uniqueHotbarName: InputValidator = {
@observer @observer
export class HotbarAddCommand extends React.Component { export class HotbarAddCommand extends React.Component {
onSubmit = (name: string) => {
onSubmit(name: string) {
if (!name.trim()) { if (!name.trim()) {
return; return;
} }
const hotbarStore = HotbarStore.getInstance(); HotbarStore.getInstance().add({ name }, { setActive: true });
const hotbar = hotbarStore.add({
name
});
hotbarStore.activeHotbarId = hotbar.id;
CommandOverlay.close(); CommandOverlay.close();
} };
render() { render() {
return ( return (
@ -58,10 +50,11 @@ export class HotbarAddCommand extends React.Component {
autoFocus={true} autoFocus={true}
theme="round-black" theme="round-black"
data-test-id="command-palette-hotbar-add-name" data-test-id="command-palette-hotbar-add-name"
validators={[uniqueHotbarName]} validators={uniqueHotbarName}
onSubmit={(v) => this.onSubmit(v)} onSubmit={this.onSubmit}
dirty={true} dirty={true}
showValidationLine={true} /> showValidationLine={true}
/>
<small className="hint"> <small className="hint">
Please provide a new hotbar name (Press &quot;Enter&quot; to confirm or &quot;Escape&quot; to cancel) Please provide a new hotbar name (Press &quot;Enter&quot; to confirm or &quot;Escape&quot; to cancel)
</small> </small>

View File

@ -114,7 +114,6 @@
} }
img { img {
padding: 3px;
border-radius: 6px; border-radius: 6px;
&.active { &.active {

View File

@ -160,7 +160,7 @@ export class HotbarMenu extends React.Component<Props> {
/> />
) : ( ) : (
<HotbarIcon <HotbarIcon
uid={item.entity.uid} uid={`hotbar-icon-${item.entity.uid}`}
title={item.entity.name} title={item.entity.name}
source={item.entity.source} source={item.entity.source}
menuItems={disabledMenuItems} menuItems={disabledMenuItems}

View File

@ -27,5 +27,4 @@ export * from "./registries";
export * from "./welcome-menu-registry"; export * from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry"; export * from "./workloads-overview-detail-registry";
export * from "./catalog"; export * from "./catalog";
export * from "./stores";
export * from "./ipc"; export * from "./ipc";

View File

@ -1,50 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { HotbarStore } from "../../common/hotbar-store";
import { ClusterStore } from "../../common/cluster-store";
import { UserStore } from "../../common/user-store";
import { ExtensionsStore } from "../../extensions/extensions-store";
import { FilesystemProvisionerStore } from "../../main/extension-filesystem";
import { ThemeStore } from "../theme.store";
import { WeblinkStore } from "../../common/weblink-store";
export async function initStores() {
const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance();
const extensionsStore = ExtensionsStore.createInstance();
const filesystemStore = FilesystemProvisionerStore.createInstance();
const themeStore = ThemeStore.createInstance();
const hotbarStore = HotbarStore.createInstance();
const weblinkStore = WeblinkStore.createInstance();
// preload common stores
await Promise.all([
userStore.load(),
hotbarStore.load(),
clusterStore.load(),
extensionsStore.load(),
filesystemStore.load(),
themeStore.init(),
weblinkStore.load()
]);
}

View File

@ -27,7 +27,7 @@ export function initWelcomeMenuRegistry() {
WelcomeMenuRegistry.getInstance() WelcomeMenuRegistry.getInstance()
.add([ .add([
{ {
title: "Browse Clusters", title: "Browse Clusters in Catalog",
icon: "view_list", icon: "view_list",
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" } } )), click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" } } )),
} }

View File

@ -20,9 +20,11 @@
*/ */
import { computed, observable, reaction, makeObservable } from "mobx"; import { computed, observable, reaction, makeObservable } from "mobx";
import { autoBind, boundMethod, Singleton } from "./utils"; import { autoBind, iter, Singleton } from "./utils";
import { UserStore } from "../common/user-store"; import { UserStore } from "../common/user-store";
import logger from "../main/logger"; import logger from "../main/logger";
import darkTheme from "./themes/lens-dark.json";
import lightTheme from "./themes/lens-light.json";
export type ThemeId = string; export type ThemeId = string;
@ -32,12 +34,15 @@ export enum ThemeType {
} }
export interface Theme { export interface Theme {
id: ThemeId; // filename without .json-extension
type: ThemeType; type: ThemeType;
name?: string; name: string;
colors?: Record<string, string>; colors: Record<string, string>;
description?: string; description: string;
author?: string; author: string;
}
export interface ThemeItems extends Theme {
id: string;
} }
export class ThemeStore extends Singleton { export class ThemeStore extends Singleton {
@ -45,16 +50,12 @@ export class ThemeStore extends Singleton {
// bundled themes from `themes/${themeId}.json` // bundled themes from `themes/${themeId}.json`
private allThemes = observable.map<string, Theme>([ private allThemes = observable.map<string, Theme>([
["lens-dark", { id: "lens-dark", type: ThemeType.DARK }], ["lens-dark", { ...darkTheme, type: ThemeType.DARK }],
["lens-light", { id: "lens-light", type: ThemeType.LIGHT }], ["lens-light", { ...lightTheme, type: ThemeType.LIGHT }],
]); ]);
@computed get themeIds(): string[] { @computed get themes(): ThemeItems[] {
return Array.from(this.allThemes.keys()); return Array.from(iter.map(this.allThemes, ([id, theme]) => ({ id, ...theme })));
}
@computed get themes(): Theme[] {
return Array.from(this.allThemes.values());
} }
@computed get activeThemeId(): string { @computed get activeThemeId(): string {
@ -62,12 +63,7 @@ export class ThemeStore extends Singleton {
} }
@computed get activeTheme(): Theme { @computed get activeTheme(): Theme {
const activeTheme = this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark"); return this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark");
return {
colors: {},
...activeTheme,
};
} }
constructor() { constructor() {
@ -77,9 +73,9 @@ export class ThemeStore extends Singleton {
autoBind(this); autoBind(this);
// auto-apply active theme // auto-apply active theme
reaction(() => this.activeThemeId, async themeId => { reaction(() => this.activeThemeId, themeId => {
try { try {
this.applyTheme(await this.loadTheme(themeId)); this.applyTheme(this.getThemeById(themeId));
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
UserStore.getInstance().resetTheme(); UserStore.getInstance().resetTheme();
@ -89,38 +85,10 @@ export class ThemeStore extends Singleton {
}); });
} }
async init() {
// preload all themes
await Promise.all(this.themeIds.map(this.loadTheme));
}
getThemeById(themeId: ThemeId): Theme { getThemeById(themeId: ThemeId): Theme {
return this.allThemes.get(themeId); return this.allThemes.get(themeId);
} }
@boundMethod
protected async loadTheme(themeId: ThemeId): Promise<Theme> {
try {
const existingTheme = this.getThemeById(themeId);
if (existingTheme) {
const theme = await import(
/* webpackChunkName: "themes/[name]" */
`./themes/${themeId}.json`
);
existingTheme.author = theme.author;
existingTheme.colors = theme.colors;
existingTheme.description = theme.description;
existingTheme.name = theme.name;
}
return existingTheme;
} catch (err) {
throw new Error(`Can't load theme "${themeId}": ${err}`);
}
}
protected applyTheme(theme: Theme) { protected applyTheme(theme: Theme) {
if (!this.styles) { if (!this.styles) {
this.styles = document.createElement("style"); this.styles = document.createElement("style");