mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Catalog & Hotbar - initial groundwork (#2418)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
e18a041b13
commit
99a464c61d
@ -12,7 +12,6 @@
|
|||||||
"dev": "npm run build --watch",
|
"dev": "npm run build --watch",
|
||||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
|
|||||||
@ -1,21 +1,55 @@
|
|||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension, Store, Interface, Component } from "@k8slens/extensions";
|
||||||
import { MetricsFeature } from "./src/metrics-feature";
|
import { MetricsFeature } from "./src/metrics-feature";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
||||||
clusterFeatures = [
|
onActivate() {
|
||||||
{
|
const category = Store.catalogCategories.getForGroupKind<Store.KubernetesClusterCategory>("entity.k8slens.dev", "KubernetesCluster");
|
||||||
title: "Metrics Stack",
|
|
||||||
components: {
|
if (!category) {
|
||||||
Description: () => (
|
return;
|
||||||
<span>
|
|
||||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
|
||||||
Install this only if you don't have existing Prometheus stack installed.
|
|
||||||
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/metrics-cluster-feature/resources" rel="noreferrer" target="_blank">here</a>.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
feature: new MetricsFeature()
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async clusterContextMenuOpen(cluster: Store.KubernetesCluster, ctx: Interface.CatalogEntityContextMenuContext) {
|
||||||
|
if (!cluster.status.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricsFeature = new MetricsFeature();
|
||||||
|
|
||||||
|
await metricsFeature.updateStatus(cluster);
|
||||||
|
|
||||||
|
if (metricsFeature.status.installed) {
|
||||||
|
if (metricsFeature.status.canUpgrade) {
|
||||||
|
ctx.menuItems.unshift({
|
||||||
|
icon: "refresh",
|
||||||
|
title: "Upgrade Lens Metrics stack",
|
||||||
|
onClick: async () => {
|
||||||
|
metricsFeature.upgrade(cluster);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.menuItems.unshift({
|
||||||
|
icon: "toggle_off",
|
||||||
|
title: "Uninstall Lens Metrics stack",
|
||||||
|
onClick: async () => {
|
||||||
|
await metricsFeature.uninstall(cluster);
|
||||||
|
|
||||||
|
Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.menuItems.unshift({
|
||||||
|
icon: "toggle_on",
|
||||||
|
title: "Install Lens Metrics stack",
|
||||||
|
onClick: async () => {
|
||||||
|
metricsFeature.install(cluster);
|
||||||
|
|
||||||
|
Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
storageClass: null,
|
storageClass: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
async install(cluster: Store.Cluster): Promise<void> {
|
async install(cluster: Store.KubernetesCluster): Promise<void> {
|
||||||
// Check if there are storageclasses
|
// Check if there are storageclasses
|
||||||
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
||||||
const scs = await storageClassApi.list();
|
const scs = await storageClassApi.list();
|
||||||
@ -62,11 +62,11 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async upgrade(cluster: Store.Cluster): Promise<void> {
|
async upgrade(cluster: Store.KubernetesCluster): Promise<void> {
|
||||||
return this.install(cluster);
|
return this.install(cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
|
async updateStatus(cluster: Store.KubernetesCluster): Promise<ClusterFeature.FeatureStatus> {
|
||||||
try {
|
try {
|
||||||
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
||||||
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||||
@ -87,12 +87,13 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
return this.status;
|
return this.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall(cluster: Store.Cluster): Promise<void> {
|
async uninstall(cluster: Store.KubernetesCluster): Promise<void> {
|
||||||
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
|
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
|
||||||
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
|
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
|
||||||
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
|
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
|
||||||
|
|
||||||
await namespaceApi.delete({name: "lens-metrics"});
|
await namespaceApi.delete({name: "lens-metrics"});
|
||||||
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
|
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
|
||||||
await clusterRoleApi.delete({name: "lens-prometheus"}); }
|
await clusterRoleApi.delete({name: "lens-prometheus"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"dev": "webpack --watch",
|
"dev": "webpack --watch",
|
||||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
"@types/analytics-node": "^3.1.3",
|
"@types/analytics-node": "^3.1.3",
|
||||||
|
|||||||
@ -102,13 +102,12 @@ export class Tracker extends Util.Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected reportData() {
|
protected reportData() {
|
||||||
const clustersList = Store.clusterStore.enabledClustersList;
|
const clustersList = Store.catalogEntities.getItemsForApiKind<Store.KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster");
|
||||||
|
|
||||||
this.event("generic-data", "report", {
|
this.event("generic-data", "report", {
|
||||||
appVersion: App.version,
|
appVersion: App.version,
|
||||||
os: this.os,
|
os: this.os,
|
||||||
clustersCount: clustersList.length,
|
clustersCount: clustersList.length,
|
||||||
workspacesCount: Store.workspaceStore.enabledWorkspacesList.length,
|
|
||||||
extensions: App.getEnabledExtensions()
|
extensions: App.getEnabledExtensions()
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,10 +117,10 @@ export class Tracker extends Util.Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reportClusterData(cluster: Store.ClusterModel) {
|
protected reportClusterData(cluster: Store.KubernetesCluster) {
|
||||||
this.event("cluster-data", "report", {
|
this.event("cluster-data", "report", {
|
||||||
id: cluster.metadata.id,
|
id: cluster.metadata.id,
|
||||||
managed: !!cluster.ownerRef,
|
managed: cluster.metadata.source !== "local",
|
||||||
kubernetesVersion: cluster.metadata.version,
|
kubernetesVersion: cluster.metadata.version,
|
||||||
distribution: cluster.metadata.distribution,
|
distribution: cluster.metadata.distribution,
|
||||||
nodesCount: cluster.metadata.nodes,
|
nodesCount: cluster.metadata.nodes,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ describe("Lens cluster pages", () => {
|
|||||||
const addCluster = async () => {
|
const addCluster = async () => {
|
||||||
await utils.clickWhatsNew(app);
|
await utils.clickWhatsNew(app);
|
||||||
await utils.clickWelcomeNotification(app);
|
await utils.clickWelcomeNotification(app);
|
||||||
|
await app.client.waitUntilTextExists("div", "Catalog");
|
||||||
await addMinikubeCluster(app);
|
await addMinikubeCluster(app);
|
||||||
await waitForMinikubeDashboard(app);
|
await waitForMinikubeDashboard(app);
|
||||||
await app.client.click('a[href="/nodes"]');
|
await app.client.click('a[href="/nodes"]');
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
import { Application } from "spectron";
|
|
||||||
import * as utils from "../helpers/utils";
|
|
||||||
import { addMinikubeCluster, minikubeReady } from "../helpers/minikube";
|
|
||||||
import { exec } from "child_process";
|
|
||||||
import * as util from "util";
|
|
||||||
|
|
||||||
export const promiseExec = util.promisify(exec);
|
|
||||||
|
|
||||||
jest.setTimeout(60000);
|
|
||||||
|
|
||||||
describe("Lens integration tests", () => {
|
|
||||||
let app: Application;
|
|
||||||
const ready = minikubeReady("workspace-int-tests");
|
|
||||||
|
|
||||||
utils.describeIf(ready)("workspaces", () => {
|
|
||||||
utils.beforeAllWrapped(async () => {
|
|
||||||
app = await utils.appStart();
|
|
||||||
await utils.clickWhatsNew(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.afterAllWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const switchToWorkspace = async (name: string) => {
|
|
||||||
await app.client.click("[data-test-id=current-workspace]");
|
|
||||||
await app.client.keys(name);
|
|
||||||
await app.client.keys("Enter");
|
|
||||||
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createWorkspace = async (name: string) => {
|
|
||||||
await app.client.click("[data-test-id=current-workspace]");
|
|
||||||
await app.client.keys("add workspace");
|
|
||||||
await app.client.keys("Enter");
|
|
||||||
await app.client.keys(name);
|
|
||||||
await app.client.keys("Enter");
|
|
||||||
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("creates new workspace", async () => {
|
|
||||||
const name = "test-workspace";
|
|
||||||
|
|
||||||
await createWorkspace(name);
|
|
||||||
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edits current workspaces", async () => {
|
|
||||||
await createWorkspace("to-be-edited");
|
|
||||||
await app.client.click("[data-test-id=current-workspace]");
|
|
||||||
await app.client.keys("edit current workspace");
|
|
||||||
await app.client.keys("Enter");
|
|
||||||
await app.client.keys("edited-workspace");
|
|
||||||
await app.client.keys("Enter");
|
|
||||||
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", "edited-workspace");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds cluster in default workspace", async () => {
|
|
||||||
await switchToWorkspace("default");
|
|
||||||
await addMinikubeCluster(app);
|
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
|
||||||
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds cluster in test-workspace", async () => {
|
|
||||||
await switchToWorkspace("test-workspace");
|
|
||||||
await addMinikubeCluster(app);
|
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -49,6 +49,8 @@ export async function addMinikubeCluster(app: Application) {
|
|||||||
} // else the only context, which must be 'minikube', is automatically selected
|
} // else the only context, which must be 'minikube', is automatically selected
|
||||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||||
await app.client.click("button.primary"); // add minikube cluster
|
await app.client.click("button.primary"); // add minikube cluster
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
|
await app.client.click("div.TableRow");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForMinikubeDashboard(app: Application) {
|
export async function waitForMinikubeDashboard(app: Application) {
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export async function appStart() {
|
|||||||
export async function clickWhatsNew(app: Application) {
|
export async function clickWhatsNew(app: Application) {
|
||||||
await app.client.waitUntilTextExists("h1", "What's new?");
|
await app.client.waitUntilTextExists("h1", "What's new?");
|
||||||
await app.client.click("button.primary");
|
await app.client.click("button.primary");
|
||||||
await app.client.waitUntilTextExists("h5", "Clusters");
|
await app.client.waitUntilTextExists("div", "Catalog");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickWelcomeNotification(app: Application) {
|
export async function clickWelcomeNotification(app: Application) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "4.2.0-rc.3",
|
"version": "5.0.0-alpha.0",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021, Mirantis, Inc.",
|
"copyright": "© 2021, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
||||||
"dev:main": "yarn run compile:main --watch",
|
"dev:main": "yarn run compile:main --watch",
|
||||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||||
"dev:extension-types": "yarn run compile:extension-types --watch --progress",
|
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import mockFs from "mock-fs";
|
|||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { Cluster } from "../../main/cluster";
|
import { Cluster } from "../../main/cluster";
|
||||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
||||||
import { workspaceStore } from "../workspace-store";
|
|
||||||
|
|
||||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||||
const kubeconfig = `
|
const kubeconfig = `
|
||||||
@ -77,8 +76,7 @@ describe("empty config", () => {
|
|||||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||||
clusterName: "minikube"
|
clusterName: "minikube"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -92,12 +90,6 @@ describe("empty config", () => {
|
|||||||
expect(storedCluster.enabled).toBe(true);
|
expect(storedCluster.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds cluster to default workspace", () => {
|
|
||||||
const storedCluster = clusterStore.getById("foo");
|
|
||||||
|
|
||||||
expect(storedCluster.workspace).toBe("default");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes cluster from store", async () => {
|
it("removes cluster from store", async () => {
|
||||||
await clusterStore.removeById("foo");
|
await clusterStore.removeById("foo");
|
||||||
expect(clusterStore.getById("foo")).toBeNull();
|
expect(clusterStore.getById("foo")).toBeNull();
|
||||||
@ -106,7 +98,6 @@ describe("empty config", () => {
|
|||||||
it("sets active cluster", () => {
|
it("sets active cluster", () => {
|
||||||
clusterStore.setActive("foo");
|
clusterStore.setActive("foo");
|
||||||
expect(clusterStore.active.id).toBe("foo");
|
expect(clusterStore.active.id).toBe("foo");
|
||||||
expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,8 +110,7 @@ describe("empty config", () => {
|
|||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "prod"
|
clusterName: "prod"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
|
||||||
workspace: "workstation"
|
|
||||||
}),
|
}),
|
||||||
new Cluster({
|
new Cluster({
|
||||||
id: "dev",
|
id: "dev",
|
||||||
@ -128,8 +118,7 @@ describe("empty config", () => {
|
|||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "dev"
|
clusterName: "dev"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
|
||||||
workspace: "workstation"
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -139,51 +128,11 @@ describe("empty config", () => {
|
|||||||
expect(clusterStore.clusters.size).toBe(2);
|
expect(clusterStore.clusters.size).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets clusters by workspaces", () => {
|
|
||||||
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
|
||||||
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
|
||||||
|
|
||||||
expect(defaultClusters.length).toBe(0);
|
|
||||||
expect(wsClusters.length).toBe(2);
|
|
||||||
expect(wsClusters[0].id).toBe("prod");
|
|
||||||
expect(wsClusters[1].id).toBe("dev");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check if cluster's kubeconfig file saved", () => {
|
it("check if cluster's kubeconfig file saved", () => {
|
||||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||||
|
|
||||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("check if reorderring works for same from and to", () => {
|
|
||||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("prod");
|
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
|
||||||
expect(clusters[1].id).toBe("dev");
|
|
||||||
expect(clusters[1].preferences.iconOrder).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check if reorderring works for different from and to", () => {
|
|
||||||
clusterStore.swapIconOrders("workstation", 0, 1);
|
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("dev");
|
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
|
||||||
expect(clusters[1].id).toBe("prod");
|
|
||||||
expect(clusters[1].preferences.iconOrder).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check if after icon reordering, changing workspaces still works", () => {
|
|
||||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
|
||||||
clusterStore.getById("prod").workspace = "default";
|
|
||||||
|
|
||||||
expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1);
|
|
||||||
expect(clusterStore.getByWorkspaceId("default").length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -486,12 +435,6 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds cluster to default workspace", async () => {
|
|
||||||
const storedClusterData = clusterStore.clustersList[0];
|
|
||||||
|
|
||||||
expect(storedClusterData.workspace).toBe("default");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||||
|
|||||||
@ -1,189 +0,0 @@
|
|||||||
import mockFs from "mock-fs";
|
|
||||||
|
|
||||||
jest.mock("electron", () => {
|
|
||||||
return {
|
|
||||||
app: {
|
|
||||||
getVersion: () => "99.99.99",
|
|
||||||
getPath: () => "tmp",
|
|
||||||
getLocale: () => "en",
|
|
||||||
setLoginItemSettings: jest.fn(),
|
|
||||||
},
|
|
||||||
ipcMain: {
|
|
||||||
handle: jest.fn(),
|
|
||||||
on: jest.fn()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { Workspace, WorkspaceStore } from "../workspace-store";
|
|
||||||
|
|
||||||
describe("workspace store tests", () => {
|
|
||||||
describe("for an empty config", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
WorkspaceStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "lens-workspace-store.json": "{}" } });
|
|
||||||
|
|
||||||
await WorkspaceStore.getInstance<WorkspaceStore>().load();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("default workspace should always exist", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(ws.workspaces.size).toBe(1);
|
|
||||||
expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("default workspace should be enabled", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(ws.workspaces.size).toBe(1);
|
|
||||||
expect(ws.getById(WorkspaceStore.defaultId).enabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot remove the default workspace", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can update workspace description", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
const workspace = ws.addWorkspace(new Workspace({
|
|
||||||
id: "foobar",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
|
|
||||||
workspace.description = "Foobar description";
|
|
||||||
ws.updateWorkspace(workspace);
|
|
||||||
|
|
||||||
expect(ws.getById("foobar").description).toBe("Foobar description");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can add workspaces", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "123",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
|
|
||||||
const workspace = ws.getById("123");
|
|
||||||
|
|
||||||
expect(workspace.name).toBe("foobar");
|
|
||||||
expect(workspace.enabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot set a non-existent workspace to be active", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(() => ws.setActive("abc")).toThrow("doesn't exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can set a existent workspace to be active", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "abc",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(() => ws.setActive("abc")).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can remove a workspace", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "123",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "1234",
|
|
||||||
name: "foobar 1",
|
|
||||||
}));
|
|
||||||
ws.removeWorkspaceById("123");
|
|
||||||
|
|
||||||
expect(ws.workspaces.size).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot create workspace with existent name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "someid",
|
|
||||||
name: "default",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot create workspace with empty name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "random",
|
|
||||||
name: "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot create workspace with ' ' name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "random",
|
|
||||||
name: " ",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
|
|
||||||
it("trim workspace name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "random",
|
|
||||||
name: "default ",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for a non-empty config", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
WorkspaceStore.resetInstance();
|
|
||||||
mockFs({
|
|
||||||
tmp: {
|
|
||||||
"lens-workspace-store.json": JSON.stringify({
|
|
||||||
currentWorkspace: "abc",
|
|
||||||
workspaces: [{
|
|
||||||
id: "abc",
|
|
||||||
name: "test"
|
|
||||||
}, {
|
|
||||||
id: "default",
|
|
||||||
name: "default"
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await WorkspaceStore.getInstance<WorkspaceStore>().load();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't revert to default workspace", async () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(ws.currentWorkspaceId).toBe("abc");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
56
src/common/catalog-category-registry.ts
Normal file
56
src/common/catalog-category-registry.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { action, computed, observable, toJS } from "mobx";
|
||||||
|
import { CatalogCategory, CatalogEntityData } from "./catalog-entity";
|
||||||
|
|
||||||
|
export class CatalogCategoryRegistry {
|
||||||
|
@observable protected categories: CatalogCategory[] = [];
|
||||||
|
|
||||||
|
@action add(category: CatalogCategory) {
|
||||||
|
this.categories.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action remove(category: CatalogCategory) {
|
||||||
|
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get items() {
|
||||||
|
return toJS(this.categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
|
||||||
|
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityForData(data: CatalogEntityData) {
|
||||||
|
const category = this.getCategoryForEntity(data);
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitApiVersion = data.apiVersion.split("/");
|
||||||
|
const version = splitApiVersion[1];
|
||||||
|
|
||||||
|
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||||
|
|
||||||
|
if (!specVersion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new specVersion.entityClass(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData) {
|
||||||
|
const splitApiVersion = data.apiVersion.split("/");
|
||||||
|
const group = splitApiVersion[0];
|
||||||
|
|
||||||
|
const category = this.categories.find((category) => {
|
||||||
|
return category.spec.group === group && category.spec.names.kind === data.kind;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!category) return null;
|
||||||
|
|
||||||
|
return category as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
2
src/common/catalog-entities/index.ts
Normal file
2
src/common/catalog-entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./kubernetes-cluster";
|
||||||
|
export * from "./web-link";
|
||||||
105
src/common/catalog-entities/kubernetes-cluster.ts
Normal file
105
src/common/catalog-entities/kubernetes-cluster.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||||
|
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||||
|
import { clusterDisconnectHandler } from "../cluster-ipc";
|
||||||
|
import { clusterStore } from "../cluster-store";
|
||||||
|
import { requestMain } from "../ipc";
|
||||||
|
|
||||||
|
export type KubernetesClusterSpec = {
|
||||||
|
kubeconfigPath: string;
|
||||||
|
kubeconfigContext: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
|
phase: "connected" | "disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KubernetesCluster implements CatalogEntity {
|
||||||
|
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "KubernetesCluster";
|
||||||
|
@observable public metadata: CatalogEntityMetadata;
|
||||||
|
@observable public status: KubernetesClusterStatus;
|
||||||
|
@observable public spec: KubernetesClusterSpec;
|
||||||
|
|
||||||
|
constructor(data: CatalogEntityData) {
|
||||||
|
this.metadata = data.metadata;
|
||||||
|
this.status = data.status as KubernetesClusterStatus;
|
||||||
|
this.spec = data.spec as KubernetesClusterSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRun(context: CatalogEntityActionContext) {
|
||||||
|
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDetailsOpen() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||||
|
context.menuItems = [
|
||||||
|
{
|
||||||
|
icon: "settings",
|
||||||
|
title: "Settings",
|
||||||
|
onClick: async () => context.navigate(`/cluster/${this.metadata.uid}/settings`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "delete",
|
||||||
|
title: "Delete",
|
||||||
|
onClick: async () => clusterStore.removeById(this.metadata.uid),
|
||||||
|
confirm: {
|
||||||
|
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.status.active) {
|
||||||
|
context.menuItems.unshift({
|
||||||
|
icon: "link_off",
|
||||||
|
title: "Disconnect",
|
||||||
|
onClick: async () => {
|
||||||
|
clusterStore.deactivate(this.metadata.uid);
|
||||||
|
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
||||||
|
|
||||||
|
if (category) category.emit("contextMenuOpen", this, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KubernetesClusterCategory extends EventEmitter implements CatalogCategory {
|
||||||
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "CatalogCategory";
|
||||||
|
public metadata = {
|
||||||
|
name: "Kubernetes Clusters"
|
||||||
|
};
|
||||||
|
public spec = {
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "v1alpha1",
|
||||||
|
entityClass: KubernetesCluster
|
||||||
|
}
|
||||||
|
],
|
||||||
|
names: {
|
||||||
|
kind: "KubernetesCluster"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogCategoryRegistry.add(new KubernetesClusterCategory());
|
||||||
65
src/common/catalog-entities/web-link.ts
Normal file
65
src/common/catalog-entities/web-link.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
|
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||||
|
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||||
|
|
||||||
|
export interface WebLinkStatus extends CatalogEntityStatus {
|
||||||
|
phase: "valid" | "invalid";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebLinkSpec = {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WebLink implements CatalogEntity {
|
||||||
|
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "KubernetesCluster";
|
||||||
|
@observable public metadata: CatalogEntityMetadata;
|
||||||
|
@observable public status: WebLinkStatus;
|
||||||
|
@observable public spec: WebLinkSpec;
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRun() {
|
||||||
|
window.open(this.spec.url, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDetailsOpen() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
async onContextMenuOpen() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebLinkCategory implements CatalogCategory {
|
||||||
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "CatalogCategory";
|
||||||
|
public metadata = {
|
||||||
|
name: "Web Links"
|
||||||
|
};
|
||||||
|
public spec = {
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "v1alpha1",
|
||||||
|
entityClass: WebLink
|
||||||
|
}
|
||||||
|
],
|
||||||
|
names: {
|
||||||
|
kind: "WebLink"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||||
32
src/common/catalog-entity-registry.ts
Normal file
32
src/common/catalog-entity-registry.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { action, computed, observable } from "mobx";
|
||||||
|
import { CatalogEntity } from "./catalog-entity";
|
||||||
|
|
||||||
|
export class CatalogEntityRegistry {
|
||||||
|
protected sources = observable.map<string, CatalogEntity[]>([], { deep: true });
|
||||||
|
|
||||||
|
@action addSource(id: string, source: CatalogEntity[]) {
|
||||||
|
this.sources.set(id, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action removeSource(id: string) {
|
||||||
|
this.sources.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get items() {
|
||||||
|
const catalogItems: CatalogEntity[] = [];
|
||||||
|
|
||||||
|
for (const items of this.sources.values()) {
|
||||||
|
items.forEach((item) => catalogItems.push(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return catalogItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
|
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||||
|
|
||||||
|
return items as T[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntityRegistry = new CatalogEntityRegistry();
|
||||||
75
src/common/catalog-entity.ts
Normal file
75
src/common/catalog-entity.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
export interface CatalogCategoryVersion {
|
||||||
|
name: string;
|
||||||
|
entityClass: { new(data: CatalogEntityData): CatalogEntity };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogCategory {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
spec: {
|
||||||
|
group: string;
|
||||||
|
versions: CatalogCategoryVersion[];
|
||||||
|
names: {
|
||||||
|
kind: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getId: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CatalogEntityMetadata = {
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
source?: string;
|
||||||
|
labels: {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
[key: string]: string | object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CatalogEntityStatus = {
|
||||||
|
phase: string;
|
||||||
|
reason?: string;
|
||||||
|
message?: string;
|
||||||
|
active?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CatalogEntityActionContext {
|
||||||
|
navigate: (url: string) => void;
|
||||||
|
setCommandPaletteContext: (context?: CatalogEntity) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CatalogEntityContextMenu = {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
onClick: () => Promise<void>;
|
||||||
|
confirm?: {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CatalogEntityContextMenuContext {
|
||||||
|
navigate: (url: string) => void;
|
||||||
|
menuItems: CatalogEntityContextMenu[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CatalogEntityData = {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
metadata: CatalogEntityMetadata;
|
||||||
|
status: CatalogEntityStatus;
|
||||||
|
spec: {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CatalogEntity extends CatalogEntityData {
|
||||||
|
getId: () => string;
|
||||||
|
getName: () => string;
|
||||||
|
onRun: (context: CatalogEntityActionContext) => Promise<void>;
|
||||||
|
onDetailsOpen: (context: CatalogEntityActionContext) => Promise<void>;
|
||||||
|
onContextMenuOpen: (context: CatalogEntityContextMenuContext) => Promise<void>;
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { workspaceStore } from "./workspace-store";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||||
import { unlink } from "fs-extra";
|
import { unlink } from "fs-extra";
|
||||||
@ -12,9 +11,6 @@ import { dumpConfigYaml } from "./kube-helpers";
|
|||||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||||
import _ from "lodash";
|
|
||||||
import move from "array-move";
|
|
||||||
import type { WorkspaceId } from "./workspace-store";
|
|
||||||
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
export interface ClusterIconUpload {
|
||||||
@ -47,8 +43,12 @@ export interface ClusterModel {
|
|||||||
/** Path to cluster kubeconfig */
|
/** Path to cluster kubeconfig */
|
||||||
kubeConfigPath: string;
|
kubeConfigPath: string;
|
||||||
|
|
||||||
/** Workspace id */
|
/**
|
||||||
workspace?: WorkspaceId;
|
* Workspace id
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
workspace?: string;
|
||||||
|
|
||||||
/** User context in kubeconfig */
|
/** User context in kubeconfig */
|
||||||
contextName?: string;
|
contextName?: string;
|
||||||
@ -226,7 +226,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.activeCluster = clusterId;
|
this.activeCluster = clusterId;
|
||||||
workspaceStore.setLastActiveClusterId(clusterId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate(id: ClusterId) {
|
deactivate(id: ClusterId) {
|
||||||
@ -235,22 +234,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
|
||||||
const clusters = this.getByWorkspaceId(workspace);
|
|
||||||
|
|
||||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
|
||||||
throw new Error(`invalid from<->to arguments`);
|
|
||||||
}
|
|
||||||
|
|
||||||
move.mutate(clusters, from, to);
|
|
||||||
|
|
||||||
for (const i in clusters) {
|
|
||||||
// This resets the iconOrder to the current display order
|
|
||||||
clusters[i].preferences.iconOrder = +i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasClusters() {
|
hasClusters() {
|
||||||
return this.clusters.size > 0;
|
return this.clusters.size > 0;
|
||||||
}
|
}
|
||||||
@ -259,13 +242,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return this.clusters.get(id) ?? null;
|
return this.clusters.get(id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
|
||||||
const clusters = Array.from(this.clusters.values())
|
|
||||||
.filter(cluster => cluster.workspace === workspaceId);
|
|
||||||
|
|
||||||
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
addClusters(...models: ClusterModel[]): Cluster[] {
|
||||||
const clusters: Cluster[] = [];
|
const clusters: Cluster[] = [];
|
||||||
@ -317,13 +293,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
removeByWorkspaceId(workspaceId: string) {
|
|
||||||
this.getByWorkspaceId(workspaceId).forEach(cluster => {
|
|
||||||
this.removeById(cluster.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
||||||
const currentClusters = this.clusters.toJS();
|
const currentClusters = this.clusters.toJS();
|
||||||
|
|||||||
67
src/common/hotbar-store.ts
Normal file
67
src/common/hotbar-store.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { action, comparer, observable, toJS } from "mobx";
|
||||||
|
import { BaseStore } from "./base-store";
|
||||||
|
import migrations from "../migrations/hotbar-store";
|
||||||
|
|
||||||
|
export interface HotbarItem {
|
||||||
|
entity: {
|
||||||
|
uid: string;
|
||||||
|
};
|
||||||
|
params?: {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hotbar {
|
||||||
|
name: string;
|
||||||
|
items: HotbarItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HotbarStoreModel {
|
||||||
|
hotbars: Hotbar[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||||
|
@observable hotbars: Hotbar[] = [];
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super({
|
||||||
|
configName: "lens-hotbar-store",
|
||||||
|
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||||
|
syncOptions: {
|
||||||
|
equals: comparer.structural,
|
||||||
|
},
|
||||||
|
migrations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||||
|
this.hotbars = data.hotbars || [{
|
||||||
|
name: "default",
|
||||||
|
items: []
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
getByName(name: string) {
|
||||||
|
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(hotbar: Hotbar) {
|
||||||
|
this.hotbars.push(hotbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(hotbar: Hotbar) {
|
||||||
|
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): HotbarStoreModel {
|
||||||
|
const model: HotbarStoreModel = {
|
||||||
|
hotbars: this.hotbars
|
||||||
|
};
|
||||||
|
|
||||||
|
return toJS(model, {
|
||||||
|
recurseEverything: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();
|
||||||
@ -1,345 +0,0 @@
|
|||||||
import { ipcRenderer } from "electron";
|
|
||||||
import { action, computed, observable, toJS, reaction } from "mobx";
|
|
||||||
import { BaseStore } from "./base-store";
|
|
||||||
import { clusterStore } from "./cluster-store";
|
|
||||||
import { appEventBus } from "./event-bus";
|
|
||||||
import { broadcastMessage, handleRequest, requestMain } from "../common/ipc";
|
|
||||||
import logger from "../main/logger";
|
|
||||||
import type { ClusterId } from "./cluster-store";
|
|
||||||
|
|
||||||
export type WorkspaceId = string;
|
|
||||||
|
|
||||||
export interface WorkspaceStoreModel {
|
|
||||||
workspaces: WorkspaceModel[];
|
|
||||||
currentWorkspace?: WorkspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceModel {
|
|
||||||
id: WorkspaceId;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
ownerRef?: string;
|
|
||||||
lastActiveClusterId?: ClusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceState {
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateFromModel = Symbol("updateFromModel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workspace
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class Workspace implements WorkspaceModel, WorkspaceState {
|
|
||||||
/**
|
|
||||||
* Unique id for workspace
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable id: WorkspaceId;
|
|
||||||
/**
|
|
||||||
* Workspace name
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable name: string;
|
|
||||||
/**
|
|
||||||
* Workspace description
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable description?: string;
|
|
||||||
/**
|
|
||||||
* Workspace owner reference
|
|
||||||
*
|
|
||||||
* If extension sets ownerRef then it needs to explicitly mark workspace as enabled onActivate (or when workspace is saved)
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable ownerRef?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last active cluster id
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable lastActiveClusterId?: ClusterId;
|
|
||||||
|
|
||||||
|
|
||||||
@observable private _enabled: boolean;
|
|
||||||
|
|
||||||
constructor(data: WorkspaceModel) {
|
|
||||||
Object.assign(this, data);
|
|
||||||
|
|
||||||
if (!ipcRenderer) {
|
|
||||||
reaction(() => this.getState(), () => {
|
|
||||||
this.pushState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is workspace enabled
|
|
||||||
*
|
|
||||||
* Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace.
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
get enabled(): boolean {
|
|
||||||
return !this.isManaged || this._enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
set enabled(enabled: boolean) {
|
|
||||||
this._enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is workspace managed by an extension
|
|
||||||
*/
|
|
||||||
get isManaged(): boolean {
|
|
||||||
return !!this.ownerRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workspace state
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
getState(): WorkspaceState {
|
|
||||||
return toJS({
|
|
||||||
enabled: this.enabled
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push state
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @param state workspace state
|
|
||||||
*/
|
|
||||||
pushState(state = this.getState()) {
|
|
||||||
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id});
|
|
||||||
broadcastMessage("workspace:state", this.id, toJS(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param state workspace state
|
|
||||||
*/
|
|
||||||
@action setState(state: WorkspaceState) {
|
|
||||||
Object.assign(this, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
[updateFromModel] = action((model: WorkspaceModel) => {
|
|
||||||
Object.assign(this, model);
|
|
||||||
});
|
|
||||||
|
|
||||||
toJSON(): WorkspaceModel {
|
|
||||||
return toJS({
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
description: this.description,
|
|
||||||
ownerRef: this.ownerRef,
|
|
||||||
lastActiveClusterId: this.lastActiveClusterId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|
||||||
static readonly defaultId: WorkspaceId = "default";
|
|
||||||
private static stateRequestChannel = "workspace:states";
|
|
||||||
|
|
||||||
@observable currentWorkspaceId = WorkspaceStore.defaultId;
|
|
||||||
@observable workspaces = observable.map<WorkspaceId, Workspace>();
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
super({
|
|
||||||
configName: "lens-workspace-store",
|
|
||||||
});
|
|
||||||
|
|
||||||
this.workspaces.set(WorkspaceStore.defaultId, new Workspace({
|
|
||||||
id: WorkspaceStore.defaultId,
|
|
||||||
name: "default"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
await super.load();
|
|
||||||
type workspaceStateSync = {
|
|
||||||
id: string;
|
|
||||||
state: WorkspaceState;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ipcRenderer) {
|
|
||||||
logger.info("[WORKSPACE-STORE] requesting initial state sync");
|
|
||||||
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
|
|
||||||
|
|
||||||
workspaceStates.forEach((workspaceState) => {
|
|
||||||
const workspace = this.getById(workspaceState.id);
|
|
||||||
|
|
||||||
if (workspace) {
|
|
||||||
workspace.setState(workspaceState.state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
|
|
||||||
const states: workspaceStateSync[] = [];
|
|
||||||
|
|
||||||
this.workspacesList.forEach((workspace) => {
|
|
||||||
states.push({
|
|
||||||
state: workspace.getState(),
|
|
||||||
id: workspace.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return states;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerIpcListener() {
|
|
||||||
logger.info("[WORKSPACE-STORE] starting to listen state events");
|
|
||||||
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
|
|
||||||
this.getById(workspaceId)?.setState(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterIpcListener() {
|
|
||||||
super.unregisterIpcListener();
|
|
||||||
ipcRenderer.removeAllListeners("workspace:state");
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get currentWorkspace(): Workspace {
|
|
||||||
return this.getById(this.currentWorkspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get workspacesList() {
|
|
||||||
return Array.from(this.workspaces.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get enabledWorkspacesList() {
|
|
||||||
return this.workspacesList.filter((w) => w.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
pushState() {
|
|
||||||
this.workspaces.forEach((w) => {
|
|
||||||
w.pushState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isDefault(id: WorkspaceId) {
|
|
||||||
return id === WorkspaceStore.defaultId;
|
|
||||||
}
|
|
||||||
|
|
||||||
getById(id: WorkspaceId): Workspace {
|
|
||||||
return this.workspaces.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getByName(name: string): Workspace {
|
|
||||||
return this.workspacesList.find(workspace => workspace.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setActive(id = WorkspaceStore.defaultId) {
|
|
||||||
if (id === this.currentWorkspaceId) return;
|
|
||||||
|
|
||||||
if (!this.getById(id)) {
|
|
||||||
throw new Error(`workspace ${id} doesn't exist`);
|
|
||||||
}
|
|
||||||
this.currentWorkspaceId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
addWorkspace(workspace: Workspace) {
|
|
||||||
const { id, name } = workspace;
|
|
||||||
|
|
||||||
if (!name.trim() || this.getByName(name.trim())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.workspaces.set(id, workspace);
|
|
||||||
|
|
||||||
if (!workspace.isManaged) {
|
|
||||||
workspace.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
appEventBus.emit({name: "workspace", action: "add"});
|
|
||||||
|
|
||||||
return workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateWorkspace(workspace: Workspace) {
|
|
||||||
this.workspaces.set(workspace.id, workspace);
|
|
||||||
appEventBus.emit({name: "workspace", action: "update"});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeWorkspace(workspace: Workspace) {
|
|
||||||
this.removeWorkspaceById(workspace.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeWorkspaceById(id: WorkspaceId) {
|
|
||||||
const workspace = this.getById(id);
|
|
||||||
|
|
||||||
if (!workspace) return;
|
|
||||||
|
|
||||||
if (this.isDefault(id)) {
|
|
||||||
throw new Error("Cannot remove default workspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentWorkspaceId === id) {
|
|
||||||
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
|
||||||
}
|
|
||||||
this.workspaces.delete(id);
|
|
||||||
appEventBus.emit({name: "workspace", action: "remove"});
|
|
||||||
clusterStore.removeByWorkspaceId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) {
|
|
||||||
this.getById(workspaceId).lastActiveClusterId = clusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
this.currentWorkspaceId = currentWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentWorkspaces = this.workspaces.toJS();
|
|
||||||
const newWorkspaceIds = new Set<WorkspaceId>([WorkspaceStore.defaultId]); // never delete default
|
|
||||||
|
|
||||||
for (const workspaceModel of workspaces) {
|
|
||||||
const oldWorkspace = this.workspaces.get(workspaceModel.id);
|
|
||||||
|
|
||||||
if (oldWorkspace) {
|
|
||||||
oldWorkspace[updateFromModel](workspaceModel);
|
|
||||||
} else {
|
|
||||||
this.workspaces.set(workspaceModel.id, new Workspace(workspaceModel));
|
|
||||||
}
|
|
||||||
|
|
||||||
newWorkspaceIds.add(workspaceModel.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove deleted workspaces
|
|
||||||
for (const workspaceId of currentWorkspaces.keys()) {
|
|
||||||
if (!newWorkspaceIds.has(workspaceId)) {
|
|
||||||
this.workspaces.delete(workspaceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): WorkspaceStoreModel {
|
|
||||||
return toJS({
|
|
||||||
currentWorkspace: this.currentWorkspaceId,
|
|
||||||
workspaces: this.workspacesList.map((w) => w.toJSON()),
|
|
||||||
}, {
|
|
||||||
recurseEverything: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const workspaceStore = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
@ -3,11 +3,12 @@ import path from "path";
|
|||||||
import hb from "handlebars";
|
import hb from "handlebars";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
import { Cluster } from "../main/cluster";
|
import { KubernetesCluster } from "./core-api/stores";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { requestMain } from "../common/ipc";
|
import { requestMain } from "../common/ipc";
|
||||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
||||||
|
import { clusterStore } from "../common/cluster-store";
|
||||||
|
|
||||||
export interface ClusterFeatureStatus {
|
export interface ClusterFeatureStatus {
|
||||||
/** feature's current version, as set by the implementation */
|
/** feature's current version, as set by the implementation */
|
||||||
@ -44,7 +45,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be installed on
|
* @param cluster the cluster that the feature is to be installed on
|
||||||
*/
|
*/
|
||||||
abstract async install(cluster: Cluster): Promise<void>;
|
abstract install(cluster: KubernetesCluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
||||||
@ -52,7 +53,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be upgraded on
|
* @param cluster the cluster that the feature is to be upgraded on
|
||||||
*/
|
*/
|
||||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
abstract upgrade(cluster: KubernetesCluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
||||||
@ -60,7 +61,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be uninstalled from
|
* @param cluster the cluster that the feature is to be uninstalled from
|
||||||
*/
|
*/
|
||||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
abstract uninstall(cluster: KubernetesCluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
||||||
@ -72,7 +73,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @return a promise, resolved with the updated ClusterFeatureStatus
|
* @return a promise, resolved with the updated ClusterFeatureStatus
|
||||||
*/
|
*/
|
||||||
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
abstract updateStatus(cluster: KubernetesCluster): Promise<ClusterFeatureStatus>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
||||||
@ -82,9 +83,15 @@ export abstract class ClusterFeature {
|
|||||||
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
|
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
|
||||||
* cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
|
* cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
|
||||||
*/
|
*/
|
||||||
protected async applyResources(cluster: Cluster, resourceSpec: string | string[]) {
|
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
|
||||||
let resources: string[];
|
let resources: string[];
|
||||||
|
|
||||||
|
const clusterModel = clusterStore.getById(cluster.metadata.uid);
|
||||||
|
|
||||||
|
if (!clusterModel) {
|
||||||
|
throw new Error(`cluster not found`);
|
||||||
|
}
|
||||||
|
|
||||||
if ( typeof resourceSpec === "string" ) {
|
if ( typeof resourceSpec === "string" ) {
|
||||||
resources = this.renderTemplates(resourceSpec);
|
resources = this.renderTemplates(resourceSpec);
|
||||||
} else {
|
} else {
|
||||||
@ -92,9 +99,9 @@ export abstract class ClusterFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (app) {
|
if (app) {
|
||||||
await new ResourceApplier(cluster).kubectlApplyAll(resources);
|
await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
|
||||||
} else {
|
} else {
|
||||||
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources);
|
await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/extensions/core-api/catalog.ts
Normal file
12
src/extensions/core-api/catalog.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import { CatalogEntity } from "../../common/catalog-entity";
|
||||||
|
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry";
|
||||||
|
|
||||||
|
export class CatalogEntityRegistry {
|
||||||
|
@computed getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
|
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntities = new CatalogEntityRegistry();
|
||||||
@ -1,8 +1,4 @@
|
|||||||
export { ExtensionStore } from "../extension-store";
|
export { ExtensionStore } from "../extension-store";
|
||||||
|
export { KubernetesCluster, KubernetesClusterCategory } from "../../common/catalog-entities/kubernetes-cluster";
|
||||||
export { clusterStore, Cluster, ClusterStore } from "../stores/cluster-store";
|
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry";
|
||||||
export type { ClusterModel, ClusterId } from "../stores/cluster-store";
|
export { catalogEntities } from "./catalog";
|
||||||
|
|
||||||
export { workspaceStore, Workspace, WorkspaceStore } from "../stores/workspace-store";
|
|
||||||
export type { WorkspaceId, WorkspaceModel } from "../stores/workspace-store";
|
|
||||||
|
|
||||||
|
|||||||
@ -213,7 +213,6 @@ export class ExtensionLoader {
|
|||||||
registries.globalPageRegistry.add(extension.globalPages, extension),
|
registries.globalPageRegistry.add(extension.globalPages, extension),
|
||||||
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
|
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
|
||||||
registries.appPreferenceRegistry.add(extension.appPreferences),
|
registries.appPreferenceRegistry.add(extension.appPreferences),
|
||||||
registries.clusterFeatureRegistry.add(extension.clusterFeatures),
|
|
||||||
registries.statusBarRegistry.add(extension.statusBarItems),
|
registries.statusBarRegistry.add(extension.statusBarItems),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(extension.commands),
|
||||||
];
|
];
|
||||||
|
|||||||
1
src/extensions/interfaces/catalog.ts
Normal file
1
src/extensions/interfaces/catalog.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "../../common/catalog-entity";
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./registrations";
|
export * from "./registrations";
|
||||||
|
export * from "./catalog";
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry";
|
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry";
|
||||||
export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../registries/cluster-feature-registry";
|
|
||||||
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
|
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
|
||||||
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
||||||
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import type { MenuRegistration } from "./registries/menu-registry";
|
|||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { WindowManager } from "../main/window-manager";
|
import { WindowManager } from "../main/window-manager";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||||
|
import { CatalogEntity } from "../common/catalog-entity";
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
@ -16,4 +18,12 @@ export class LensMainExtension extends LensExtension {
|
|||||||
|
|
||||||
await windowManager.navigate(pageUrl, frameId);
|
await windowManager.navigate(pageUrl, frameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addCatalogSource(id: string, source: CatalogEntity[]) {
|
||||||
|
catalogEntityRegistry.addSource(`${this.name}:${id}`, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCatalogSource(id: string) {
|
||||||
|
catalogEntityRegistry.removeSource(`${this.name}:${id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
|
import type { AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
|
||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
@ -11,7 +11,6 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
||||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||||
appPreferences: AppPreferenceRegistration[] = [];
|
appPreferences: AppPreferenceRegistration[] = [];
|
||||||
clusterFeatures: ClusterFeatureRegistration[] = [];
|
|
||||||
statusBarItems: StatusBarRegistration[] = [];
|
statusBarItems: StatusBarRegistration[] = [];
|
||||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
|
||||||
import { ClusterFeature } from "../cluster-feature";
|
|
||||||
|
|
||||||
export interface ClusterFeatureComponents {
|
|
||||||
Description: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClusterFeatureRegistration {
|
|
||||||
title: string;
|
|
||||||
components: ClusterFeatureComponents
|
|
||||||
feature: ClusterFeature
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> {
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clusterFeatureRegistry = new ClusterFeatureRegistry();
|
|
||||||
@ -1,25 +1,25 @@
|
|||||||
// Extensions API -> Commands
|
// Extensions API -> Commands
|
||||||
|
|
||||||
import type { Cluster } from "../../main/cluster";
|
|
||||||
import type { Workspace } from "../../common/workspace-store";
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { action } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { LensExtension } from "../lens-extension";
|
import { LensExtension } from "../lens-extension";
|
||||||
|
import { CatalogEntity } from "../../common/catalog-entity";
|
||||||
|
|
||||||
export type CommandContext = {
|
export type CommandContext = {
|
||||||
cluster?: Cluster;
|
entity?: CatalogEntity;
|
||||||
workspace?: Workspace;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CommandRegistration {
|
export interface CommandRegistration {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
scope: "cluster" | "global";
|
scope: "entity" | "global";
|
||||||
action: (context: CommandContext) => void;
|
action: (context: CommandContext) => void;
|
||||||
isActive?: (context: CommandContext) => boolean;
|
isActive?: (context: CommandContext) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
||||||
|
@observable activeEntity: CatalogEntity;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
|
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
|
||||||
const itemArray = [items].flat();
|
const itemArray = [items].flat();
|
||||||
|
|||||||
@ -7,6 +7,5 @@ export * from "./app-preference-registry";
|
|||||||
export * from "./status-bar-registry";
|
export * from "./status-bar-registry";
|
||||||
export * from "./kube-object-detail-registry";
|
export * from "./kube-object-detail-registry";
|
||||||
export * from "./kube-object-menu-registry";
|
export * from "./kube-object-menu-registry";
|
||||||
export * from "./cluster-feature-registry";
|
|
||||||
export * from "./kube-object-status-registry";
|
export * from "./kube-object-status-registry";
|
||||||
export * from "./command-registry";
|
export * from "./command-registry";
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
import { clusterStore as internalClusterStore, ClusterId } from "../../common/cluster-store";
|
|
||||||
import type { ClusterModel } from "../../common/cluster-store";
|
|
||||||
import { Cluster } from "../../main/cluster";
|
|
||||||
import { Singleton } from "../core-api/utils";
|
|
||||||
import { ObservableMap } from "mobx";
|
|
||||||
|
|
||||||
export { Cluster } from "../../main/cluster";
|
|
||||||
export type { ClusterModel, ClusterId } from "../../common/cluster-store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store for all added clusters
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class ClusterStore extends Singleton {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active cluster id
|
|
||||||
*/
|
|
||||||
get activeClusterId(): string {
|
|
||||||
return internalClusterStore.activeCluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set active cluster id
|
|
||||||
*/
|
|
||||||
set activeClusterId(id : ClusterId) {
|
|
||||||
internalClusterStore.setActive(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all clusters
|
|
||||||
*/
|
|
||||||
get clusters(): ObservableMap<string, Cluster> {
|
|
||||||
return internalClusterStore.clusters;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get active cluster (a cluster which is currently visible)
|
|
||||||
*/
|
|
||||||
get activeCluster(): Cluster | null {
|
|
||||||
return internalClusterStore.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all clusters
|
|
||||||
*/
|
|
||||||
get clustersList(): Cluster[] {
|
|
||||||
return internalClusterStore.clustersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all enabled clusters
|
|
||||||
*/
|
|
||||||
get enabledClustersList(): Cluster[] {
|
|
||||||
return internalClusterStore.enabledClustersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all clusters that have active connection to a Kubernetes cluster
|
|
||||||
*/
|
|
||||||
get connectedClustersList(): Cluster[] {
|
|
||||||
return internalClusterStore.connectedClustersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cluster object by cluster id
|
|
||||||
* @param id cluster id
|
|
||||||
*/
|
|
||||||
getById(id: ClusterId): Cluster {
|
|
||||||
return internalClusterStore.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all clusters belonging to a workspace
|
|
||||||
* @param workspaceId workspace id
|
|
||||||
*/
|
|
||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
|
||||||
return internalClusterStore.getByWorkspaceId(workspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add clusters to store
|
|
||||||
* @param models list of cluster models
|
|
||||||
*/
|
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
|
||||||
return internalClusterStore.addClusters(...models);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a cluster to store
|
|
||||||
* @param model cluster
|
|
||||||
*/
|
|
||||||
addCluster(model: ClusterModel | Cluster): Cluster {
|
|
||||||
return internalClusterStore.addCluster(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a cluster from store
|
|
||||||
* @param model cluster
|
|
||||||
*/
|
|
||||||
async removeCluster(model: ClusterModel) {
|
|
||||||
return internalClusterStore.removeById(model.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a cluster from store by id
|
|
||||||
* @param clusterId cluster id
|
|
||||||
*/
|
|
||||||
async removeById(clusterId: ClusterId) {
|
|
||||||
return internalClusterStore.removeById(clusterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all clusters belonging to a workspaces
|
|
||||||
* @param workspaceId workspace id
|
|
||||||
*/
|
|
||||||
removeByWorkspaceId(workspaceId: string) {
|
|
||||||
return internalClusterStore.removeByWorkspaceId(workspaceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { Singleton } from "../core-api/utils";
|
|
||||||
import { workspaceStore as internalWorkspaceStore, WorkspaceStore as InternalWorkspaceStore, Workspace, WorkspaceId } from "../../common/workspace-store";
|
|
||||||
import { ObservableMap } from "mobx";
|
|
||||||
|
|
||||||
export { Workspace } from "../../common/workspace-store";
|
|
||||||
export type { WorkspaceId, WorkspaceModel } from "../../common/workspace-store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores all workspaces
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class WorkspaceStore extends Singleton {
|
|
||||||
/**
|
|
||||||
* Default workspace id, this workspace is always present
|
|
||||||
*/
|
|
||||||
static readonly defaultId: WorkspaceId = InternalWorkspaceStore.defaultId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently active workspace id
|
|
||||||
*/
|
|
||||||
get currentWorkspaceId(): string {
|
|
||||||
return internalWorkspaceStore.currentWorkspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set active workspace id
|
|
||||||
*/
|
|
||||||
set currentWorkspaceId(id: string) {
|
|
||||||
internalWorkspaceStore.currentWorkspaceId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all workspaces
|
|
||||||
*/
|
|
||||||
get workspaces(): ObservableMap<string, Workspace> {
|
|
||||||
return internalWorkspaceStore.workspaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently active workspace
|
|
||||||
*/
|
|
||||||
get currentWorkspace(): Workspace {
|
|
||||||
return internalWorkspaceStore.currentWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all workspaces
|
|
||||||
*/
|
|
||||||
get workspacesList(): Workspace[] {
|
|
||||||
return internalWorkspaceStore.workspacesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all enabled (visible) workspaces
|
|
||||||
*/
|
|
||||||
get enabledWorkspacesList(): Workspace[] {
|
|
||||||
return internalWorkspaceStore.enabledWorkspacesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workspace by id
|
|
||||||
* @param id workspace id
|
|
||||||
*/
|
|
||||||
getById(id: WorkspaceId): Workspace {
|
|
||||||
return internalWorkspaceStore.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workspace by name
|
|
||||||
* @param name workspace name
|
|
||||||
*/
|
|
||||||
getByName(name: string): Workspace {
|
|
||||||
return internalWorkspaceStore.getByName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set active workspace
|
|
||||||
* @param id workspace id
|
|
||||||
*/
|
|
||||||
setActive(id = WorkspaceStore.defaultId) {
|
|
||||||
return internalWorkspaceStore.setActive(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a workspace to store
|
|
||||||
* @param workspace workspace
|
|
||||||
*/
|
|
||||||
addWorkspace(workspace: Workspace) {
|
|
||||||
return internalWorkspaceStore.addWorkspace(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a workspace in store
|
|
||||||
* @param workspace workspace
|
|
||||||
*/
|
|
||||||
updateWorkspace(workspace: Workspace) {
|
|
||||||
return internalWorkspaceStore.updateWorkspace(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove workspace from store
|
|
||||||
* @param workspace workspace
|
|
||||||
*/
|
|
||||||
removeWorkspace(workspace: Workspace) {
|
|
||||||
return internalWorkspaceStore.removeWorkspace(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove workspace by id
|
|
||||||
* @param id workspace
|
|
||||||
*/
|
|
||||||
removeWorkspaceById(id: WorkspaceId) {
|
|
||||||
return internalWorkspaceStore.removeWorkspaceById(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const workspaceStore = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
@ -31,7 +31,6 @@ jest.mock("request-promise-native");
|
|||||||
|
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { getFreePort } from "../port";
|
import { getFreePort } from "../port";
|
||||||
@ -81,8 +80,7 @@ describe("create clusters", () => {
|
|||||||
c = new Cluster({
|
c = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -162,8 +160,7 @@ describe("create clusters", () => {
|
|||||||
}({
|
}({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await c.init(port);
|
await c.init(port);
|
||||||
|
|||||||
@ -26,7 +26,6 @@ jest.mock("winston", () => ({
|
|||||||
import { KubeconfigManager } from "../kubeconfig-manager";
|
import { KubeconfigManager } from "../kubeconfig-manager";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { getFreePort } from "../port";
|
import { getFreePort } from "../port";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
@ -77,8 +76,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
const contextHandler = new ContextHandler(cluster);
|
const contextHandler = new ContextHandler(cluster);
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
@ -98,8 +96,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
const contextHandler = new ContextHandler(cluster);
|
const contextHandler = new ContextHandler(cluster);
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|||||||
32
src/main/catalog-pusher.ts
Normal file
32
src/main/catalog-pusher.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { autorun, toJS } from "mobx";
|
||||||
|
import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc";
|
||||||
|
import { CatalogEntityRegistry} from "../common/catalog-entity-registry";
|
||||||
|
import "../common/catalog-entities/kubernetes-cluster";
|
||||||
|
|
||||||
|
export class CatalogPusher {
|
||||||
|
static init(catalog: CatalogEntityRegistry) {
|
||||||
|
new CatalogPusher(catalog).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(private catalog: CatalogEntityRegistry) {}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const disposers: { (): void; }[] = [];
|
||||||
|
|
||||||
|
disposers.push(autorun(() => {
|
||||||
|
this.broadcast();
|
||||||
|
}));
|
||||||
|
|
||||||
|
const listener = subscribeToBroadcast("catalog:broadcast", () => {
|
||||||
|
this.broadcast();
|
||||||
|
});
|
||||||
|
|
||||||
|
disposers.push(() => unsubscribeFromBroadcast("catalog:broadcast", listener));
|
||||||
|
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast() {
|
||||||
|
broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,25 @@
|
|||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { autorun, reaction } from "mobx";
|
import { action, autorun, observable, reaction, toJS } from "mobx";
|
||||||
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
|
import { CatalogEntity } from "../common/catalog-entity";
|
||||||
|
import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
|
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||||
|
|
||||||
|
const clusterOwnerRef = "ClusterManager";
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
|
@observable.deep catalogSource: CatalogEntity[] = [];
|
||||||
|
|
||||||
constructor(public readonly port: number) {
|
constructor(public readonly port: number) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource);
|
||||||
// auto-init clusters
|
// auto-init clusters
|
||||||
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
||||||
clusters.forEach((cluster) => {
|
clusters.forEach((cluster) => {
|
||||||
@ -19,8 +28,18 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster.init(port);
|
cluster.init(port);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}, { fireImmediately: true });
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
|
reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => {
|
||||||
|
this.updateCatalogSource(clusterStore.enabledClustersList);
|
||||||
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
|
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
||||||
|
this.syncClustersFromCatalog(entities);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
||||||
@ -40,6 +59,90 @@ export class ClusterManager extends Singleton {
|
|||||||
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
|
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action protected updateCatalogSource(clusters: Cluster[]) {
|
||||||
|
this.catalogSource.forEach((entity, index) => {
|
||||||
|
const clusterIndex = clusters.findIndex((cluster) => entity.metadata.uid === cluster.id);
|
||||||
|
|
||||||
|
if (clusterIndex === -1) {
|
||||||
|
this.catalogSource.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clusters.filter((c) => !c.ownerRef).forEach((cluster) => {
|
||||||
|
const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id);
|
||||||
|
const newEntity = this.catalogEntityFromCluster(cluster);
|
||||||
|
|
||||||
|
if (entityIndex === -1) {
|
||||||
|
this.catalogSource.push(newEntity);
|
||||||
|
} else {
|
||||||
|
const oldEntity = this.catalogSource[entityIndex];
|
||||||
|
|
||||||
|
newEntity.status.phase = cluster.disconnected ? "disconnected" : "connected";
|
||||||
|
newEntity.status.active = !cluster.disconnected;
|
||||||
|
newEntity.metadata.labels = {
|
||||||
|
...newEntity.metadata.labels,
|
||||||
|
...oldEntity.metadata.labels
|
||||||
|
};
|
||||||
|
this.catalogSource.splice(entityIndex, 1, newEntity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||||
|
entities.filter((entity) => entity.metadata.source !== "local").forEach((entity: KubernetesCluster) => {
|
||||||
|
const cluster = clusterStore.getById(entity.metadata.uid);
|
||||||
|
|
||||||
|
if (!cluster) {
|
||||||
|
clusterStore.addCluster({
|
||||||
|
id: entity.metadata.uid,
|
||||||
|
enabled: true,
|
||||||
|
ownerRef: clusterOwnerRef,
|
||||||
|
preferences: {
|
||||||
|
clusterName: entity.metadata.name
|
||||||
|
},
|
||||||
|
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||||
|
contextName: entity.spec.kubeconfigContext
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cluster.enabled = true;
|
||||||
|
if (!cluster.ownerRef) cluster.ownerRef = clusterOwnerRef;
|
||||||
|
cluster.preferences.clusterName = entity.metadata.name;
|
||||||
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
|
|
||||||
|
entity.status = {
|
||||||
|
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||||
|
active: !cluster.disconnected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected catalogEntityFromCluster(cluster: Cluster) {
|
||||||
|
return new KubernetesCluster(toJS({
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: cluster.id,
|
||||||
|
name: cluster.name,
|
||||||
|
source: "local",
|
||||||
|
labels: {
|
||||||
|
"distro": (cluster.metadata["distribution"] || "unknown").toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
|
kubeconfigContext: cluster.contextName
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||||
|
reason: "",
|
||||||
|
message: "",
|
||||||
|
active: !cluster.disconnected
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
protected onNetworkOffline() {
|
protected onNetworkOffline() {
|
||||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
logger.info("[CLUSTER-MANAGER]: network is offline");
|
||||||
clusterStore.enabledClustersList.forEach((cluster) => {
|
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store";
|
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store";
|
||||||
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||||
import type { WorkspaceId } from "../common/workspace-store";
|
|
||||||
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||||
@ -104,18 +103,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable contextName: string;
|
@observable contextName: string;
|
||||||
/**
|
|
||||||
* Workspace id
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable workspace: WorkspaceId;
|
|
||||||
/**
|
/**
|
||||||
* Path to kubeconfig
|
* Path to kubeconfig
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable kubeConfigPath: string;
|
@observable kubeConfigPath: string;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@observable workspace: string;
|
||||||
/**
|
/**
|
||||||
* Kubernetes API server URL
|
* Kubernetes API server URL
|
||||||
*
|
*
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import { registerFileProtocol } from "../common/register-protocol";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import { extensionsStore } from "../extensions/extensions-store";
|
import { extensionsStore } from "../extensions/extensions-store";
|
||||||
@ -30,6 +29,9 @@ import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
|||||||
import { bindBroadcastHandlers } from "../common/ipc";
|
import { bindBroadcastHandlers } from "../common/ipc";
|
||||||
import { startUpdateChecking } from "./app-updater";
|
import { startUpdateChecking } from "./app-updater";
|
||||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||||
|
import { CatalogPusher } from "./catalog-pusher";
|
||||||
|
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||||
|
import { hotbarStore } from "../common/hotbar-store";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -107,7 +109,7 @@ app.on("ready", async () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
workspaceStore.load(),
|
hotbarStore.load(),
|
||||||
extensionsStore.load(),
|
extensionsStore.load(),
|
||||||
filesystemProvisionerStore.load(),
|
filesystemProvisionerStore.load(),
|
||||||
]);
|
]);
|
||||||
@ -164,6 +166,7 @@ app.on("ready", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
||||||
|
CatalogPusher.init(catalogEntityRegistry);
|
||||||
startUpdateChecking();
|
startUpdateChecking();
|
||||||
LensProtocolRouterMain
|
LensProtocolRouterMain
|
||||||
.getInstance<LensProtocolRouterMain>()
|
.getInstance<LensProtocolRouterMain>()
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences.
|
|||||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||||
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
||||||
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
|
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
|
||||||
|
import { catalogURL } from "../renderer/components/+catalog/catalog.route";
|
||||||
import { menuRegistry } from "../extensions/registries/menu-registry";
|
import { menuRegistry } from "../extensions/registries/menu-registry";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
@ -175,6 +176,13 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
const viewMenu: MenuItemConstructorOptions = {
|
const viewMenu: MenuItemConstructorOptions = {
|
||||||
label: "View",
|
label: "View",
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Catalog",
|
||||||
|
accelerator: "Shift+CmdOrCtrl+C",
|
||||||
|
click() {
|
||||||
|
navigate(catalogURL());
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Command Palette...",
|
label: "Command Palette...",
|
||||||
accelerator: "Shift+CmdOrCtrl+P",
|
accelerator: "Shift+CmdOrCtrl+P",
|
||||||
|
|||||||
@ -5,10 +5,7 @@ import { autorun } from "mobx";
|
|||||||
import { showAbout } from "./menu";
|
import { showAbout } from "./menu";
|
||||||
import { checkForUpdates } from "./app-updater";
|
import { checkForUpdates } from "./app-updater";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
|
||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { isDevelopment, isWindows } from "../common/vars";
|
import { isDevelopment, isWindows } from "../common/vars";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
@ -78,28 +75,6 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to Preferences`, { error }));
|
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to Preferences`, { error }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Clusters",
|
|
||||||
submenu: workspaceStore.enabledWorkspacesList
|
|
||||||
.map(workspace => [workspace, clusterStore.getByWorkspaceId(workspace.id)] as const)
|
|
||||||
.map(([workspace, clusters]) => ({
|
|
||||||
label: workspace.name,
|
|
||||||
toolTip: workspace.description,
|
|
||||||
enabled: clusters.length > 0,
|
|
||||||
submenu: clusters.map(({ id: clusterId, name: label, online, workspace }) => ({
|
|
||||||
checked: online,
|
|
||||||
type: "checkbox",
|
|
||||||
label,
|
|
||||||
toolTip: clusterId,
|
|
||||||
click() {
|
|
||||||
workspaceStore.setActive(workspace);
|
|
||||||
windowManager
|
|
||||||
.navigate(clusterViewURL({ params: { clusterId } }))
|
|
||||||
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to cluster`, { clusterId, error }));
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Check for updates",
|
label: "Check for updates",
|
||||||
click() {
|
click() {
|
||||||
|
|||||||
28
src/migrations/hotbar-store/5.0.0-alpha.0.ts
Normal file
28
src/migrations/hotbar-store/5.0.0-alpha.0.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Cleans up a store that had the state related data stored
|
||||||
|
import { Hotbar } from "../../common/hotbar-store";
|
||||||
|
import { clusterStore } from "../../common/cluster-store";
|
||||||
|
import { migration } from "../migration-wrapper";
|
||||||
|
|
||||||
|
export default migration({
|
||||||
|
version: "5.0.0-alpha.0",
|
||||||
|
run(store) {
|
||||||
|
const hotbars: Hotbar[] = [];
|
||||||
|
|
||||||
|
clusterStore.enabledClustersList.forEach((cluster: any) => {
|
||||||
|
const name = cluster.workspace || "default";
|
||||||
|
let hotbar = hotbars.find((h) => h.name === name);
|
||||||
|
|
||||||
|
if (!hotbar) {
|
||||||
|
hotbar = { name, items: [] };
|
||||||
|
hotbars.push(hotbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
hotbar.items.push({
|
||||||
|
entity: { uid: cluster.id },
|
||||||
|
params: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
store.set("hotbars", hotbars);
|
||||||
|
}
|
||||||
|
});
|
||||||
7
src/migrations/hotbar-store/index.ts
Normal file
7
src/migrations/hotbar-store/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Hotbar store migrations
|
||||||
|
|
||||||
|
import version500alpha0 from "./5.0.0-alpha.0";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...version500alpha0,
|
||||||
|
};
|
||||||
135
src/renderer/api/__tests__/catalog-entity-registry.test.ts
Normal file
135
src/renderer/api/__tests__/catalog-entity-registry.test.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||||
|
import "../../../common/catalog-entities";
|
||||||
|
import { catalogCategoryRegistry } from "../../../common/catalog-category-registry";
|
||||||
|
|
||||||
|
describe("CatalogEntityRegistry", () => {
|
||||||
|
describe("updateItems", () => {
|
||||||
|
it("adds new catalog item", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "456",
|
||||||
|
name: "barbaz",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores unknown items", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "FooBar",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates existing items", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
expect(catalog.items[0].status.phase).toEqual("disconnected");
|
||||||
|
|
||||||
|
items[0].status.phase = "connected";
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
expect(catalog.items[0].status.phase).toEqual("connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes deleted items", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "456",
|
||||||
|
name: "barbaz",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
items.splice(0, 1);
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
expect(catalog.items[0].metadata.uid).toEqual("456");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -145,10 +145,10 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
|||||||
describe("Pods", () => {
|
describe("Pods", () => {
|
||||||
const podTests = [];
|
const podTests = [];
|
||||||
|
|
||||||
for (let r = 0; r < 10; r += 1) {
|
for (let r = 0; r < 3; r += 1) {
|
||||||
for (let d = 0; d < 10; d += 1) {
|
for (let d = 0; d < 3; d += 1) {
|
||||||
for (let ir = 0; ir < 10; ir += 1) {
|
for (let ir = 0; ir < 3; ir += 1) {
|
||||||
for (let id = 0; id < 10; id += 1) {
|
for (let id = 0; id < 3; id += 1) {
|
||||||
podTests.push([r, d, ir, id]);
|
podTests.push([r, d, ir, id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/renderer/api/catalog-entity-registry.ts
Normal file
61
src/renderer/api/catalog-entity-registry.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { action, observable } from "mobx";
|
||||||
|
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
|
||||||
|
import { CatalogCategory, CatalogEntity, CatalogEntityData } from "../../common/catalog-entity";
|
||||||
|
import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../common/catalog-category-registry";
|
||||||
|
import "../../common/catalog-entities";
|
||||||
|
|
||||||
|
export class CatalogEntityRegistry {
|
||||||
|
@observable protected _items: CatalogEntity[] = observable.array([], { deep: true });
|
||||||
|
|
||||||
|
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
subscribeToBroadcast("catalog:items", (ev, items: CatalogEntityData[]) => {
|
||||||
|
this.updateItems(items);
|
||||||
|
});
|
||||||
|
broadcastMessage("catalog:broadcast");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateItems(items: CatalogEntityData[]) {
|
||||||
|
this._items.forEach((item, index) => {
|
||||||
|
const foundIndex = items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid);
|
||||||
|
|
||||||
|
if (foundIndex === -1) {
|
||||||
|
this._items.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
items.forEach((data) => {
|
||||||
|
const item = this.categoryRegistry.getEntityForData(data);
|
||||||
|
|
||||||
|
if (!item) return; // invalid data
|
||||||
|
|
||||||
|
const index = this._items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
this._items.push(item);
|
||||||
|
} else {
|
||||||
|
this._items.splice(index, 1, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get items() {
|
||||||
|
return this._items;
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
|
const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||||
|
|
||||||
|
return items as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
|
||||||
|
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`);
|
||||||
|
const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind);
|
||||||
|
|
||||||
|
return items as T[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
12
src/renderer/api/catalog-entity.ts
Normal file
12
src/renderer/api/catalog-entity.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { navigate } from "../navigation";
|
||||||
|
import { commandRegistry } from "../../extensions/registries";
|
||||||
|
import { CatalogEntity } from "../../common/catalog-entity";
|
||||||
|
|
||||||
|
export { CatalogEntity, CatalogEntityData, CatalogEntityActionContext, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../common/catalog-entity";
|
||||||
|
|
||||||
|
export const catalogEntityRunContext = {
|
||||||
|
navigate: (url: string) => navigate(url),
|
||||||
|
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
||||||
|
commandRegistry.activeEntity = entity;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -62,7 +62,9 @@ export interface IKubeResourceList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeApiCluster {
|
export interface IKubeApiCluster {
|
||||||
id: string;
|
metadata: {
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor<T>): KubeApi<T> {
|
export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor<T>): KubeApi<T> {
|
||||||
@ -71,7 +73,7 @@ export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeC
|
|||||||
debug: isDevelopment,
|
debug: isDevelopment,
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Cluster-ID": cluster.id
|
"X-Cluster-ID": cluster.metadata.uid
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -10,11 +10,11 @@ import { clusterStore } from "../common/cluster-store";
|
|||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { delay } from "../common/utils";
|
import { delay } from "../common/utils";
|
||||||
import { isMac, isDevelopment } from "../common/vars";
|
import { isMac, isDevelopment } from "../common/vars";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
|
||||||
import * as LensExtensions from "../extensions/extension-api";
|
import * as LensExtensions from "../extensions/extension-api";
|
||||||
import { extensionDiscovery } from "../extensions/extension-discovery";
|
import { extensionDiscovery } from "../extensions/extension-discovery";
|
||||||
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 { hotbarStore } from "../common/hotbar-store";
|
||||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import { App } from "./components/app";
|
import { App } from "./components/app";
|
||||||
import { LensApp } from "./lens-app";
|
import { LensApp } from "./lens-app";
|
||||||
@ -56,7 +56,7 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
// preload common stores
|
// preload common stores
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
workspaceStore.load(),
|
hotbarStore.load(),
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
extensionsStore.load(),
|
extensionsStore.load(),
|
||||||
filesystemProvisionerStore.load(),
|
filesystemProvisionerStore.load(),
|
||||||
@ -65,7 +65,6 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
|
|
||||||
// Register additional store listeners
|
// Register additional store listeners
|
||||||
clusterStore.registerIpcListener();
|
clusterStore.registerIpcListener();
|
||||||
workspaceStore.registerIpcListener();
|
|
||||||
|
|
||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
if (App.init) {
|
if (App.init) {
|
||||||
@ -74,7 +73,6 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
window.addEventListener("message", (ev: MessageEvent) => {
|
window.addEventListener("message", (ev: MessageEvent) => {
|
||||||
if (ev.data === "teardown") {
|
if (ev.data === "teardown") {
|
||||||
userStore.unregisterIpcListener();
|
userStore.unregisterIpcListener();
|
||||||
workspaceStore.unregisterIpcListener();
|
|
||||||
clusterStore.unregisterIpcListener();
|
clusterStore.unregisterIpcListener();
|
||||||
unmountComponentAtNode(rootElem);
|
unmountComponentAtNode(rootElem);
|
||||||
window.location.href = "about:blank";
|
window.location.href = "about:blank";
|
||||||
|
|||||||
@ -12,11 +12,9 @@ import { Button } from "../button";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
||||||
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { userStore } from "../../../common/user-store";
|
import { userStore } from "../../../common/user-store";
|
||||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Tab, Tabs } from "../tabs";
|
import { Tab, Tabs } from "../tabs";
|
||||||
@ -24,6 +22,7 @@ import { ExecValidationNotFoundError } from "../../../common/custom-errors";
|
|||||||
import { appEventBus } from "../../../common/event-bus";
|
import { appEventBus } from "../../../common/event-bus";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
import { docsUrl } from "../../../common/vars";
|
import { docsUrl } from "../../../common/vars";
|
||||||
|
import { catalogURL } from "../+catalog";
|
||||||
|
|
||||||
enum KubeConfigSourceTab {
|
enum KubeConfigSourceTab {
|
||||||
FILE = "file",
|
FILE = "file",
|
||||||
@ -171,7 +170,6 @@ export class AddCluster extends React.Component {
|
|||||||
return {
|
return {
|
||||||
id: clusterId,
|
id: clusterId,
|
||||||
kubeConfigPath,
|
kubeConfigPath,
|
||||||
workspace: workspaceStore.currentWorkspaceId,
|
|
||||||
contextName: kubeConfig.currentContext,
|
contextName: kubeConfig.currentContext,
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: kubeConfig.currentContext,
|
clusterName: kubeConfig.currentContext,
|
||||||
@ -183,18 +181,11 @@ export class AddCluster extends React.Component {
|
|||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
clusterStore.addClusters(...newClusters);
|
clusterStore.addClusters(...newClusters);
|
||||||
|
|
||||||
if (newClusters.length === 1) {
|
Notifications.ok(
|
||||||
const clusterId = newClusters[0].id;
|
<>Successfully imported <b>{newClusters.length}</b> cluster(s)</>
|
||||||
|
);
|
||||||
|
|
||||||
clusterStore.setActive(clusterId);
|
navigate(catalogURL());
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
} else {
|
|
||||||
if (newClusters.length > 1) {
|
|
||||||
Notifications.ok(
|
|
||||||
<>Successfully imported <b>{newClusters.length}</b> cluster(s)</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.refreshContexts();
|
this.refreshContexts();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import { releaseURL } from "../+apps-releases";
|
|||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewHelmCharts",
|
id: "cluster.viewHelmCharts",
|
||||||
title: "Cluster: View Helm Charts",
|
title: "Cluster: View Helm Charts",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(helmChartsURL())
|
action: () => navigate(helmChartsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewHelmReleases",
|
id: "cluster.viewHelmReleases",
|
||||||
title: "Cluster: View Helm Releases",
|
title: "Cluster: View Helm Releases",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(releaseURL())
|
action: () => navigate(releaseURL())
|
||||||
});
|
});
|
||||||
|
|||||||
81
src/renderer/components/+catalog/catalog-entity.store.ts
Normal file
81
src/renderer/components/+catalog/catalog-entity.store.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
|
||||||
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
import { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
|
||||||
|
import { ItemObject, ItemStore } from "../../item.store";
|
||||||
|
import { autobind } from "../../utils";
|
||||||
|
import { CatalogCategory } from "../../../common/catalog-entity";
|
||||||
|
|
||||||
|
export class CatalogEntityItem implements ItemObject {
|
||||||
|
constructor(public entity: CatalogEntity) {}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.entity.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.entity.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.entity.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get phase() {
|
||||||
|
return this.entity.status.phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
get labels() {
|
||||||
|
const labels: string[] = [];
|
||||||
|
|
||||||
|
Object.keys(this.entity.metadata.labels).forEach((key) => {
|
||||||
|
const value = this.entity.metadata.labels[key];
|
||||||
|
|
||||||
|
labels.push(`${key}=${value}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
get source() {
|
||||||
|
return this.entity.metadata.source || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
onRun(ctx: CatalogEntityActionContext) {
|
||||||
|
this.entity.onRun(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async onContextMenuOpen(ctx: any) {
|
||||||
|
return this.entity.onContextMenuOpen(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
|
||||||
|
@observable activeCategory: CatalogCategory;
|
||||||
|
|
||||||
|
@computed get entities() {
|
||||||
|
if (!this.activeCategory) return [];
|
||||||
|
|
||||||
|
console.log("computing entities", this.activeCategory);
|
||||||
|
|
||||||
|
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch() {
|
||||||
|
const disposers: IReactionDisposer[] = [
|
||||||
|
reaction(() => this.entities, () => this.loadAll()),
|
||||||
|
reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100})
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => disposers.forEach((dispose) => dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAll() {
|
||||||
|
return this.loadItems(() => this.entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/renderer/components/+catalog/catalog.route.ts
Normal file
8
src/renderer/components/+catalog/catalog.route.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { RouteProps } from "react-router";
|
||||||
|
import { buildURL } from "../../../common/utils/buildUrl";
|
||||||
|
|
||||||
|
export const catalogRoute: RouteProps = {
|
||||||
|
path: "/catalog"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const catalogURL = buildURL(catalogRoute.path);
|
||||||
26
src/renderer/components/+catalog/catalog.scss
Normal file
26
src/renderer/components/+catalog/catalog.scss
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.CatalogPage {
|
||||||
|
--width: 100%;
|
||||||
|
--height: 100%;
|
||||||
|
--nav-column-width: 230px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.sidebarRegion {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentRegion {
|
||||||
|
.content {
|
||||||
|
padding: 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.TableCell.status {
|
||||||
|
&.connected {
|
||||||
|
color: var(--colorSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disconnected {
|
||||||
|
color: var(--halfGray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/renderer/components/+catalog/catalog.tsx
Normal file
195
src/renderer/components/+catalog/catalog.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import "./catalog.scss";
|
||||||
|
import React from "react";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import { ItemListLayout } from "../item-object-list";
|
||||||
|
import { observable, reaction } from "mobx";
|
||||||
|
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { kebabCase } from "lodash";
|
||||||
|
import { PageLayout } from "../layout/page-layout";
|
||||||
|
import { MenuItem, MenuActions } from "../menu";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||||
|
import { Badge } from "../badge";
|
||||||
|
import { hotbarStore } from "../../../common/hotbar-store";
|
||||||
|
import { addClusterURL } from "../+add-cluster";
|
||||||
|
import { autobind } from "../../utils";
|
||||||
|
import { Notifications } from "../notifications";
|
||||||
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
|
import { Tab, Tabs } from "../tabs";
|
||||||
|
import { catalogCategoryRegistry } from "../../../common/catalog-category-registry";
|
||||||
|
|
||||||
|
enum sortBy {
|
||||||
|
name = "name",
|
||||||
|
source = "source",
|
||||||
|
status = "status"
|
||||||
|
}
|
||||||
|
@observer
|
||||||
|
export class Catalog extends React.Component {
|
||||||
|
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||||
|
@observable.deep private contextMenu: CatalogEntityContextMenuContext;
|
||||||
|
@observable activeTab: string;
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
this.contextMenu = {
|
||||||
|
menuItems: [],
|
||||||
|
navigate: (url: string) => navigate(url)
|
||||||
|
};
|
||||||
|
this.catalogEntityStore = new CatalogEntityStore();
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
this.catalogEntityStore.watch(),
|
||||||
|
reaction(() => catalogCategoryRegistry.items, (items) => {
|
||||||
|
if (!this.activeTab && items.length > 0) {
|
||||||
|
this.activeTab = items[0].getId();
|
||||||
|
this.catalogEntityStore.activeCategory = items[0];
|
||||||
|
}
|
||||||
|
}, { fireImmediately: true })
|
||||||
|
]);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.catalogEntityStore.items.length === 0) {
|
||||||
|
Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, {
|
||||||
|
timeout: 30_000,
|
||||||
|
id: "catalog-welcome"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 2_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHotbar(item: CatalogEntityItem) {
|
||||||
|
const hotbar = hotbarStore.getByName("default"); // FIXME
|
||||||
|
|
||||||
|
if (!hotbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hotbar.items.push({ entity: { uid: item.id }});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromHotbar(item: CatalogEntityItem) {
|
||||||
|
const hotbar = hotbarStore.getByName("default"); // FIXME
|
||||||
|
|
||||||
|
if (!hotbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hotbar.items = hotbar.items.filter((i) => i.entity.uid !== item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetails(item: CatalogEntityItem) {
|
||||||
|
item.onRun(catalogEntityRunContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
|
if (menuItem.confirm) {
|
||||||
|
ConfirmDialog.open({
|
||||||
|
okButtonProps: {
|
||||||
|
primary: false,
|
||||||
|
accent: true,
|
||||||
|
},
|
||||||
|
ok: () => {
|
||||||
|
menuItem.onClick();
|
||||||
|
},
|
||||||
|
message: menuItem.confirm.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
menuItem.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get categories() {
|
||||||
|
return catalogCategoryRegistry.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabChange = (tabId: string) => {
|
||||||
|
this.activeTab = tabId;
|
||||||
|
|
||||||
|
const activeCategory = this.categories.find((category) => category.getId() === tabId);
|
||||||
|
|
||||||
|
if (activeCategory) {
|
||||||
|
this.catalogEntityStore.activeCategory = activeCategory;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderNavigation() {
|
||||||
|
return (
|
||||||
|
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
||||||
|
<div className="header">Catalog</div>
|
||||||
|
{ this.categories.map((category, index) => {
|
||||||
|
return <Tab value={category.getId()} key={index} label={category.metadata.name} data-testid={`${category.getId()}-tab`} />;
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
renderItemMenu(item: CatalogEntityItem) {
|
||||||
|
const onOpen = async () => {
|
||||||
|
await item.onContextMenuOpen(this.contextMenu);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions onOpen={() => onOpen()}>
|
||||||
|
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
|
||||||
|
<Icon material="add" small interactive={true} title="Add to hotbar"/> Add to Hotbar
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(item) }>
|
||||||
|
<Icon material="clear" small interactive={true} title="Remove from hotbar"/> Remove from Hotbar
|
||||||
|
</MenuItem>
|
||||||
|
{ this.contextMenu.menuItems.map((menuItem, index) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||||
|
<Icon material={menuItem.icon} small interactive={true} title={menuItem.title}/> {menuItem.title}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.catalogEntityStore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout
|
||||||
|
className="CatalogPage"
|
||||||
|
navigation={this.renderNavigation()}
|
||||||
|
provideBackButtonNavigation={false}
|
||||||
|
contentGaps={false}>
|
||||||
|
<ItemListLayout
|
||||||
|
isClusterScoped
|
||||||
|
isSearchable={true}
|
||||||
|
isSelectable={false}
|
||||||
|
className="CatalogItemList"
|
||||||
|
store={this.catalogEntityStore}
|
||||||
|
tableId="catalog-items"
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
||||||
|
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
||||||
|
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
||||||
|
}}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Source", className: "source" },
|
||||||
|
{ title: "Labels", className: "labels" },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
item.name,
|
||||||
|
item.source,
|
||||||
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
|
]}
|
||||||
|
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||||
|
renderItemMenu={this.renderItemMenu}
|
||||||
|
addRemoveButtons={{
|
||||||
|
addTooltip: "Add Kubernetes Cluster",
|
||||||
|
onAdd: () => navigate(addClusterURL()),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/renderer/components/+catalog/index.tsx
Normal file
2
src/renderer/components/+catalog/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./catalog.route";
|
||||||
|
export * from "./catalog";
|
||||||
@ -12,5 +12,5 @@ commandRegistry.add({
|
|||||||
clusterId: clusterStore.active.id
|
clusterId: clusterStore.active.id
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
isActive: (context) => !!context.cluster
|
isActive: (context) => !!context.entity
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,12 +4,9 @@ import React from "react";
|
|||||||
import { reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { observer, disposeOnUnmount } from "mobx-react";
|
import { observer, disposeOnUnmount } from "mobx-react";
|
||||||
import { Features } from "./features";
|
|
||||||
import { Removal } from "./removal";
|
|
||||||
import { Status } from "./status";
|
import { Status } from "./status";
|
||||||
import { General } from "./general";
|
import { General } from "./general";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
|
||||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
@ -58,7 +55,6 @@ export class ClusterSettings extends React.Component<Props> {
|
|||||||
if (!cluster) return null;
|
if (!cluster) return null;
|
||||||
const header = (
|
const header = (
|
||||||
<>
|
<>
|
||||||
<ClusterIcon cluster={cluster} showErrors={false} showTooltip={false}/>
|
|
||||||
<h2>{cluster.preferences.clusterName}</h2>
|
<h2>{cluster.preferences.clusterName}</h2>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -67,8 +63,6 @@ export class ClusterSettings extends React.Component<Props> {
|
|||||||
<PageLayout className="ClusterSettings" header={header} showOnTop={true}>
|
<PageLayout className="ClusterSettings" header={header} showOnTop={true}>
|
||||||
<Status cluster={cluster}></Status>
|
<Status cluster={cluster}></Status>
|
||||||
<General cluster={cluster}></General>
|
<General cluster={cluster}></General>
|
||||||
<Features cluster={cluster}></Features>
|
|
||||||
<Removal cluster={cluster}></Removal>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { SubTitle } from "../../layout/sub-title";
|
|
||||||
import { ClusterIcon } from "../../cluster-icon";
|
|
||||||
|
|
||||||
enum GeneralInputStatus {
|
|
||||||
CLEAN = "clean",
|
|
||||||
ERROR = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClusterIconSetting extends React.Component<Props> {
|
|
||||||
@observable status = GeneralInputStatus.CLEAN;
|
|
||||||
@observable errorText?: string;
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
async onIconPick([file]: File[]) {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (file) {
|
|
||||||
const buf = Buffer.from(await file.arrayBuffer());
|
|
||||||
|
|
||||||
cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`;
|
|
||||||
} else {
|
|
||||||
// this has to be done as a seperate branch (and not always) because `cluster`
|
|
||||||
// is observable and triggers an update loop.
|
|
||||||
cluster.preferences.icon = undefined;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.errorText = e.toString();
|
|
||||||
this.status = GeneralInputStatus.ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getClearButton() {
|
|
||||||
if (this.props.cluster.preferences.icon) {
|
|
||||||
return <Button tooltip="Revert back to default icon" accent onClick={() => this.onIconPick([])}>Clear</Button>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const label = (
|
|
||||||
<>
|
|
||||||
<ClusterIcon
|
|
||||||
cluster={this.props.cluster}
|
|
||||||
showErrors={false}
|
|
||||||
showTooltip={false}
|
|
||||||
/>
|
|
||||||
{"Browse for new icon..."}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SubTitle title="Cluster Icon" />
|
|
||||||
<p>Define cluster icon. By default automatically generated.</p>
|
|
||||||
<div className="file-loader">
|
|
||||||
<FilePicker
|
|
||||||
accept="image/*"
|
|
||||||
label={label}
|
|
||||||
onOverSizeLimit={OverSizeLimitStyle.FILTER}
|
|
||||||
handler={this.onIconPick}
|
|
||||||
/>
|
|
||||||
{this.getClearButton()}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { workspaceStore } from "../../../../common/workspace-store";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Select } from "../../../components/select";
|
|
||||||
import { SubTitle } from "../../layout/sub-title";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClusterWorkspaceSetting extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SubTitle title="Cluster Workspace"/>
|
|
||||||
<p>
|
|
||||||
Define cluster workspace.
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
value={this.props.cluster.workspace}
|
|
||||||
onChange={({value}) => this.props.cluster.workspace = value}
|
|
||||||
options={workspaceStore.enabledWorkspacesList.map(w =>
|
|
||||||
({value: w.id, label: w.name})
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observable, reaction, comparer } from "mobx";
|
|
||||||
import { observer, disposeOnUnmount } from "mobx-react";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { Notifications } from "../../notifications";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { ClusterFeature } from "../../../../extensions/cluster-feature";
|
|
||||||
import { interval } from "../../../utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster
|
|
||||||
feature: ClusterFeature
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class InstallFeature extends React.Component<Props> {
|
|
||||||
@observable loading = false;
|
|
||||||
@observable message = "";
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const feature = this.props.feature;
|
|
||||||
const cluster = this.props.cluster;
|
|
||||||
const statusUpdate = interval(20, () => {
|
|
||||||
feature.updateStatus(cluster);
|
|
||||||
});
|
|
||||||
|
|
||||||
statusUpdate.start(true);
|
|
||||||
|
|
||||||
disposeOnUnmount(this, () => {
|
|
||||||
statusUpdate.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
disposeOnUnmount(this,
|
|
||||||
reaction(() => feature.status.installed, () => {
|
|
||||||
this.loading = false;
|
|
||||||
this.message = "";
|
|
||||||
}, { equals: comparer.structural })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActionButtons() {
|
|
||||||
const { cluster, feature } = this.props;
|
|
||||||
const disabled = !cluster.isAdmin || this.loading;
|
|
||||||
const loadingIcon = this.loading ? <Spinner/> : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gaps align-center">
|
|
||||||
{feature.status.canUpgrade &&
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.runAction(() =>
|
|
||||||
feature.upgrade(cluster)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{feature.status.installed &&
|
|
||||||
<Button
|
|
||||||
accent
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.runAction(async () => {
|
|
||||||
this.message = "Uninstalling feature ...";
|
|
||||||
feature.uninstall(cluster);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Uninstall
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{!feature.status.installed && !feature.status.canUpgrade &&
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.runAction(async () =>{
|
|
||||||
this.message = "Installing feature ...";
|
|
||||||
feature.install(cluster);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{loadingIcon}
|
|
||||||
{!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>}
|
|
||||||
{cluster.isAdmin && this.loading && this.message !== "" && <span className='admin-note'>{this.message}</span>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction(action: () => Promise<any>): () => Promise<void> {
|
|
||||||
return async () => {
|
|
||||||
try {
|
|
||||||
this.loading = true;
|
|
||||||
await action();
|
|
||||||
} catch (err) {
|
|
||||||
Notifications.error(err.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.props.children}
|
|
||||||
<div className="button-area">{this.getActionButtons()}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { InstallFeature } from "./components/install-feature";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { clusterFeatureRegistry } from "../../../extensions/registries/cluster-feature-registry";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Features extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Features</h2>
|
|
||||||
{
|
|
||||||
clusterFeatureRegistry
|
|
||||||
.getItems()
|
|
||||||
.map((f) => (
|
|
||||||
<InstallFeature key={f.title} cluster={cluster} feature={f.feature}>
|
|
||||||
<>
|
|
||||||
<SubTitle title={f.title} />
|
|
||||||
<p><f.components.Description /></p>
|
|
||||||
</>
|
|
||||||
</InstallFeature>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
||||||
import { ClusterWorkspaceSetting } from "./components/cluster-workspace-setting";
|
|
||||||
import { ClusterIconSetting } from "./components/cluster-icon-setting";
|
|
||||||
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
||||||
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||||
@ -19,8 +17,6 @@ export class General extends React.Component<Props> {
|
|||||||
return <div>
|
return <div>
|
||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
<ClusterNameSetting cluster={this.props.cluster} />
|
<ClusterNameSetting cluster={this.props.cluster} />
|
||||||
<ClusterWorkspaceSetting cluster={this.props.cluster} />
|
|
||||||
<ClusterIconSetting cluster={this.props.cluster} />
|
|
||||||
<ClusterProxySetting cluster={this.props.cluster} />
|
<ClusterProxySetting cluster={this.props.cluster} />
|
||||||
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
||||||
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { RemoveClusterButton } from "./components/remove-cluster-button";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Removal extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Removal</h2>
|
|
||||||
<RemoveClusterButton cluster={cluster} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,41 +10,41 @@ import { pdbURL } from "../+config-pod-disruption-budgets";
|
|||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewConfigMaps",
|
id: "cluster.viewConfigMaps",
|
||||||
title: "Cluster: View ConfigMaps",
|
title: "Cluster: View ConfigMaps",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(configMapsURL())
|
action: () => navigate(configMapsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewSecrets",
|
id: "cluster.viewSecrets",
|
||||||
title: "Cluster: View Secrets",
|
title: "Cluster: View Secrets",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(secretsURL())
|
action: () => navigate(secretsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewResourceQuotas",
|
id: "cluster.viewResourceQuotas",
|
||||||
title: "Cluster: View ResourceQuotas",
|
title: "Cluster: View ResourceQuotas",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(resourceQuotaURL())
|
action: () => navigate(resourceQuotaURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewLimitRanges",
|
id: "cluster.viewLimitRanges",
|
||||||
title: "Cluster: View LimitRanges",
|
title: "Cluster: View LimitRanges",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(limitRangeURL())
|
action: () => navigate(limitRangeURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewHorizontalPodAutoscalers",
|
id: "cluster.viewHorizontalPodAutoscalers",
|
||||||
title: "Cluster: View HorizontalPodAutoscalers (HPA)",
|
title: "Cluster: View HorizontalPodAutoscalers (HPA)",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(hpaURL())
|
action: () => navigate(hpaURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewPodDisruptionBudget",
|
id: "cluster.viewPodDisruptionBudget",
|
||||||
title: "Cluster: View PodDisruptionBudgets",
|
title: "Cluster: View PodDisruptionBudgets",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(pdbURL())
|
action: () => navigate(pdbURL())
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./landing-page.route";
|
|
||||||
export * from "./landing-page";
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import type { RouteProps } from "react-router";
|
|
||||||
import { buildURL } from "../../../common/utils/buildUrl";
|
|
||||||
|
|
||||||
export const landingRoute: RouteProps = {
|
|
||||||
path: "/landing"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const landingURL = buildURL(landingRoute.path);
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
.PageLayout.LandingOverview {
|
|
||||||
--width: 100%;
|
|
||||||
--height: 100%;
|
|
||||||
text-align: center;
|
|
||||||
bottom: 22px; // Making bottom bar visible
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
.content {
|
|
||||||
margin: unset;
|
|
||||||
max-width: unset;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import "./landing-page.scss";
|
|
||||||
import React from "react";
|
|
||||||
import { computed, observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { WorkspaceOverview } from "./workspace-overview";
|
|
||||||
import { PageLayout } from "../layout/page-layout";
|
|
||||||
import { Notifications } from "../notifications";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class LandingPage extends React.Component {
|
|
||||||
@observable showHint = true;
|
|
||||||
|
|
||||||
@computed
|
|
||||||
get clusters() {
|
|
||||||
return clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const noClustersInScope = !this.clusters.length;
|
|
||||||
const showStartupHint = this.showHint;
|
|
||||||
|
|
||||||
if (showStartupHint && noClustersInScope) {
|
|
||||||
Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, {
|
|
||||||
timeout: 30_000,
|
|
||||||
id: "landing-welcome"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const showBackButton = this.clusters.length > 0;
|
|
||||||
const header = <><Icon svg="logo-lens" big /> <h2>{workspaceStore.currentWorkspace.name}</h2></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>
|
|
||||||
<WorkspaceOverview />
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { ClusterItem, WorkspaceClusterStore } from "./workspace-cluster.store";
|
|
||||||
import { autobind, cssNames } from "../../utils";
|
|
||||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
|
||||||
import { MenuItem } from "../menu";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { Workspace } from "../../../common/workspace-store";
|
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
|
|
||||||
interface Props extends MenuActionsProps {
|
|
||||||
clusterItem: ClusterItem;
|
|
||||||
workspace: Workspace;
|
|
||||||
workspaceClusterStore: WorkspaceClusterStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceClusterMenu extends React.Component<Props> {
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
remove() {
|
|
||||||
const { clusterItem, workspaceClusterStore } = this.props;
|
|
||||||
|
|
||||||
return workspaceClusterStore.remove(clusterItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
gotoSettings() {
|
|
||||||
const { clusterItem } = this.props;
|
|
||||||
|
|
||||||
navigate(clusterSettingsURL({
|
|
||||||
params: {
|
|
||||||
clusterId: clusterItem.id
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
renderRemoveMessage() {
|
|
||||||
const { clusterItem, workspace } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p>Remove cluster <b>{clusterItem.name}</b> from workspace <b>{workspace.name}</b>?</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
renderContent() {
|
|
||||||
const { toolbar } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MenuItem onClick={this.gotoSettings}>
|
|
||||||
<Icon material="settings" interactive={toolbar} title="Settings"/>
|
|
||||||
<span className="title">Settings</span>
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { clusterItem: { cluster: { isManaged } }, className, ...menuProps } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuActions
|
|
||||||
{...menuProps}
|
|
||||||
className={cssNames("WorkspaceClusterMenu", className)}
|
|
||||||
removeAction={isManaged ? null : this.remove}
|
|
||||||
removeConfirmationMessage={this.renderRemoveMessage}
|
|
||||||
>
|
|
||||||
{this.renderContent()}
|
|
||||||
</MenuActions>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import { WorkspaceId } from "../../../common/workspace-store";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
|
||||||
import { ItemObject, ItemStore } from "../../item.store";
|
|
||||||
import { autobind } from "../../utils";
|
|
||||||
import { computed, reaction } from "mobx";
|
|
||||||
|
|
||||||
export class ClusterItem implements ItemObject {
|
|
||||||
constructor(public cluster: Cluster) {}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.cluster.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get distribution() {
|
|
||||||
return this.cluster.metadata?.distribution?.toString() ?? "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
get version() {
|
|
||||||
return this.cluster.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
get connectionStatus() {
|
|
||||||
return this.cluster.online ? "connected" : "disconnected";
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get id() {
|
|
||||||
return this.cluster.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
get clusterId() {
|
|
||||||
return this.cluster.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** an ItemStore of the clusters belonging to a given workspace */
|
|
||||||
@autobind()
|
|
||||||
export class WorkspaceClusterStore extends ItemStore<ClusterItem> {
|
|
||||||
|
|
||||||
workspaceId: WorkspaceId;
|
|
||||||
|
|
||||||
constructor(workspaceId: WorkspaceId) {
|
|
||||||
super();
|
|
||||||
this.workspaceId = workspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get clusters(): ClusterItem[] {
|
|
||||||
return clusterStore
|
|
||||||
.getByWorkspaceId(this.workspaceId)
|
|
||||||
.filter(cluster => cluster.enabled)
|
|
||||||
.map(cluster => new ClusterItem(cluster));
|
|
||||||
}
|
|
||||||
|
|
||||||
watch() {
|
|
||||||
return reaction(() => this.clusters, () => this.loadAll(), {
|
|
||||||
fireImmediately: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAll() {
|
|
||||||
return this.loadItems(() => this.clusters);
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(clusterItem: ClusterItem) {
|
|
||||||
const { cluster: { isManaged, id: clusterId }} = clusterItem;
|
|
||||||
|
|
||||||
if (!isManaged) {
|
|
||||||
return super.removeItem(clusterItem, () => clusterStore.removeById(clusterId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
.WorkspaceOverview {
|
|
||||||
max-height: 50%;
|
|
||||||
.Table {
|
|
||||||
padding-bottom: 60px;
|
|
||||||
}
|
|
||||||
.TableCell {
|
|
||||||
display: flex;
|
|
||||||
align-items: left;
|
|
||||||
|
|
||||||
&.cluster-icon {
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 0.2;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.connected {
|
|
||||||
color: var(--colorSuccess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableCell.status {
|
|
||||||
flex: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableCell.distribution {
|
|
||||||
flex: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableCell.version {
|
|
||||||
flex: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import "./workspace-overview.scss";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
|
||||||
import { ClusterItem, WorkspaceClusterStore } from "./workspace-cluster.store";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
|
||||||
import { WorkspaceClusterMenu } from "./workspace-cluster-menu";
|
|
||||||
import { kebabCase } from "lodash";
|
|
||||||
import { addClusterURL } from "../+add-cluster";
|
|
||||||
import { IReactionDisposer, observable, reaction } from "mobx";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
|
|
||||||
enum sortBy {
|
|
||||||
name = "name",
|
|
||||||
distribution = "distribution",
|
|
||||||
version = "version",
|
|
||||||
online = "online"
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class WorkspaceOverview extends Component {
|
|
||||||
@observable private workspaceClusterStore?: WorkspaceClusterStore;
|
|
||||||
|
|
||||||
disposeWorkspaceWatch: IReactionDisposer;
|
|
||||||
disposeClustersWatch: IReactionDisposer;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.disposeWorkspaceWatch = reaction(() => workspaceStore.currentWorkspaceId, workspaceId => {
|
|
||||||
this.workspaceClusterStore = new WorkspaceClusterStore(workspaceId);
|
|
||||||
this.disposeClustersWatch?.();
|
|
||||||
this.disposeClustersWatch = this.workspaceClusterStore.watch();
|
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.disposeWorkspaceWatch?.();
|
|
||||||
this.disposeClustersWatch?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
showCluster = ({ clusterId }: ClusterItem) => {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { workspaceClusterStore } = this;
|
|
||||||
|
|
||||||
if (!workspaceClusterStore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemListLayout
|
|
||||||
renderHeaderTitle="Clusters"
|
|
||||||
isClusterScoped
|
|
||||||
isSearchable={false}
|
|
||||||
isSelectable={false}
|
|
||||||
className="WorkspaceOverview"
|
|
||||||
store={workspaceClusterStore}
|
|
||||||
sortingCallbacks={{
|
|
||||||
[sortBy.name]: (item: ClusterItem) => item.name,
|
|
||||||
[sortBy.distribution]: (item: ClusterItem) => item.distribution,
|
|
||||||
[sortBy.version]: (item: ClusterItem) => item.version,
|
|
||||||
[sortBy.online]: (item: ClusterItem) => item.connectionStatus,
|
|
||||||
}}
|
|
||||||
renderTableHeader={[
|
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
|
||||||
{ title: "Distribution", className: "distribution", sortBy: sortBy.distribution },
|
|
||||||
{ title: "Version", className: "version", sortBy: sortBy.version },
|
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.online },
|
|
||||||
]}
|
|
||||||
renderTableContents={(item: ClusterItem) => [
|
|
||||||
item.name,
|
|
||||||
item.distribution,
|
|
||||||
item.version,
|
|
||||||
{ title: item.connectionStatus, className: kebabCase(item.connectionStatus) }
|
|
||||||
]}
|
|
||||||
onDetails={this.showCluster}
|
|
||||||
addRemoveButtons={{
|
|
||||||
addTooltip: "Add Cluster",
|
|
||||||
onAdd: () => navigate(addClusterURL()),
|
|
||||||
}}
|
|
||||||
renderItemMenu={(clusterItem: ClusterItem) => (
|
|
||||||
<WorkspaceClusterMenu clusterItem={clusterItem} workspace={workspaceStore.currentWorkspace} workspaceClusterStore={workspaceClusterStore}/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,27 +8,27 @@ import { networkPoliciesURL } from "../+network-policies";
|
|||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewServices",
|
id: "cluster.viewServices",
|
||||||
title: "Cluster: View Services",
|
title: "Cluster: View Services",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(servicesURL())
|
action: () => navigate(servicesURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewEndpoints",
|
id: "cluster.viewEndpoints",
|
||||||
title: "Cluster: View Endpoints",
|
title: "Cluster: View Endpoints",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(endpointURL())
|
action: () => navigate(endpointURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewIngresses",
|
id: "cluster.viewIngresses",
|
||||||
title: "Cluster: View Ingresses",
|
title: "Cluster: View Ingresses",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(ingressURL())
|
action: () => navigate(ingressURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewNetworkPolicies",
|
id: "cluster.viewNetworkPolicies",
|
||||||
title: "Cluster: View NetworkPolicies",
|
title: "Cluster: View NetworkPolicies",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(networkPoliciesURL())
|
action: () => navigate(networkPoliciesURL())
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,6 @@ import { nodesURL } from "./nodes.route";
|
|||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewNodes",
|
id: "cluster.viewNodes",
|
||||||
title: "Cluster: View Nodes",
|
title: "Cluster: View Nodes",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(nodesURL())
|
action: () => navigate(nodesURL())
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,41 +5,41 @@ import { cronJobsURL, daemonSetsURL, deploymentsURL, jobsURL, podsURL, statefulS
|
|||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewPods",
|
id: "cluster.viewPods",
|
||||||
title: "Cluster: View Pods",
|
title: "Cluster: View Pods",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(podsURL())
|
action: () => navigate(podsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewDeployments",
|
id: "cluster.viewDeployments",
|
||||||
title: "Cluster: View Deployments",
|
title: "Cluster: View Deployments",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(deploymentsURL())
|
action: () => navigate(deploymentsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewDaemonSets",
|
id: "cluster.viewDaemonSets",
|
||||||
title: "Cluster: View DaemonSets",
|
title: "Cluster: View DaemonSets",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(daemonSetsURL())
|
action: () => navigate(daemonSetsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewStatefulSets",
|
id: "cluster.viewStatefulSets",
|
||||||
title: "Cluster: View StatefulSets",
|
title: "Cluster: View StatefulSets",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(statefulSetsURL())
|
action: () => navigate(statefulSetsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewJobs",
|
id: "cluster.viewJobs",
|
||||||
title: "Cluster: View Jobs",
|
title: "Cluster: View Jobs",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(jobsURL())
|
action: () => navigate(jobsURL())
|
||||||
});
|
});
|
||||||
|
|
||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.viewCronJobs",
|
id: "cluster.viewCronJobs",
|
||||||
title: "Cluster: View CronJobs",
|
title: "Cluster: View CronJobs",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => navigate(cronJobsURL())
|
action: () => navigate(cronJobsURL())
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { Workspace, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Input, InputValidator } from "../input";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { landingURL } from "../+landing-page";
|
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
|
||||||
|
|
||||||
const uniqueWorkspaceName: InputValidator = {
|
|
||||||
condition: ({ required }) => required,
|
|
||||||
message: () => `Workspace with this name already exists`,
|
|
||||||
validate: value => !workspaceStore.getByName(value),
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class AddWorkspace extends React.Component {
|
|
||||||
onSubmit(name: string) {
|
|
||||||
if (!name.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workspace = workspaceStore.addWorkspace(new Workspace({
|
|
||||||
id: uuid(),
|
|
||||||
name
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!workspace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceStore.setActive(workspace.id);
|
|
||||||
clusterStore.setActive(null);
|
|
||||||
navigate(landingURL());
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
placeholder="Workspace name"
|
|
||||||
autoFocus={true}
|
|
||||||
theme="round-black"
|
|
||||||
data-test-id="command-palette-workspace-add-name"
|
|
||||||
validators={[uniqueWorkspaceName]}
|
|
||||||
onSubmit={(v) => this.onSubmit(v)}
|
|
||||||
dirty={true}
|
|
||||||
showValidationLine={true} />
|
|
||||||
<small className="hint">
|
|
||||||
Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel)
|
|
||||||
</small>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.addWorkspace",
|
|
||||||
title: "Workspace: Add workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<AddWorkspace />)
|
|
||||||
});
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Input, InputValidator } from "../input";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
|
|
||||||
const validateWorkspaceName: InputValidator = {
|
|
||||||
condition: ({ required }) => required,
|
|
||||||
message: () => `Workspace with this name already exists`,
|
|
||||||
validate: (value) => {
|
|
||||||
const current = workspaceStore.currentWorkspace;
|
|
||||||
|
|
||||||
if (current.name === value.trim()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !workspaceStore.enabledWorkspacesList.find((workspace) => workspace.name === value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EditWorkspaceState {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class EditWorkspace extends React.Component<{}, EditWorkspaceState> {
|
|
||||||
|
|
||||||
state: EditWorkspaceState = {
|
|
||||||
name: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setState({name: workspaceStore.currentWorkspace.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(name: string) {
|
|
||||||
if (name.trim() === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceStore.currentWorkspace.name = name;
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(name: string) {
|
|
||||||
this.setState({name});
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.state.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
placeholder="Workspace name"
|
|
||||||
autoFocus={true}
|
|
||||||
theme="round-black"
|
|
||||||
data-test-id="command-palette-workspace-add-name"
|
|
||||||
validators={[validateWorkspaceName]}
|
|
||||||
onChange={(v) => this.onChange(v)}
|
|
||||||
onSubmit={(v) => this.onSubmit(v)}
|
|
||||||
dirty={true}
|
|
||||||
value={this.name}
|
|
||||||
showValidationLine={true} />
|
|
||||||
<small className="hint">
|
|
||||||
Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel)
|
|
||||||
</small>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.editCurrentWorkspace",
|
|
||||||
title: "Workspace: Edit current workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<EditWorkspace />),
|
|
||||||
isActive: (context) => context.workspace?.id !== WorkspaceStore.defaultId
|
|
||||||
});
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./workspaces";
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { computed} from "mobx";
|
|
||||||
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Select } from "../select";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class RemoveWorkspace extends React.Component {
|
|
||||||
@computed get options() {
|
|
||||||
return workspaceStore.enabledWorkspacesList.filter((workspace) => workspace.id !== WorkspaceStore.defaultId).map((workspace) => {
|
|
||||||
return { value: workspace.id, label: workspace.name };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(id: string) {
|
|
||||||
const workspace = workspaceStore.enabledWorkspacesList.find((workspace) => workspace.id === id);
|
|
||||||
|
|
||||||
if (!workspace ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandOverlay.close();
|
|
||||||
ConfirmDialog.open({
|
|
||||||
okButtonProps: {
|
|
||||||
label: `Remove Workspace`,
|
|
||||||
primary: false,
|
|
||||||
accent: true,
|
|
||||||
},
|
|
||||||
ok: () => {
|
|
||||||
workspaceStore.removeWorkspace(workspace);
|
|
||||||
},
|
|
||||||
message: (
|
|
||||||
<div className="confirm flex column gaps">
|
|
||||||
<p>
|
|
||||||
Are you sure you want remove workspace <b>{workspace.name}</b>?
|
|
||||||
</p>
|
|
||||||
<p className="info">
|
|
||||||
All clusters within workspace will be cleared as well
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
data-test-id="command-palette-workspace-remove-select"
|
|
||||||
placeholder="Remove workspace" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.removeWorkspace",
|
|
||||||
title: "Workspace: Remove workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<RemoveWorkspace />)
|
|
||||||
});
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { computed} from "mobx";
|
|
||||||
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Select } from "../select";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { AddWorkspace } from "./add-workspace";
|
|
||||||
import { RemoveWorkspace } from "./remove-workspace";
|
|
||||||
import { EditWorkspace } from "./edit-workspace";
|
|
||||||
import { landingURL } from "../+landing-page";
|
|
||||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ChooseWorkspace extends React.Component {
|
|
||||||
private static overviewActionId = "__overview__";
|
|
||||||
private static addActionId = "__add__";
|
|
||||||
private static removeActionId = "__remove__";
|
|
||||||
private static editActionId = "__edit__";
|
|
||||||
|
|
||||||
@computed get options() {
|
|
||||||
const options = workspaceStore.enabledWorkspacesList.map((workspace) => {
|
|
||||||
return { value: workspace.id, label: workspace.name };
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push({ value: ChooseWorkspace.overviewActionId, label: "Show current workspace overview ..." });
|
|
||||||
|
|
||||||
options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." });
|
|
||||||
|
|
||||||
if (options.length > 1) {
|
|
||||||
options.push({ value: ChooseWorkspace.removeActionId, label: "Remove workspace ..." });
|
|
||||||
|
|
||||||
if (workspaceStore.currentWorkspace.id !== WorkspaceStore.defaultId) {
|
|
||||||
options.push({ value: ChooseWorkspace.editActionId, label: "Edit current workspace ..." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(id: string) {
|
|
||||||
if (id === ChooseWorkspace.overviewActionId) {
|
|
||||||
navigate(landingURL()); // overview of active workspace. TODO: change name from landing
|
|
||||||
CommandOverlay.close();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === ChooseWorkspace.addActionId) {
|
|
||||||
CommandOverlay.open(<AddWorkspace />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === ChooseWorkspace.removeActionId) {
|
|
||||||
CommandOverlay.open(<RemoveWorkspace />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === ChooseWorkspace.editActionId) {
|
|
||||||
CommandOverlay.open(<EditWorkspace />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceStore.setActive(id);
|
|
||||||
const clusterId = workspaceStore.getById(id).lastActiveClusterId;
|
|
||||||
|
|
||||||
if (clusterId) {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
} else {
|
|
||||||
navigate(landingURL());
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Switch to workspace" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.chooseWorkspace",
|
|
||||||
title: "Workspace: Switch to workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<ChooseWorkspace />)
|
|
||||||
});
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, observable, reaction } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "../navigation";
|
import { history } from "../navigation";
|
||||||
@ -36,7 +36,7 @@ import { webFrame } from "electron";
|
|||||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||||
import { extensionLoader } from "../../extensions/extension-loader";
|
import { extensionLoader } from "../../extensions/extension-loader";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
import { broadcastMessage, requestMain } from "../../common/ipc";
|
import { requestMain } from "../../common/ipc";
|
||||||
import whatInput from "what-input";
|
import whatInput from "what-input";
|
||||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||||
@ -86,20 +86,12 @@ export class App extends React.Component {
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
|
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
|
||||||
preload: true,
|
preload: true,
|
||||||
}),
|
})
|
||||||
|
|
||||||
reaction(() => this.warningsTotal, (count: number) => {
|
|
||||||
broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count);
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
|
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
|
||||||
|
|
||||||
@computed get warningsTotal(): number {
|
|
||||||
return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
||||||
const routes: TabLayoutRoute[] = [];
|
const routes: TabLayoutRoute[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
.ClusterIcon {
|
|
||||||
--size: 37px;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: $radius;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.interactive {
|
|
||||||
img {
|
|
||||||
opacity: .55;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active, &.interactive:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
|
|
||||||
img {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Badge {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: -$padding;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
background: $colorError;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import "./cluster-icon.scss";
|
|
||||||
|
|
||||||
import React, { DOMAttributes } from "react";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import { Params as HashiconParams } from "@emeraldpay/hashicon";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { cssNames, IClassName } from "../../utils";
|
|
||||||
import { Badge } from "../badge";
|
|
||||||
import { Tooltip } from "../tooltip";
|
|
||||||
import { subscribeToBroadcast } from "../../../common/ipc";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
|
||||||
cluster: Cluster;
|
|
||||||
className?: IClassName;
|
|
||||||
errorClass?: IClassName;
|
|
||||||
showErrors?: boolean;
|
|
||||||
showTooltip?: boolean;
|
|
||||||
interactive?: boolean;
|
|
||||||
isActive?: boolean;
|
|
||||||
options?: HashiconParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
|
||||||
showErrors: true,
|
|
||||||
showTooltip: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClusterIcon extends React.Component<Props> {
|
|
||||||
static defaultProps = defaultProps as object;
|
|
||||||
|
|
||||||
@observable eventCount = 0;
|
|
||||||
|
|
||||||
get eventCountBroadcast() {
|
|
||||||
return `cluster-warning-event-count:${this.props.cluster.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const subscriber = subscribeToBroadcast(this.eventCountBroadcast, (ev, eventCount) => {
|
|
||||||
this.eventCount = eventCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
subscriber
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive,
|
|
||||||
children, ...elemProps
|
|
||||||
} = this.props;
|
|
||||||
const { name, preferences, id: clusterId, online } = cluster;
|
|
||||||
const eventCount = this.eventCount;
|
|
||||||
const { icon } = preferences;
|
|
||||||
const clusterIconId = `cluster-icon-${clusterId}`;
|
|
||||||
const className = cssNames("ClusterIcon flex inline", this.props.className, {
|
|
||||||
interactive: interactive !== undefined ? interactive : !!this.props.onClick,
|
|
||||||
active: isActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
|
|
||||||
{showTooltip && (
|
|
||||||
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
|
|
||||||
)}
|
|
||||||
{icon && <img src={icon} alt={name}/>}
|
|
||||||
{!icon && <Hashicon value={clusterId} options={options}/>}
|
|
||||||
{showErrors && eventCount > 0 && !isActive && online && (
|
|
||||||
<Badge
|
|
||||||
className={cssNames("events-count", errorClass)}
|
|
||||||
label={eventCount >= 1000 ? `${Math.ceil(eventCount / 1000)}k+` : eventCount}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./cluster-icon";
|
|
||||||
@ -6,7 +6,7 @@
|
|||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
height: var(--bottom-bar-height);
|
height: var(--bottom-bar-height);
|
||||||
|
|
||||||
#current-workspace {
|
#catalog-link {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
color: white;
|
color: white;
|
||||||
padding: $padding / 4 $padding / 2;
|
padding: $padding / 4 $padding / 2;
|
||||||
|
|||||||
@ -2,11 +2,10 @@ import "./bottom-bar.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
|
import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
import { navigate } from "../../navigation";
|
||||||
import { ChooseWorkspace } from "../+workspaces";
|
import { catalogURL } from "../+catalog";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class BottomBar extends React.Component {
|
export class BottomBar extends React.Component {
|
||||||
@ -45,13 +44,11 @@ export class BottomBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentWorkspace } = workspaceStore;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="BottomBar flex gaps">
|
<div className="BottomBar flex gaps">
|
||||||
<div id="current-workspace" data-test-id="current-workspace" className="flex gaps align-center" onClick={() => CommandOverlay.open(<ChooseWorkspace />)}>
|
<div id="catalog-link" data-test-id="catalog-link" className="flex gaps align-center" onClick={() => navigate(catalogURL())}>
|
||||||
<Icon smallest material="layers"/>
|
<Icon smallest material="view_list"/>
|
||||||
<span className="workspace-name" data-test-id="current-workspace-name">{currentWorkspace.name}</span>
|
<span className="workspace-name" data-test-id="current-workspace-name">Catalog</span>
|
||||||
</div>
|
</div>
|
||||||
{this.renderRegisteredItems()}
|
{this.renderRegisteredItems()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import uniqueId from "lodash/uniqueId";
|
import uniqueId from "lodash/uniqueId";
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
import { clusterSettingsURL } from "../+cluster-settings";
|
||||||
import { landingURL } from "../+landing-page";
|
import { catalogURL } from "../+catalog";
|
||||||
|
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { broadcastMessage, requestMain } from "../../../common/ipc";
|
import { broadcastMessage, requestMain } from "../../../common/ipc";
|
||||||
@ -25,7 +25,7 @@ export const ClusterActions = (cluster: Cluster) => ({
|
|||||||
})),
|
})),
|
||||||
disconnect: async () => {
|
disconnect: async () => {
|
||||||
clusterStore.deactivate(cluster.id);
|
clusterStore.deactivate(cluster.id);
|
||||||
navigate(landingURL());
|
navigate(catalogURL());
|
||||||
await requestMain(clusterDisconnectHandler, cluster.id);
|
await requestMain(clusterDisconnectHandler, cluster.id);
|
||||||
},
|
},
|
||||||
remove: () => {
|
remove: () => {
|
||||||
@ -40,7 +40,7 @@ export const ClusterActions = (cluster: Cluster) => ({
|
|||||||
ok: () => {
|
ok: () => {
|
||||||
clusterStore.deactivate(cluster.id);
|
clusterStore.deactivate(cluster.id);
|
||||||
clusterStore.removeById(cluster.id);
|
clusterStore.removeById(cluster.id);
|
||||||
navigate(landingURL());
|
navigate(catalogURL());
|
||||||
},
|
},
|
||||||
message: <p>
|
message: <p>
|
||||||
Are you sure want to remove cluster <b id={tooltipId}>{cluster.name}</b>?
|
Are you sure want to remove cluster <b id={tooltipId}>{cluster.name}</b>?
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ClustersMenu {
|
.HotbarMenu {
|
||||||
grid-area: menu;
|
grid-area: menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,19 +4,19 @@ import React from "react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { comparer, reaction } from "mobx";
|
import { comparer, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ClustersMenu } from "./clusters-menu";
|
|
||||||
import { BottomBar } from "./bottom-bar";
|
import { BottomBar } from "./bottom-bar";
|
||||||
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
import { Catalog, catalogRoute, catalogURL } from "../+catalog";
|
||||||
import { Preferences, preferencesRoute } from "../+preferences";
|
import { Preferences, preferencesRoute } from "../+preferences";
|
||||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||||
import { ClusterView } from "./cluster-view";
|
import { ClusterView } from "./cluster-view";
|
||||||
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||||
import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
|
import { clusterViewRoute } from "./cluster-view.route";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||||
import { Extensions, extensionsRoute } from "../+extensions";
|
import { Extensions, extensionsRoute } from "../+extensions";
|
||||||
import { getMatchedClusterId } from "../../navigation";
|
import { getMatchedClusterId } from "../../navigation";
|
||||||
|
import { HotbarMenu } from "../hotbar/hotbar-menu";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterManager extends React.Component {
|
export class ClusterManager extends React.Component {
|
||||||
@ -44,17 +44,7 @@ export class ClusterManager extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get startUrl() {
|
get startUrl() {
|
||||||
const { activeClusterId } = clusterStore;
|
return catalogURL();
|
||||||
|
|
||||||
if (activeClusterId) {
|
|
||||||
return clusterViewURL({
|
|
||||||
params: {
|
|
||||||
clusterId: activeClusterId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return landingURL();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -63,7 +53,7 @@ export class ClusterManager extends React.Component {
|
|||||||
<main>
|
<main>
|
||||||
<div id="lens-views"/>
|
<div id="lens-views"/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={LandingPage} {...landingRoute} />
|
<Route component={Catalog} {...catalogRoute} />
|
||||||
<Route component={Preferences} {...preferencesRoute} />
|
<Route component={Preferences} {...preferencesRoute} />
|
||||||
<Route component={Extensions} {...extensionsRoute} />
|
<Route component={Extensions} {...extensionsRoute} />
|
||||||
<Route component={AddCluster} {...addClusterRoute} />
|
<Route component={AddCluster} {...addClusterRoute} />
|
||||||
@ -75,7 +65,7 @@ export class ClusterManager extends React.Component {
|
|||||||
<Redirect exact to={this.startUrl}/>
|
<Redirect exact to={this.startUrl}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
<ClustersMenu/>
|
<HotbarMenu/>
|
||||||
<BottomBar/>
|
<BottomBar/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ClusterStatus } from "./cluster-status";
|
|||||||
import { hasLoadedView } from "./lens-views";
|
import { hasLoadedView } from "./lens-views";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { catalogURL } from "../+catalog";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -26,6 +28,9 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
|
}),
|
||||||
|
reaction(() => this.cluster.online, (online) => {
|
||||||
|
if (!online) navigate(catalogURL());
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
.ClustersMenu {
|
|
||||||
$spacing: $padding * 2;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
background: $clusterMenuBackground;
|
|
||||||
border-right: 1px solid $clusterMenuBorderColor;
|
|
||||||
padding: $spacing 0;
|
|
||||||
min-width: 75px;
|
|
||||||
|
|
||||||
.is-mac &:before {
|
|
||||||
content: "";
|
|
||||||
height: 20px; // extra spacing for mac-os "traffic-light" buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
.clusters {
|
|
||||||
@include hidden-scrollbar;
|
|
||||||
padding: 0 $spacing; // extra spacing for cluster-icon's badge
|
|
||||||
margin-bottom: $margin;
|
|
||||||
|
|
||||||
.ClusterIcon {
|
|
||||||
margin-bottom: $margin * 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .WorkspaceMenu {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: $margin;
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
margin-bottom: $margin * 1.5;
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: $padding / 3;
|
|
||||||
color: var(--textColorPrimary);
|
|
||||||
background: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: none;
|
|
||||||
color: var(--textColorAccent);
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .extensions {
|
|
||||||
&:not(:empty) {
|
|
||||||
padding-top: $spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
--size: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Menu.WorkspaceMenu {
|
|
||||||
z-index: 2; // Place behind Preferences, Extension pages etc...
|
|
||||||
}
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
import "./clusters-menu.scss";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { remote } from "electron";
|
|
||||||
import type { Cluster } from "../../../main/cluster";
|
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { autobind, cssNames, IClassName } from "../../utils";
|
|
||||||
import { isActiveRoute, navigate } from "../../navigation";
|
|
||||||
import { addClusterURL } from "../+add-cluster";
|
|
||||||
import { landingURL } from "../+landing-page";
|
|
||||||
import { clusterViewURL } from "./cluster-view.route";
|
|
||||||
import { ClusterActions } from "./cluster-actions";
|
|
||||||
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { computed, observable } from "mobx";
|
|
||||||
import { Select } from "../select";
|
|
||||||
import { Menu, MenuItem } from "../menu";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: IClassName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClustersMenu extends React.Component<Props> {
|
|
||||||
@observable workspaceMenuVisible = false;
|
|
||||||
|
|
||||||
showCluster = (clusterId: ClusterId) => {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
showContextMenu = (cluster: Cluster) => {
|
|
||||||
const { Menu, MenuItem } = remote;
|
|
||||||
const menu = new Menu();
|
|
||||||
const actions = ClusterActions(cluster);
|
|
||||||
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: `Settings`,
|
|
||||||
click: actions.showSettings
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (cluster.online) {
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: `Disconnect`,
|
|
||||||
click: actions.disconnect
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: `Remove`,
|
|
||||||
click: actions.remove
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
menu.popup({
|
|
||||||
window: remote.getCurrentWindow()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
swapClusterIconOrder(result: DropResult) {
|
|
||||||
if (result.reason === "DROP") {
|
|
||||||
const { currentWorkspaceId } = workspaceStore;
|
|
||||||
const {
|
|
||||||
source: { index: from },
|
|
||||||
destination: { index: to },
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
clusterStore.swapIconOrders(currentWorkspaceId, from, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { className } = this.props;
|
|
||||||
const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId);
|
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled);
|
|
||||||
const activeClusterId = clusterStore.activeCluster;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cssNames("ClustersMenu flex column", className)}>
|
|
||||||
<div className="clusters flex column gaps">
|
|
||||||
<DragDropContext onDragEnd={this.swapClusterIconOrder}>
|
|
||||||
<Droppable droppableId="cluster-menu" type="CLUSTER">
|
|
||||||
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
|
|
||||||
<div ref={innerRef} {...droppableProps}>
|
|
||||||
{clusters.map((cluster, index) => {
|
|
||||||
const isActive = cluster.id === activeClusterId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
|
||||||
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
|
|
||||||
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
|
|
||||||
<ClusterIcon
|
|
||||||
key={cluster.id}
|
|
||||||
showErrors={true}
|
|
||||||
cluster={cluster}
|
|
||||||
isActive={isActive}
|
|
||||||
onClick={() => this.showCluster(cluster.id)}
|
|
||||||
onContextMenu={() => this.showContextMenu(cluster)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="WorkspaceMenu">
|
|
||||||
<Icon big material="menu" id="workspace-menu-icon" data-test-id="workspace-menu" />
|
|
||||||
<Menu
|
|
||||||
usePortal
|
|
||||||
htmlFor="workspace-menu-icon"
|
|
||||||
className="WorkspaceMenu"
|
|
||||||
isOpen={this.workspaceMenuVisible}
|
|
||||||
open={() => this.workspaceMenuVisible = true}
|
|
||||||
close={() => this.workspaceMenuVisible = false}
|
|
||||||
toggleEvent="click"
|
|
||||||
>
|
|
||||||
<MenuItem onClick={() => navigate(addClusterURL())} data-test-id="add-cluster-menu-item">
|
|
||||||
<Icon small material="add" /> Add Cluster
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => navigate(landingURL())} data-test-id="workspace-overview-menu-item">
|
|
||||||
<Icon small material="dashboard" /> Workspace Overview
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="extensions">
|
|
||||||
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
|
||||||
const registeredPage = globalPageRegistry.getByPageTarget(target);
|
|
||||||
|
|
||||||
if (!registeredPage){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pageUrl = getExtensionPageUrl(target);
|
|
||||||
const isActive = isActiveRoute(registeredPage.url);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
key={pageUrl}
|
|
||||||
tooltip={title}
|
|
||||||
active={isActive}
|
|
||||||
onClick={() => navigate(pageUrl)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ChooseCluster extends React.Component {
|
|
||||||
@computed get options() {
|
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId).filter(cluster => cluster.enabled);
|
|
||||||
const options = clusters.map((cluster) => {
|
|
||||||
return { value: cluster.id, label: cluster.name };
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(clusterId: string) {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Switch to cluster" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.chooseCluster",
|
|
||||||
title: "Workspace: Switch to cluster ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<ChooseCluster />)
|
|
||||||
});
|
|
||||||
@ -8,8 +8,6 @@ import { EventEmitter } from "../../../common/event-emitter";
|
|||||||
import { subscribeToBroadcast } from "../../../common/ipc";
|
import { subscribeToBroadcast } from "../../../common/ipc";
|
||||||
import { CommandDialog } from "./command-dialog";
|
import { CommandDialog } from "./command-dialog";
|
||||||
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
|
|
||||||
export type CommandDialogEvent = {
|
export type CommandDialogEvent = {
|
||||||
component: React.ReactElement
|
component: React.ReactElement
|
||||||
@ -49,8 +47,7 @@ export class CommandContainer extends React.Component<{ clusterId?: string }> {
|
|||||||
|
|
||||||
private runCommand(command: CommandRegistration) {
|
private runCommand(command: CommandRegistration) {
|
||||||
command.action({
|
command.action({
|
||||||
cluster: clusterStore.active,
|
entity: commandRegistry.activeEntity
|
||||||
workspace: workspaceStore.currentWorkspace
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { observer } from "mobx-react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { CommandOverlay } from "./command-container";
|
import { CommandOverlay } from "./command-container";
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
@ -17,12 +16,11 @@ export class CommandDialog extends React.Component {
|
|||||||
|
|
||||||
@computed get options() {
|
@computed get options() {
|
||||||
const context = {
|
const context = {
|
||||||
cluster: clusterStore.active,
|
entity: commandRegistry.activeEntity
|
||||||
workspace: workspaceStore.currentWorkspace
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return commandRegistry.getItems().filter((command) => {
|
return commandRegistry.getItems().filter((command) => {
|
||||||
if (command.scope === "cluster" && !clusterStore.active) {
|
if (command.scope === "entity" && !clusterStore.active) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,16 +54,15 @@ export class CommandDialog extends React.Component {
|
|||||||
|
|
||||||
if (command.scope === "global") {
|
if (command.scope === "global") {
|
||||||
action({
|
action({
|
||||||
cluster: clusterStore.active,
|
entity: commandRegistry.activeEntity
|
||||||
workspace: workspaceStore.currentWorkspace
|
|
||||||
});
|
});
|
||||||
} else if(clusterStore.active) {
|
} else if(commandRegistry.activeEntity) {
|
||||||
navigate(clusterViewURL({
|
navigate(clusterViewURL({
|
||||||
params: {
|
params: {
|
||||||
clusterId: clusterStore.active.id
|
clusterId: commandRegistry.activeEntity.metadata.uid
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
broadcastMessage(`command-palette:run-action:${clusterStore.active.id}`, command.id);
|
broadcastMessage(`command-palette:run-action:${commandRegistry.activeEntity.metadata.uid}`, command.id);
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
|
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
|
||||||
|
|||||||
@ -136,7 +136,7 @@ export class Dock extends React.Component<Props> {
|
|||||||
commandRegistry.add({
|
commandRegistry.add({
|
||||||
id: "cluster.openTerminal",
|
id: "cluster.openTerminal",
|
||||||
title: "Cluster: Open terminal",
|
title: "Cluster: Open terminal",
|
||||||
scope: "cluster",
|
scope: "entity",
|
||||||
action: () => createTerminalTab(),
|
action: () => createTerminalTab(),
|
||||||
isActive: (context) => !!context.cluster
|
isActive: (context) => !!context.entity
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export class ErrorBoundary extends React.Component<Props, State> {
|
|||||||
if (error) {
|
if (error) {
|
||||||
const slackLink = <a href={slackUrl} rel="noreferrer" target="_blank">Slack</a>;
|
const slackLink = <a href={slackUrl} rel="noreferrer" target="_blank">Slack</a>;
|
||||||
const githubLink = <a href={issuesTrackerUrl} rel="noreferrer" target="_blank">Github</a>;
|
const githubLink = <a href={issuesTrackerUrl} rel="noreferrer" target="_blank">Github</a>;
|
||||||
const pageUrl = location.href;
|
const pageUrl = location.pathname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ErrorBoundary flex column gaps">
|
<div className="ErrorBoundary flex column gaps">
|
||||||
|
|||||||
58
src/renderer/components/hotbar/hotbar-icon.scss
Normal file
58
src/renderer/components/hotbar/hotbar-icon.scss
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
.HotbarMenu {
|
||||||
|
.HotbarIcon {
|
||||||
|
--size: 37px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
div.MuiAvatar-colorDefault {
|
||||||
|
font-weight:500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.active {
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.default {
|
||||||
|
background-color: var(--halfGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
margin-left: -3px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active, &.interactive:hover {
|
||||||
|
|
||||||
|
div {
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.HotbarIconMenu {
|
||||||
|
left: 30px;
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user