From 4ac0c7c8995a2f4a214626fad3ca864ce1d81ad1 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 31 Mar 2021 14:42:46 +0300 Subject: [PATCH] hotbar store Signed-off-by: Jari Kolehmainen --- package.json | 2 +- .../catalog-entities/kubernetes-cluster.ts | 30 ++++++++- src/common/catalog-entity.ts | 8 ++- src/common/cluster-ipc.ts | 8 --- src/common/cluster-store.ts | 1 - src/common/hotbar-store.ts | 64 +++++++++++++++++++ src/main/catalog-pusher.ts | 1 + src/main/cluster-manager.ts | 6 +- src/main/index.ts | 2 + src/migrations/hotbar-store/5.0.0-alpha.0.ts | 28 ++++++++ src/migrations/hotbar-store/index.ts | 7 ++ src/renderer/api/catalog-entity-registry.ts | 2 +- src/renderer/bootstrap.tsx | 2 + .../+landing-page/catalog-entity.store.ts | 16 +++++ .../components/+landing-page/landing-page.tsx | 26 ++++++++ .../components/hotbar/hotbar-menu.tsx | 6 +- 16 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 src/common/hotbar-store.ts create mode 100644 src/migrations/hotbar-store/5.0.0-alpha.0.ts create mode 100644 src/migrations/hotbar-store/index.ts diff --git a/package.json b/package.json index 1399309ede..25b08758ca 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.2.0-rc.2", + "version": "5.0.0-alpha.0", "main": "static/build/main.js", "copyright": "© 2021, Mirantis, Inc.", "license": "MIT", diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index a2ccfcf600..526d9136d3 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -1,7 +1,9 @@ import { observable } from "mobx"; import { catalogCategoryRegistry } from "../catalog-category-registry"; -import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity"; +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; @@ -45,8 +47,30 @@ export class KubernetesCluster implements CatalogEntity { // } - async onContextMenuOpen() { - // + 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) + }, + ]; + + if (this.status.active) { + context.menuItems.unshift({ + icon: "link_off", + title: "Disconnect", + onClick: async () => { + clusterStore.deactivate(this.metadata.uid); + requestMain(clusterDisconnectHandler, this.metadata.uid); + } + }); + } } } diff --git a/src/common/catalog-entity.ts b/src/common/catalog-entity.ts index e69eeb9f58..9a78b5eab9 100644 --- a/src/common/catalog-entity.ts +++ b/src/common/catalog-entity.ts @@ -40,8 +40,14 @@ export interface CatalogEntityActionContext { navigate: (url: string) => void; } +export type CatalogEntityContextMenu = { + icon: string; + title: string; + onClick: () => Promise; +}; + export interface CatalogEntityContextMenuContext extends CatalogEntityActionContext { - menu: any + menuItems: CatalogEntityContextMenu[]; } export type CatalogEntityData = { diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index 30e485bece..d44634407e 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -4,7 +4,6 @@ import { appEventBus } from "./event-bus"; import { ResourceApplier } from "../main/resource-applier"; import { ipcMain, IpcMainInvokeEvent } from "electron"; import { clusterFrameMap } from "./cluster-frames"; -import { catalogEntityRegistry } from "./catalog-entity-registry"; export const clusterActivateHandler = "cluster:activate"; export const clusterSetFrameIdHandler = "cluster:set-frame-id"; @@ -18,10 +17,6 @@ if (ipcMain) { const cluster = clusterStore.getById(clusterId); if (cluster) { - catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster").forEach((item) => { - if (item.metadata.uid === cluster.id) item.status.active = true; - }); - return cluster.activate(force); } }); @@ -48,9 +43,6 @@ if (ipcMain) { if (cluster) { cluster.disconnect(); - catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster").forEach((item) => { - if (item.metadata.uid === cluster.id) item.status.active = false; - }); clusterFrameMap.delete(cluster.id); } }); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index a161f95189..989823352b 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -104,7 +104,6 @@ export class ClusterStore extends BaseStore { @observable removedClusters = observable.map(); @observable clusters = observable.map(); - private static stateRequestChannel = "cluster:states"; private constructor() { diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts new file mode 100644 index 0000000000..146559fde3 --- /dev/null +++ b/src/common/hotbar-store.ts @@ -0,0 +1,64 @@ +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 { + @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 = {}) { + this.hotbars = data.hotbars || []; + } + + 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(); diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index 268566b81e..2c1fb547cf 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -21,6 +21,7 @@ export class CatalogPusher { } broadcast() { + console.log("BROADCAST"); broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true })); } } diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index f12f6e77c0..c03b06f737 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -69,9 +69,7 @@ export class ClusterManager extends Singleton { if (entityIndex === -1) { this.catalogSource.push(newEntity); } else { - const entity = this.catalogSource[entityIndex] as KubernetesCluster; - - newEntity.status.active = entity.status.active; + newEntity.status.active = !cluster.disconnected; this.catalogSource.splice(entityIndex, 1, newEntity); } }); @@ -86,7 +84,7 @@ export class ClusterManager extends Singleton { name: cluster.name, source: "local", labels: { - "distro": cluster.metadata["distro"]?.toString() + "distro": (cluster.metadata["distribution"] || "unknown").toString() } }, spec: { diff --git a/src/main/index.ts b/src/main/index.ts index b40f9bf52e..2ba462ce0c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -31,6 +31,7 @@ import { startUpdateChecking } from "./app-updater"; 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); let proxyPort: number; @@ -108,6 +109,7 @@ app.on("ready", async () => { await Promise.all([ userStore.load(), clusterStore.load(), + hotbarStore.load(), extensionsStore.load(), filesystemProvisionerStore.load(), ]); diff --git a/src/migrations/hotbar-store/5.0.0-alpha.0.ts b/src/migrations/hotbar-store/5.0.0-alpha.0.ts new file mode 100644 index 0000000000..22087ab569 --- /dev/null +++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts @@ -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); + } +}); diff --git a/src/migrations/hotbar-store/index.ts b/src/migrations/hotbar-store/index.ts new file mode 100644 index 0000000000..ae9d4bc125 --- /dev/null +++ b/src/migrations/hotbar-store/index.ts @@ -0,0 +1,7 @@ +// Hotbar store migrations + +import version500alpha0 from "./5.0.0-alpha.0"; + +export default { + ...version500alpha0, +}; diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 1d02e0308b..fc04c690e8 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -4,7 +4,7 @@ import { CatalogEntity, CatalogEntityData } from "../../common/catalog-entity"; import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../common/catalog-category-registry"; import "../../common/catalog-entities"; -export { CatalogEntity, CatalogEntityData } from "../../common/catalog-entity"; +export { CatalogEntity, CatalogEntityData, CatalogEntityContextMenuContext } from "../../common/catalog-entity"; export class CatalogEntityRegistry { @observable protected _items: CatalogEntity[] = observable.array([], { deep: true }); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index a45188b6b5..c9ef607871 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -14,6 +14,7 @@ import * as LensExtensions from "../extensions/extension-api"; import { extensionDiscovery } from "../extensions/extension-discovery"; import { extensionLoader } from "../extensions/extension-loader"; import { extensionsStore } from "../extensions/extensions-store"; +import { hotbarStore } from "../common/hotbar-store"; import { filesystemProvisionerStore } from "../main/extension-filesystem"; import { App } from "./components/app"; import { LensApp } from "./lens-app"; @@ -55,6 +56,7 @@ export async function bootstrap(App: AppComponent) { // preload common stores await Promise.all([ userStore.load(), + hotbarStore.load(), clusterStore.load(), extensionsStore.load(), filesystemProvisionerStore.load(), diff --git a/src/renderer/components/+landing-page/catalog-entity.store.ts b/src/renderer/components/+landing-page/catalog-entity.store.ts index 6d47a81e03..0790c9e7f6 100644 --- a/src/renderer/components/+landing-page/catalog-entity.store.ts +++ b/src/renderer/components/+landing-page/catalog-entity.store.ts @@ -26,9 +26,25 @@ export class CatalogEntityItem implements ItemObject { 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; + } + onRun(ctx: any) { this.entity.onRun(ctx); } + + onContextMenuOpen(ctx: any) { + this.entity.onContextMenuOpen(ctx); + } } @autobind() diff --git a/src/renderer/components/+landing-page/landing-page.tsx b/src/renderer/components/+landing-page/landing-page.tsx index 939119dc25..f12c21e1c2 100644 --- a/src/renderer/components/+landing-page/landing-page.tsx +++ b/src/renderer/components/+landing-page/landing-page.tsx @@ -7,6 +7,10 @@ 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 { CatalogEntityContextMenuContext } from "../../api/catalog-entity-registry"; +import { Badge } from "../badge"; enum sortBy { name = "name", @@ -47,13 +51,35 @@ export class LandingPage extends React.Component { }} renderTableHeader={[ { title: "Name", className: "name", sortBy: sortBy.name }, + { title: "Labels", className: "labels" }, { title: "Status", className: "status", sortBy: sortBy.status }, ]} renderTableContents={(item: CatalogEntityItem) => [ item.name, + item.labels.map((label) => ), { title: item.phase, className: kebabCase(item.phase) } ]} onDetails={(item: CatalogEntityItem) => item.onRun({ navigate: (url: string) => navigate(url)})} + renderItemMenu={(item: CatalogEntityItem) => { + const menuOpenContext: CatalogEntityContextMenuContext = { + menuItems: [], + navigate: (url: string) => navigate(url) + }; + + item.onContextMenuOpen(menuOpenContext); + + return ( + + { menuOpenContext.menuItems.map((menuItem) => { + return ( + menuItem.onClick()}> + {menuItem.title} + + ); + })} + + ); + }} /> ); diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 898c62db37..9bf9f19683 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -5,8 +5,8 @@ import { observer } from "mobx-react"; import { HotbarIcon } from "./hotbar-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { KubernetesCluster } from "../../../common/catalog-entities/kubernetes-cluster"; import { navigate } from "../../navigation"; +import { hotbarStore } from "../../../common/hotbar-store"; interface Props { className?: IClassName; @@ -16,7 +16,8 @@ interface Props { export class HotbarMenu extends React.Component { render() { const { className } = this.props; - const items = catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster"); + const hotbar = hotbarStore.getByName("default"); // FIXME + const items = hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean); const runContext = { navigate: (url: string) => navigate(url) }; @@ -31,7 +32,6 @@ export class HotbarMenu extends React.Component { entity={entity} isActive={entity.status.active} onClick={() => entity.onRun(runContext)} - onContextMenu={() => entity.onContextMenuOpen()} /> ); })}