From 683a520186a3dc3724fc9e9189512f45cb7024bf Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 7 Jun 2021 01:23:09 -0400 Subject: [PATCH 01/33] Fix drawer closing on updating kube objects (#2964) Signed-off-by: Sebastian Malton --- src/renderer/kube-object.store.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index b7686d168b..f1b9cfba96 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -27,7 +27,7 @@ import { KubeObject, KubeStatus } from "./api/kube-object"; import type { IKubeWatchEvent } from "./api/kube-watch-api"; import { ItemStore } from "./item.store"; import { apiManager } from "./api/api-manager"; -import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; +import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; import type { KubeJsonApiData } from "./api/kube-json-api"; import { Notifications } from "./components/notifications"; @@ -280,6 +280,9 @@ export abstract class KubeObjectStore extends ItemSt async update(item: T, data: Partial): Promise { const newItem = await item.update(data); + + ensureObjectSelfLink(this.api, newItem); + const index = this.items.findIndex(item => item.getId() === newItem.getId()); this.items.splice(index, 1, newItem); From e7947021e95ab01058403c7f0da8733db7366860 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 7 Jun 2021 08:30:37 +0300 Subject: [PATCH 02/33] Set in-tree extensions package.json version on build (#2946) * set extensions package.json version on build Signed-off-by: Jari Kolehmainen * set extensions package.json version on build Signed-off-by: Jari Kolehmainen --- build/set_build_version.ts | 12 ++++++++++++ extensions/kube-object-event-status/package.json | 2 +- extensions/metrics-cluster-feature/package.json | 2 +- extensions/node-menu/package.json | 2 +- extensions/pod-menu/package.json | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/build/set_build_version.ts b/build/set_build_version.ts index df48a1da13..13d46d4ff3 100644 --- a/build/set_build_version.ts +++ b/build/set_build_version.ts @@ -22,6 +22,7 @@ import * as fs from "fs"; import * as path from "path"; import appInfo from "../package.json"; import semver from "semver"; +import fastGlob from "fast-glob"; const packagePath = path.join(__dirname, "../package.json"); const versionInfo = semver.parse(appInfo.version); @@ -41,3 +42,14 @@ if (versionInfo.prerelease) { fs.writeFileSync(packagePath, `${JSON.stringify(appInfo, null, 2)}\n`); + +const extensionManifests = fastGlob.sync(["extensions/*/package.json"]); + +for (const manifestPath of extensionManifests) { + const packagePath = path.join(__dirname, "..", manifestPath); + + import(packagePath).then((packageInfo) => { + packageInfo.default.version = `${versionInfo.raw}.${Date.now()}`; + fs.writeFileSync(packagePath, `${JSON.stringify(packageInfo.default, null, 2)}\n`); + }); +} diff --git a/extensions/kube-object-event-status/package.json b/extensions/kube-object-event-status/package.json index eb9a791c3f..429bb98827 100644 --- a/extensions/kube-object-event-status/package.json +++ b/extensions/kube-object-event-status/package.json @@ -1,6 +1,6 @@ { "name": "kube-object-event-status", - "version": "0.1.0", + "version": "0.0.1", "description": "Adds kube object status from events", "renderer": "dist/renderer.js", "lens": { diff --git a/extensions/metrics-cluster-feature/package.json b/extensions/metrics-cluster-feature/package.json index c47334cea6..8683b75737 100644 --- a/extensions/metrics-cluster-feature/package.json +++ b/extensions/metrics-cluster-feature/package.json @@ -1,6 +1,6 @@ { "name": "lens-metrics-cluster-feature", - "version": "0.1.0", + "version": "0.0.1", "description": "Lens metrics cluster feature", "renderer": "dist/renderer.js", "lens": { diff --git a/extensions/node-menu/package.json b/extensions/node-menu/package.json index 2790e40139..dcec7757ec 100644 --- a/extensions/node-menu/package.json +++ b/extensions/node-menu/package.json @@ -1,6 +1,6 @@ { "name": "lens-node-menu", - "version": "0.1.0", + "version": "0.0.1", "description": "Lens node menu", "renderer": "dist/renderer.js", "lens": { diff --git a/extensions/pod-menu/package.json b/extensions/pod-menu/package.json index 1ff9d23d42..71ca4c2f3e 100644 --- a/extensions/pod-menu/package.json +++ b/extensions/pod-menu/package.json @@ -1,6 +1,6 @@ { "name": "lens-pod-menu", - "version": "0.1.0", + "version": "0.0.1", "description": "Lens pod menu", "renderer": "dist/renderer.js", "lens": { From 031c57962b211853564656d76e27dee41ee1b499 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 7 Jun 2021 03:06:52 -0400 Subject: [PATCH 03/33] Readd NamespaceSelectFilter to releases.tsx (#2962) Signed-off-by: Sebastian Malton --- src/renderer/components/+apps-releases/releases.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index 7465ad16b5..bdc8cf22b6 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -34,6 +34,7 @@ import { navigation } from "../../navigation"; import { ItemListLayout } from "../item-object-list/item-list-layout"; import { HelmReleaseMenu } from "./release-menu"; import { secretsStore } from "../+config-secrets/secrets.store"; +import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; enum columnId { name = "name", @@ -117,6 +118,15 @@ export class HelmReleases extends Component { (release: HelmRelease) => release.getVersion(), ]} renderHeaderTitle="Releases" + customizeHeader={({ filters, ...headerPlaceholders }) => ({ + filters: ( + <> + {filters} + + + ), + ...headerPlaceholders, + })} renderTableHeader={[ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, From 0fb927f96baf9e5e7f067284ec147e73d4d128cd Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 7 Jun 2021 10:09:00 +0300 Subject: [PATCH 04/33] catalog details panel (#2939) Signed-off-by: Jari Kolehmainen Co-authored-by: Sebastian Malton --- integration/helpers/minikube.ts | 2 + .../catalog-entities/kubernetes-cluster.ts | 6 +- src/common/catalog/catalog-entity.ts | 19 ++- src/extensions/extension-loader.ts | 1 + src/extensions/lens-renderer-extension.ts | 3 +- .../catalog-entity-detail-registry.ts | 46 +++++++ src/extensions/registries/index.ts | 1 + .../+catalog/catalog-entity-details.scss | 43 ++++++ .../+catalog/catalog-entity-details.tsx | 129 ++++++++++++++++++ .../+catalog/catalog-entity-drawer-menu.tsx | 127 +++++++++++++++++ src/renderer/components/+catalog/catalog.tsx | 23 ++-- .../components/hotbar/hotbar-entity-icon.tsx | 1 + .../components/hotbar/hotbar-icon.tsx | 9 +- .../components/hotbar/hotbar-menu.tsx | 2 + 14 files changed, 395 insertions(+), 17 deletions(-) create mode 100644 src/extensions/registries/catalog-entity-detail-registry.ts create mode 100644 src/renderer/components/+catalog/catalog-entity-details.scss create mode 100644 src/renderer/components/+catalog/catalog-entity-details.tsx create mode 100644 src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts index c94b31aa93..bbe411047b 100644 --- a/integration/helpers/minikube.ts +++ b/integration/helpers/minikube.ts @@ -64,6 +64,8 @@ export async function waitForMinikubeDashboard(app: Application) { await app.client.setValue(".Input.SearchInput input", "minikube"); await app.client.waitUntilTextExists("div.TableCell", "minikube"); await app.client.click("div.TableRow"); + await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube"); + await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root"); await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); await app.client.waitForExist(`iframe[name="minikube"]`); await app.client.frame("minikube"); diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index dc6d36d023..67d82e3501 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -104,6 +104,7 @@ export class KubernetesCluster extends CatalogEntity context.navigate(`/entity/${this.metadata.uid}/settings`) }, @@ -112,6 +113,7 @@ export class KubernetesCluster extends CatalogEntity ClusterStore.getInstance().removeById(this.metadata.uid), confirm: { @@ -123,6 +125,7 @@ export class KubernetesCluster extends CatalogEntity { requestMain(clusterDisconnectHandler, this.metadata.uid); } @@ -130,6 +133,7 @@ export class KubernetesCluster extends CatalogEntity { context.navigate(`/cluster/${this.metadata.uid}`); } @@ -147,7 +151,7 @@ export class KubernetesClusterCategory extends CatalogCategory { public readonly kind = "CatalogCategory"; public metadata = { name: "Kubernetes Clusters", - icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line + icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line }; public spec: CatalogCategorySpec = { group: "entity.k8slens.dev", diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index afc15d4c64..6c58497204 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -96,9 +96,25 @@ export interface CatalogEntityActionContext { } export interface CatalogEntityContextMenu { + /** + * Menu title + */ title: string; - onlyVisibleForSource?: string; // show only if empty or if matches with entity source + /** + * Menu icon + */ + icon?: string; + /** + * Show only if empty or if value matches with entity.metadata.source + */ + onlyVisibleForSource?: string; + /** + * OnClick handler + */ onClick: () => void | Promise; + /** + * Confirm click with a message + */ confirm?: { message: string; } @@ -175,7 +191,6 @@ export abstract class CatalogEntity< } public abstract onRun?(context: CatalogEntityActionContext): void | Promise; - public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise; public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise; public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise; } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 8651455380..6a4af088e8 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -250,6 +250,7 @@ export class ExtensionLoader extends Singleton { registries.statusBarRegistry.add(extension.statusBarItems), registries.commandRegistry.add(extension.commands), registries.welcomeMenuRegistry.add(extension.welcomeMenus), + registries.catalogEntityDetailRegistry.add(extension.catalogEntityDetailItems), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index e9105dbcd9..fb26d383b0 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -20,7 +20,7 @@ */ import type { - AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, + AppPreferenceRegistration, CatalogEntityDetailRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration, } from "./registries"; import type { Cluster } from "../main/cluster"; @@ -43,6 +43,7 @@ export class LensRendererExtension extends LensExtension { kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = []; commands: CommandRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = []; + catalogEntityDetailItems: CatalogEntityDetailRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); diff --git a/src/extensions/registries/catalog-entity-detail-registry.ts b/src/extensions/registries/catalog-entity-detail-registry.ts new file mode 100644 index 0000000000..0ed00b7a8b --- /dev/null +++ b/src/extensions/registries/catalog-entity-detail-registry.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import type React from "react"; +import { BaseRegistry } from "./base-registry"; + +export interface CatalogEntityDetailComponents { + Details: React.ComponentType; +} + +export interface CatalogEntityDetailRegistration { + kind: string; + apiVersions: string[]; + components: CatalogEntityDetailComponents; + priority?: number; +} + +export class CatalogEntityDetailRegistry extends BaseRegistry { + getItemsForKind(kind: string, apiVersion: string) { + const items = this.getItems().filter((item) => { + return item.kind === kind && item.apiVersions.includes(apiVersion); + }); + + return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50)); + } +} + +export const catalogEntityDetailRegistry = new CatalogEntityDetailRegistry(); diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index e3a6490d61..b9ba986521 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -33,4 +33,5 @@ export * from "./command-registry"; export * from "./entity-setting-registry"; export * from "./welcome-menu-registry"; export * from "./protocol-handler-registry"; +export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; diff --git a/src/renderer/components/+catalog/catalog-entity-details.scss b/src/renderer/components/+catalog/catalog-entity-details.scss new file mode 100644 index 0000000000..f63e7c3ba3 --- /dev/null +++ b/src/renderer/components/+catalog/catalog-entity-details.scss @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + .CatalogEntityDetails { + .EntityMetadata { + margin-right: $margin; + } + .EntityIcon.box.top.left { + margin-right: $margin * 2; + + .IconHint { + text-align: center; + font-size: var(--font-size-small); + text-transform: uppercase; + margin-top: $margin; + cursor: default; + user-select: none; + opacity: 0.5; + } + + div * { + font-size: 1.5em; + } + } + } diff --git a/src/renderer/components/+catalog/catalog-entity-details.tsx b/src/renderer/components/+catalog/catalog-entity-details.tsx new file mode 100644 index 0000000000..8837920de4 --- /dev/null +++ b/src/renderer/components/+catalog/catalog-entity-details.tsx @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import "./catalog-entity-details.scss"; +import React, { Component } from "react"; +import { observer } from "mobx-react"; +import { Drawer, DrawerItem, DrawerItemLabels } from "../drawer"; +import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; +import type { CatalogCategory } from "../../../common/catalog"; +import { Icon } from "../icon"; +import { KubeObject } from "../../api/kube-object"; +import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu"; +import { catalogEntityDetailRegistry } from "../../../extensions/registries"; +import { HotbarIcon } from "../hotbar/hotbar-icon"; + +interface Props { + entity: CatalogEntity; + hideDetails(): void; +} + +@observer +export class CatalogEntityDetails extends Component { + private abortController?: AbortController; + + constructor(props: Props) { + super(props); + } + + componentWillUnmount() { + this.abortController?.abort(); + } + + categoryIcon(category: CatalogCategory) { + if (category.metadata.icon.includes("; + } else { + return ; + } + } + + openEntity() { + this.props.entity.onRun(catalogEntityRunContext); + } + + renderContent() { + const { entity } = this.props; + const labels = KubeObject.stringifyLabels(entity.metadata.labels); + const detailItems = catalogEntityDetailRegistry.getItemsForKind(entity.kind, entity.apiVersion); + const details = detailItems.map((item, index) => { + return ; + }); + + const showDetails = detailItems.find((item) => item.priority > 999) === undefined; + + return ( + <> + {showDetails && ( +

+
+ this.openEntity()} + size={128} /> +
+ Click to open +
+
+
+ + {entity.metadata.name} + + + {entity.kind} + + + {entity.metadata.source} + + +
+
+ )} +
+ {details} +
+ + ); + } + + render() { + const { entity, hideDetails } = this.props; + const title = `${entity.kind}: ${entity.metadata.name}`; + + return ( + } + onClose={hideDetails} + > + {this.renderContent()} + + ); + } +} diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx new file mode 100644 index 0000000000..77c9df5d47 --- /dev/null +++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { cssNames } from "../../utils"; +import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; +import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; +import { observer } from "mobx-react"; +import { makeObservable, observable } from "mobx"; +import { navigate } from "../../navigation"; +import { MenuItem } from "../menu"; +import { ConfirmDialog } from "../confirm-dialog"; +import { HotbarStore } from "../../../common/hotbar-store"; +import { Icon } from "../icon"; + +export interface CatalogEntityDrawerMenuProps extends MenuActionsProps { + entity: T | null | undefined; +} + +@observer +export class CatalogEntityDrawerMenu extends React.Component> { + @observable private contextMenu: CatalogEntityContextMenuContext; + + constructor(props: CatalogEntityDrawerMenuProps) { + super(props); + makeObservable(this); + } + + componentDidMount() { + this.contextMenu = { + menuItems: [], + navigate: (url: string) => navigate(url) + }; + this.props.entity?.onContextMenuOpen(this.contextMenu); + } + + onMenuItemClick(menuItem: CatalogEntityContextMenu) { + if (menuItem.confirm) { + ConfirmDialog.open({ + okButtonProps: { + primary: false, + accent: true, + }, + ok: () => { + menuItem.onClick(); + }, + message: menuItem.confirm.message + }); + } else { + menuItem.onClick(); + } + } + + addToHotbar(entity: CatalogEntity): void { + HotbarStore.getInstance().addToHotbar(entity); + } + + getMenuItems(entity: T): React.ReactChild[] { + if (!entity) { + return []; + } + + const menuItems = this.contextMenu.menuItems.filter((menuItem) => { + return menuItem.icon && !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source; + }); + + const items = menuItems.map((menuItem, index) => { + const props = menuItem.icon.includes(" this.onMenuItemClick(menuItem)}> + + + ); + + }); + + items.unshift( + this.addToHotbar(entity) }> + + + ); + + items.reverse(); + + return items; + } + + render() { + if (!this.contextMenu) { + return null; + } + + const { className, entity, ...menuProps } = this.props; + + return ( + + {this.getMenuItems(entity)} + + ); + } +} diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 066295b864..90adf27a65 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -29,7 +29,7 @@ import { navigate } from "../../navigation"; import { kebabCase } from "lodash"; import { PageLayout } from "../layout/page-layout"; import { MenuItem, MenuActions } from "../menu"; -import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; +import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { Badge } from "../badge"; import { HotbarStore } from "../../../common/hotbar-store"; import { ConfirmDialog } from "../confirm-dialog"; @@ -40,6 +40,7 @@ import type { RouteComponentProps } from "react-router"; import type { ICatalogViewRouteParam } from "./catalog.route"; import { Notifications } from "../notifications"; import { Avatar } from "../avatar/avatar"; +import { CatalogEntityDetails } from "./catalog-entity-details"; enum sortBy { name = "name", @@ -55,6 +56,7 @@ export class Catalog extends React.Component { @observable private catalogEntityStore?: CatalogEntityStore; @observable private contextMenu: CatalogEntityContextMenuContext; @observable activeTab?: string; + @observable selectedItem?: CatalogEntityItem; constructor(props: Props) { super(props); @@ -103,7 +105,7 @@ export class Catalog extends React.Component { } onDetails(item: CatalogEntityItem) { - item.onRun(catalogEntityRunContext); + this.selectedItem = item; } onMenuItemClick(menuItem: CatalogEntityContextMenu) { @@ -181,12 +183,6 @@ export class Catalog extends React.Component { }; renderIcon(item: CatalogEntityItem) { - const category = catalogCategoryRegistry.getCategoryForEntity(item.entity); - - if (!category) { - return null; - } - return ( { item.labels.map((label) => ), { title: item.phase, className: kebabCase(item.phase) } ]} + detailsItem={this.selectedItem} onDetails={(item: CatalogEntityItem) => this.onDetails(item) } renderItemMenu={this.renderItemMenu} /> @@ -287,7 +284,15 @@ export class Catalog extends React.Component { provideBackButtonNavigation={false} contentGaps={false}> { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } - + { !this.selectedItem && ( + + )} + { this.selectedItem && ( + this.selectedItem = null} + /> + )} ); } diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 8c267e8956..f346727523 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -39,6 +39,7 @@ interface Props extends DOMAttributes { errorClass?: IClassName; add: (item: CatalogEntity, index: number) => void; remove: (uid: string) => void; + size?: number; } @observer diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 6dd86414e7..dd74a944de 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -31,7 +31,7 @@ import { MaterialTooltip } from "../material-tooltip/material-tooltip"; import { observer } from "mobx-react"; import { Avatar } from "../avatar/avatar"; -interface Props extends DOMAttributes { +export interface HotbarIconProps extends DOMAttributes { uid: string; title: string; source: string; @@ -40,6 +40,7 @@ interface Props extends DOMAttributes { active?: boolean; menuItems?: CatalogEntityContextMenu[]; disabled?: boolean; + size?: number; } function onMenuItemClick(menuItem: CatalogEntityContextMenu) { @@ -59,7 +60,7 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) { } } -export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { +export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: HotbarIconProps) => { const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props; const id = `hotbarIcon-${uid}`; const [menuOpen, setMenuOpen] = useState(false); @@ -77,8 +78,8 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { title={title} colorHash={`${title}-${source}`} className={active ? "active" : "default"} - width={40} - height={40} + width={size} + height={size} /> {children} diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index d3bb838ac7..68d93b1675 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -157,6 +157,7 @@ export class HotbarMenu extends React.Component { className={cssNames({ isDragging: snapshot.isDragging })} remove={this.removeItem} add={this.addItem} + size={40} /> ) : ( { source={item.entity.source} menuItems={disabledMenuItems} disabled + size={40} /> )} From 278510a90a9f26c7db806de3b885bae1119c8ac2 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 7 Jun 2021 03:40:39 -0400 Subject: [PATCH 05/33] Make a master tag of the extensions API (#2888) * Make a master tag of the extensions API Signed-off-by: Sebastian Malton * use the gitref as a prerelease header Signed-off-by: Sebastian Malton * Reduce the number of releases Signed-off-by: Sebastian Malton * Fix if Signed-off-by: Sebastian Malton --- .github/workflows/publish-master-npm.yml | 38 +++++++++++++++++++ ...ublish-npm.yml => publish-release-npm.yml} | 4 +- Makefile | 3 +- build/set_npm_version.ts | 18 +++++++-- 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/publish-master-npm.yml rename .github/workflows/{publish-npm.yml => publish-release-npm.yml} (90%) diff --git a/.github/workflows/publish-master-npm.yml b/.github/workflows/publish-master-npm.yml new file mode 100644 index 0000000000..e470289e9e --- /dev/null +++ b/.github/workflows/publish-master-npm.yml @@ -0,0 +1,38 @@ +name: Publish NPM Package `master` +on: + pull_request: + branches: + - master + types: + - closed +jobs: + publish: + name: Publish NPM Package `master` + runs-on: ubuntu-latest + if: | + ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }} + strategy: + matrix: + node-version: [12.x] + steps: + - name: Checkout Release + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Using Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Generate NPM package + run: | + make build-npm + + - name: publish new release + if: contains(github.ref, 'refs/tags/v') + run: | + make publish-npm + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_RELEASE_TAG: master diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-release-npm.yml similarity index 90% rename from .github/workflows/publish-npm.yml rename to .github/workflows/publish-release-npm.yml index 2502249ee3..5a8bd9aff5 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-release-npm.yml @@ -1,11 +1,11 @@ -name: Publish NPM +name: Publish NPM Package Release on: release: types: - published jobs: publish: - name: Publish NPM + name: Publish NPM Package Release runs-on: ubuntu-latest strategy: matrix: diff --git a/Makefile b/Makefile index fc764d0a94..3139b077b4 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,8 @@ build-extension-types: node_modules src/extensions/npm/extensions/dist .PHONY: publish-npm publish-npm: node_modules build-npm ./node_modules/.bin/npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" - cd src/extensions/npm/extensions && npm publish --access=public + cd src/extensions/npm/extensions && npm publish --access=public --tag=${NPM_RELEASE_TAG:-latest} + git restore src/extensions/npm/extensions/package.json .PHONY: docs docs: diff --git a/build/set_npm_version.ts b/build/set_npm_version.ts index b7614103a2..c0b24b54a6 100644 --- a/build/set_npm_version.ts +++ b/build/set_npm_version.ts @@ -22,8 +22,20 @@ import * as fs from "fs"; import * as path from "path"; import packageInfo from "../src/extensions/npm/extensions/package.json"; import appInfo from "../package.json"; +import { SemVer } from "semver"; +import { execSync } from "child_process"; -const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json"); +const { NPM_RELEASE_TAG = "latest" } = process.env; +const version = new SemVer(appInfo.version); -packageInfo.version = appInfo.version; -fs.writeFileSync(packagePath, `${JSON.stringify(packageInfo, null, 2)}\n`); +if (NPM_RELEASE_TAG !== "latest") { + const gitRef = execSync("git rev-parse --short HEAD", { + encoding: "utf-8", + }); + + version.inc("prerelease", `git.${gitRef.trim()}`); +} + +packageInfo.version = version.format(); + +fs.writeFileSync(path.join(__dirname, "../src/extensions/npm/extensions/package.json"), `${JSON.stringify(packageInfo, null, 2)}\n`); From 026cbbac0997d04fb6c414941d88a42530705bde Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 7 Jun 2021 11:46:40 +0300 Subject: [PATCH 06/33] Generic TopBar component for Catalog/Cluster views (#2882) * Rendering close button in cluster view Signed-off-by: Alex Andreev * Changing Icon hover style Signed-off-by: Alex Andreev * Moving onClick handler away Signed-off-by: Alex Andreev * Removing sidebar compact view Signed-off-by: Alex Andreev * Removing 'compact' refs in SidebarItem Signed-off-by: Alex Andreev * Wrapping Catalog with MainLayout Signed-off-by: Alex Andreev * Making sidebar resizing indicator visible on hover Signed-off-by: Alex Andreev * Adding TopBar to catalog view Signed-off-by: Alex Andreev * Using TopBar in cluster views Signed-off-by: Alex Andreev * Cleaning up Sidebar styles Signed-off-by: Alex Andreev * Using getActiveClusterEntity() for searching Signed-off-by: Alex Andreev * Fix resizing anchor position Signed-off-by: Alex Andreev * Align cluster name on left Signed-off-by: Alex Andreev * Removing unused files Signed-off-by: Alex Andreev * Removing unused css var Signed-off-by: Alex Andreev * Showing Topbar in ClusterStatus page Signed-off-by: Alex Andreev * Removing TopBar from ClusterManager Signed-off-by: Alex Andreev --- .../components/+catalog/catalog.module.css | 91 +++++++++++++++ src/renderer/components/+catalog/catalog.tsx | 58 ++++++---- src/renderer/components/+welcome/welcome.scss | 1 + src/renderer/components/app.scss | 2 +- src/renderer/components/app.tsx | 4 +- .../cluster-manager/cluster-manager.scss | 3 +- .../cluster-manager/cluster-topbar.tsx | 44 ++++++++ .../cluster-manager/cluster-view.tsx | 5 +- src/renderer/components/icon/icon.scss | 2 +- .../item-object-list/item-list-layout.scss | 2 +- .../__test__/main-layout-header.test.tsx | 105 ------------------ .../components/layout/main-layout.module.css | 50 +++++++++ .../components/layout/main-layout.scss | 76 ------------- .../components/layout/main-layout.tsx | 50 +++------ .../components/layout/sidebar-item.tsx | 12 +- .../components/layout/sidebar-storage.ts | 4 +- src/renderer/components/layout/sidebar.scss | 44 +------- src/renderer/components/layout/sidebar.tsx | 22 +--- .../components/layout/tab-layout.scss | 9 +- .../components/layout/topbar.module.css | 29 ++++- .../{main-layout-header.tsx => topbar.tsx} | 17 ++- .../resizing-anchor/resizing-anchor.scss | 28 +++++ src/renderer/themes/lens-dark.json | 3 +- src/renderer/themes/lens-light.json | 1 + 24 files changed, 325 insertions(+), 337 deletions(-) create mode 100644 src/renderer/components/+catalog/catalog.module.css create mode 100644 src/renderer/components/cluster-manager/cluster-topbar.tsx delete mode 100644 src/renderer/components/layout/__test__/main-layout-header.test.tsx create mode 100644 src/renderer/components/layout/main-layout.module.css delete mode 100755 src/renderer/components/layout/main-layout.scss rename __mocks__/@linguiMacro.ts => src/renderer/components/layout/topbar.module.css (70%) rename src/renderer/components/layout/{main-layout-header.tsx => topbar.tsx} (75%) diff --git a/src/renderer/components/+catalog/catalog.module.css b/src/renderer/components/+catalog/catalog.module.css new file mode 100644 index 0000000000..937195aa8f --- /dev/null +++ b/src/renderer/components/+catalog/catalog.module.css @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.iconCell { + max-width: 40px; + display: flex; + align-items: center; +} + +.nameCell { +} + +.sourceCell { + max-width: 100px; +} + +.statusCell { + max-width: 100px; +} + +.connected { + color: var(--colorSuccess); +} + +.disconnected { + color: var(--halfGray); +} + +.labelsCell { + overflow-x: scroll; + text-overflow: unset; +} + +.labelsCell::-webkit-scrollbar { + display: none; +} + +.badge { + overflow: unset; + text-overflow: unset; + max-width: unset; +} + +.badge:not(:first-child) { + margin-left: 0.5em; +} + +.catalogIcon { + font-size: 10px; + -webkit-font-smoothing: auto; +} + +.tabs { + @apply flex flex-grow flex-col; +} + +.tab { + @apply px-8 py-4; +} + +.tab:hover { + background-color: var(--sidebarItemHoverBackground); + --color-active: var(--textColorTertiary); +} + +.tab::after { + display: none; +} + +.activeTab, .activeTab:hover { + background-color: var(--blue); + --color-active: white; +} \ No newline at end of file diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 90adf27a65..2059658d1f 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -19,7 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./catalog.scss"; +import styles from "./catalog.module.css"; + import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { ItemListLayout } from "../item-object-list"; @@ -27,7 +28,6 @@ import { action, makeObservable, observable, reaction, when } 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 type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { Badge } from "../badge"; @@ -40,6 +40,12 @@ import type { RouteComponentProps } from "react-router"; import type { ICatalogViewRouteParam } from "./catalog.route"; import { Notifications } from "../notifications"; import { Avatar } from "../avatar/avatar"; +import { MainLayout } from "../layout/main-layout"; +import { cssNames } from "../../utils"; +import { TopBar } from "../layout/topbar"; +import { welcomeURL } from "../+welcome"; +import { Icon } from "../icon"; +import { MaterialTooltip } from "../material-tooltip/material-tooltip"; import { CatalogEntityDetails } from "./catalog-entity-details"; enum sortBy { @@ -139,14 +145,14 @@ export class Catalog extends React.Component { renderNavigation() { return ( - -
Catalog
-
+ +
{ this.categories.map(category => ( @@ -155,6 +161,7 @@ export class Catalog extends React.Component { key={category.getId()} label={category.metadata.name} data-testid={`${category.getId()}-tab`} + className={cssNames(styles.tab, { [styles.activeTab]: this.activeTab == category.getId() })} /> )) } @@ -189,7 +196,7 @@ export class Catalog extends React.Component { colorHash={`${item.name}-${item.source}`} width={24} height={24} - className="catalogIcon" + className={styles.catalogIcon} /> ); } @@ -278,22 +285,29 @@ export class Catalog extends React.Component { } return ( - - { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } - { !this.selectedItem && ( - - )} - { this.selectedItem && ( - this.selectedItem = null} - /> - )} - + <> + +
+ + navigate(welcomeURL())}/> + +
+
+ +
+ { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } +
+ { !this.selectedItem && ( + + )} + { this.selectedItem && ( + this.selectedItem = null} + /> + )} +
+ ); } } diff --git a/src/renderer/components/+welcome/welcome.scss b/src/renderer/components/+welcome/welcome.scss index f74e135f70..6141f9f517 100644 --- a/src/renderer/components/+welcome/welcome.scss +++ b/src/renderer/components/+welcome/welcome.scss @@ -22,6 +22,7 @@ .Welcome { text-align: center; width: 100%; + height: 100%; z-index: 1; .box { diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index b4c15b89fd..e1d002c8f7 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -39,7 +39,7 @@ --font-weight-normal: 400; --font-weight-bold: 500; --main-layout-header: 40px; - --drag-region-height: 22px + --drag-region-height: 22px; } *, *:before, *:after { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 89a04a58d3..948fec03ce 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -71,6 +71,8 @@ import { CommandContainer } from "./command-palette/command-container"; import { KubeObjectStore } from "../kube-object.store"; import { clusterContext } from "./context"; import { namespaceStore } from "./+namespaces/namespace.store"; +import { Sidebar } from "./layout/sidebar"; +import { Dock } from "./dock"; @observer export class App extends React.Component { @@ -176,7 +178,7 @@ export class App extends React.Component { return ( - + } footer={}> diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss index 0c7c450f1b..16f77051c9 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.scss +++ b/src/renderer/components/cluster-manager/cluster-manager.scss @@ -32,6 +32,7 @@ grid-area: main; position: relative; display: flex; + flex-direction: column; } .HotbarMenu { @@ -45,7 +46,7 @@ #lens-views { position: absolute; left: 0; - top: 0; + top: var(--main-layout-header); // Move below the TopBar right: 0; bottom: 0; display: flex; diff --git a/src/renderer/components/cluster-manager/cluster-topbar.tsx b/src/renderer/components/cluster-manager/cluster-topbar.tsx new file mode 100644 index 0000000000..af49194056 --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-topbar.tsx @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { catalogURL } from "../+catalog"; +import type { Cluster } from "../../../main/cluster"; +import { navigate } from "../../navigation"; +import { Icon } from "../icon"; +import { TopBar } from "../layout/topbar"; +import { MaterialTooltip } from "../material-tooltip/material-tooltip"; + +interface Props { + cluster: Cluster +} + +export function ClusterTopbar({ cluster }: Props) { + return ( + +
+ + navigate(catalogURL())}/> + +
+
+ ); +} diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index fc9ba90807..f1530032e3 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -32,13 +32,13 @@ import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { navigate } from "../../navigation"; import { catalogURL } from "../+catalog/catalog.route"; +import { ClusterTopbar } from "./cluster-topbar"; import type { RouteComponentProps } from "react-router-dom"; import type { IClusterViewRouteParams } from "./cluster-view.route"; interface Props extends RouteComponentProps { } - @observer export class ClusterView extends React.Component { private store = ClusterStore.getInstance(); @@ -103,7 +103,8 @@ export class ClusterView extends React.Component { render() { return ( -
+
+ {this.cluster && } {this.renderStatus()}
); diff --git a/src/renderer/components/icon/icon.scss b/src/renderer/components/icon/icon.scss index 124e2c1cb9..b10eb3708b 100644 --- a/src/renderer/components/icon/icon.scss +++ b/src/renderer/components/icon/icon.scss @@ -135,7 +135,7 @@ &.interactive { cursor: pointer; transition: 250ms color, 250ms opacity, 150ms background-color, 150ms box-shadow; - border-radius: 50%; + border-radius: var(--border-radius); &.focusable:focus:not(:hover) { box-shadow: 0 0 0 2px var(--focus-color); diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss index a91e2a2a18..2609d0b323 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -28,7 +28,7 @@ padding: var(--flex-gap); .title { - color: $textColorPrimary; + color: var(--textColorTertiary); } .info-panel { diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx deleted file mode 100644 index ad50d77fe8..0000000000 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -jest.mock("../../../../common/ipc"); - -import React from "react"; -import { render } from "@testing-library/react"; -import "@testing-library/jest-dom/extend-expect"; - -import { MainLayoutHeader } from "../main-layout-header"; -import { Cluster } from "../../../../main/cluster"; -import { ClusterStore } from "../../../../common/cluster-store"; -import mockFs from "mock-fs"; -import { ThemeStore } from "../../../theme.store"; -import { UserStore } from "../../../../common/user-store"; - -jest.mock("electron", () => { - return { - app: { - getVersion: () => "99.99.99", - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - }; -}); - -describe("", () => { - let cluster: Cluster; - - beforeEach(() => { - const mockOpts = { - "minikube-config.yml": JSON.stringify({ - apiVersion: "v1", - clusters: [{ - name: "minikube", - cluster: { - server: "https://192.168.64.3:8443", - }, - }], - contexts: [{ - context: { - cluster: "minikube", - user: "minikube", - }, - name: "minikube", - }], - users: [{ - name: "minikube", - }], - kind: "Config", - preferences: {}, - }) - }; - - mockFs(mockOpts); - - UserStore.createInstance(); - ThemeStore.createInstance(); - ClusterStore.createInstance(); - - cluster = new Cluster({ - id: "foo", - contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - }); - }); - - afterEach(() => { - ClusterStore.resetInstance(); - ThemeStore.resetInstance(); - UserStore.resetInstance(); - mockFs.restore(); - }); - - it("renders w/o errors", () => { - const { container } = render(); - - expect(container).toBeInstanceOf(HTMLElement); - }); - - it("renders cluster name", () => { - const { getByText } = render(); - - expect(getByText("minikube")).toBeInTheDocument(); - }); -}); diff --git a/src/renderer/components/layout/main-layout.module.css b/src/renderer/components/layout/main-layout.module.css new file mode 100644 index 0000000000..abee3c0258 --- /dev/null +++ b/src/renderer/components/layout/main-layout.module.css @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.mainLayout { + display: grid; + grid-template-areas: + "sidebar contents" + "sidebar footer"; + grid-template-rows: [contents] 1fr [footer] auto; + grid-template-columns: [sidebar] var(--sidebar-width) [contents] 1fr; + width: 100%; + z-index: 1; + height: calc(100% - var(--main-layout-header)); +} + +.sidebar { + grid-area: sidebar; + display: flex; + position: relative; + background: var(--sidebarBackground); +} + +.contents { + grid-area: contents; + overflow: auto; +} + +.footer { + position: relative; + grid-area: footer; + min-width: 0; /* restrict size when overflow content (e.g. tabs scrolling) */ +} \ No newline at end of file diff --git a/src/renderer/components/layout/main-layout.scss b/src/renderer/components/layout/main-layout.scss deleted file mode 100755 index a2deeea392..0000000000 --- a/src/renderer/components/layout/main-layout.scss +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -.MainLayout { - display: grid; - grid-template-areas: - "aside header" - "aside tabs" - "aside main" - "aside footer"; - grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto; - grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr; - height: 100%; - - > header { - grid-area: header; - background: $layoutBackground; - padding: $padding $padding * 2; - } - - > aside { - grid-area: aside; - position: relative; - background: $sidebarBackground; - white-space: nowrap; - transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1); - width: var(--sidebar-width); - - &.compact { - position: absolute; - width: var(--main-layout-header); - height: 100%; - overflow: hidden; - - &:hover { - width: var(--sidebar-width); - transition-delay: 750ms; - box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35); - z-index: $zIndex-sidebar-hover; - } - } - } - - > main { - display: contents; - - > * { - grid-area: main; - overflow: auto; - } - } - - footer { - position: relative; - grid-area: footer; - min-width: 0; // restrict size when overflow content (e.g. tabs scrolling) - } -} diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx index 455f0dfdfc..a85e90b158 100755 --- a/src/renderer/components/layout/main-layout.tsx +++ b/src/renderer/components/layout/main-layout.tsx @@ -19,73 +19,53 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./main-layout.scss"; +import styles from "./main-layout.module.css"; import React from "react"; import { observer } from "mobx-react"; -import { getHostedCluster } from "../../../common/cluster-store"; import { cssNames } from "../../utils"; -import { Dock } from "../dock"; import { ErrorBoundary } from "../error-boundary"; import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; -import { MainLayoutHeader } from "./main-layout-header"; -import { Sidebar } from "./sidebar"; import { sidebarStorage } from "./sidebar-storage"; -export interface MainLayoutProps { - className?: any; +interface Props { + sidebar: React.ReactNode; + className?: string; footer?: React.ReactNode; - headerClass?: string; - footerClass?: string; } @observer -export class MainLayout extends React.Component { - onSidebarCompactModeChange = () => { - sidebarStorage.merge(draft => { - draft.compact = !draft.compact; - }); - }; - +export class MainLayout extends React.Component { onSidebarResize = (width: number) => { sidebarStorage.merge({ width }); }; render() { - const cluster = getHostedCluster(); - const { onSidebarCompactModeChange, onSidebarResize } = this; - const { className, headerClass, footer, footerClass, children } = this.props; - const { compact, width: sidebarWidth } = sidebarStorage.get(); + const { onSidebarResize } = this; + const { className, footer, children, sidebar } = this.props; + const { width: sidebarWidth } = sidebarStorage.get(); const style = { "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties; - if (!cluster) { - return null; // fix: skip render when removing active (visible) cluster - } - return ( -
- - - +
-
+
{children} -
+
-
{footer ?? }
+
{footer}
); } diff --git a/src/renderer/components/layout/sidebar-item.tsx b/src/renderer/components/layout/sidebar-item.tsx index 61957a61a3..113cef6634 100644 --- a/src/renderer/components/layout/sidebar-item.tsx +++ b/src/renderer/components/layout/sidebar-item.tsx @@ -62,10 +62,6 @@ export class SidebarItem extends React.Component { return this.props.id; } - @computed get compact(): boolean { - return Boolean(sidebarStorage.get().compact); - } - @computed get expanded(): boolean { return Boolean(sidebarStorage.get().expanded[this.id]); } @@ -78,8 +74,6 @@ export class SidebarItem extends React.Component { } @computed get isExpandable(): boolean { - if (this.compact) return false; // not available in compact-mode currently - return Boolean(this.props.children); } @@ -108,10 +102,8 @@ export class SidebarItem extends React.Component { if (isHidden) return null; - const { isActive, id, compact, expanded, isExpandable, toggleExpand } = this; - const classNames = cssNames(SidebarItem.displayName, className, { - compact, - }); + const { isActive, id, expanded, isExpandable, toggleExpand } = this; + const classNames = cssNames(SidebarItem.displayName, className); return (
diff --git a/src/renderer/components/layout/sidebar-storage.ts b/src/renderer/components/layout/sidebar-storage.ts index 976f40b31c..663cb7c177 100644 --- a/src/renderer/components/layout/sidebar-storage.ts +++ b/src/renderer/components/layout/sidebar-storage.ts @@ -23,14 +23,12 @@ import { createStorage } from "../../utils"; export interface SidebarStorageState { width: number; - compact: boolean; expanded: { [itemId: string]: boolean; } } export const sidebarStorage = createStorage("sidebar", { - width: 200, // sidebar size in non-compact mode - compact: false, // compact-mode (icons only) + width: 200, expanded: {}, }); diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index 6212758905..f0d777fadd 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -23,49 +23,9 @@ $iconSize: 24px; $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); - &.compact { - .sidebar-nav { - @include hidden-scrollbar; // fix: scrollbar overlaps icons - } - } - - .header { - background: $sidebarLogoBackground; - padding: $padding / 2; - height: var(--main-layout-header); - - a { - font-size: 18.5px; - text-decoration: none; - } - - div.logo-text { - position: absolute; - left: 42px; - top: 11px; - } - - .logo-icon { - width: 28px; - height: 28px; - margin-left: 2px; - margin-top: 2px; - margin-right: 10px; - - svg { - --size: 28px; - padding: 2px; - } - } - - .pin-icon { - margin: auto; - margin-right: $padding / 2; - } - } - .sidebar-nav { - padding: $padding / 1.5 0; + width: var(--sidebar-width); + padding-bottom: calc(var(--padding) * 3); overflow: auto; .Icon { diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 30b14919e9..6ecc9b8df9 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -24,7 +24,6 @@ import type { TabLayoutRoute } from "./tab-layout"; import React from "react"; import { observer } from "mobx-react"; -import { NavLink } from "react-router-dom"; import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route"; @@ -52,8 +51,6 @@ import { SidebarItem } from "./sidebar-item"; interface Props { className?: string; - compact?: boolean; // compact-mode view: show only icons and expand on :hover - toggle(): void; // compact-mode updater } @observer @@ -173,24 +170,11 @@ export class Sidebar extends React.Component { } render() { - const { toggle, compact, className } = this.props; + const { className } = this.props; return ( -
-
- - -
Lens
-
- -
-
+
+
.Tabs { - grid-area: tabs; background: $layoutTabsBackground; + min-height: 32px; } - main { $spacing: $margin * 2; - grid-area: main; + flex-grow: 1; overflow-y: scroll; // always reserve space for scrollbar (17px) overflow-x: auto; margin: $spacing; diff --git a/__mocks__/@linguiMacro.ts b/src/renderer/components/layout/topbar.module.css similarity index 70% rename from __mocks__/@linguiMacro.ts rename to src/renderer/components/layout/topbar.module.css index 35fca119d9..08378bb1d4 100644 --- a/__mocks__/@linguiMacro.ts +++ b/src/renderer/components/layout/topbar.module.css @@ -18,7 +18,28 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export default { - Trans: ({ children }: { children: React.ReactNode }) => children, - t: (message: string) => message -}; + +.topBar { + display: grid; + grid-template-columns: [title] 1fr [controls] auto; + grid-template-rows: var(--main-layout-header); + grid-template-areas: "title controls"; + background-color: var(--layoutBackground); + z-index: 1; + width: 100%; +} + +.title { + @apply font-bold px-6; + color: var(--textColorAccent); + align-items: center; + display: flex; +} + +.controls { + align-self: flex-end; + padding-right: 1.5rem; + align-items: center; + display: flex; + height: 100%; +} \ No newline at end of file diff --git a/src/renderer/components/layout/main-layout-header.tsx b/src/renderer/components/layout/topbar.tsx similarity index 75% rename from src/renderer/components/layout/main-layout-header.tsx rename to src/renderer/components/layout/topbar.tsx index 354794e363..2fe302ce55 100644 --- a/src/renderer/components/layout/main-layout-header.tsx +++ b/src/renderer/components/layout/topbar.tsx @@ -19,20 +19,19 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import styles from "./topbar.module.css"; import React from "react"; import { observer } from "mobx-react"; -import type { Cluster } from "../../../main/cluster"; -import { cssNames } from "../../utils"; -interface Props { - cluster: Cluster - className?: string +interface Props extends React.HTMLAttributes { + label: React.ReactNode; } -export const MainLayoutHeader = observer(({ cluster, className }: Props) => { +export const TopBar = observer(({ label, children, ...rest }: Props) => { return ( -
- {cluster.name} -
+
+
{label}
+
{children}
+
); }); diff --git a/src/renderer/components/resizing-anchor/resizing-anchor.scss b/src/renderer/components/resizing-anchor/resizing-anchor.scss index 0896a9ee62..024a83105b 100644 --- a/src/renderer/components/resizing-anchor/resizing-anchor.scss +++ b/src/renderer/components/resizing-anchor/resizing-anchor.scss @@ -31,6 +31,23 @@ body.resizing { position: absolute; z-index: 10; + &::after { + content: " "; + display: block; + width: 3px; + height: 100%; + margin-left: 50%; + background: transparent; + transition: all 0.2s 0s; + } + + &:hover { + &::after { + background: var(--blue); + transition: all 0.2s 0.5s; + } + } + &.disabled { display: none; } @@ -56,6 +73,17 @@ body.resizing { cursor: col-resize; width: $dimension; + // Expand hoverable area while resizing to keep highlighting resizer. + // Otherwise, cursor can move far away dropping hover indicator. + .resizing & { + $expandedWidth: 200px; + width: $expandedWidth; + + &.trailing { + right: -$expandedWidth / 2; + } + } + &.leading { left: -$dimension / 2; } diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.json index f96eb81453..822a5f1c36 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -26,6 +26,7 @@ "sidebarLogoBackground": "#414448", "sidebarActiveColor": "#ffffff", "sidebarSubmenuActiveColor": "#ffffff", + "sidebarItemHoverBackground": "#3a3e44", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", "buttonLightBackground": "#f1f1f1", @@ -109,7 +110,7 @@ "addClusterIconColor": "#252729", "boxShadow": "#0000003a", "iconActiveColor": "#ffffff", - "iconActiveBackground": "#ffffff22", + "iconActiveBackground": "#ffffff18", "filterAreaBackground": "#23272b", "chartLiveBarBackgound": "#00000033", "chartStripesColor": "#ffffff08", diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index d3b5938de3..8d9339d71a 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -27,6 +27,7 @@ "sidebarActiveColor": "#ffffff", "sidebarSubmenuActiveColor": "#3d90ce", "sidebarBackground": "#e8e8e8", + "sidebarItemHoverBackground": "#f0f2f5", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", "buttonLightBackground": "#f1f1f1", From 96aa9f16e1395521d2e183247edba61f5ed75ad9 Mon Sep 17 00:00:00 2001 From: Mario Sarcher Date: Mon, 7 Jun 2021 11:06:54 +0200 Subject: [PATCH 07/33] Always publish npm package on master branch updates (#2970) Signed-off-by: Mario Sarcher --- .github/workflows/publish-master-npm.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish-master-npm.yml b/.github/workflows/publish-master-npm.yml index e470289e9e..c6c4284e6a 100644 --- a/.github/workflows/publish-master-npm.yml +++ b/.github/workflows/publish-master-npm.yml @@ -30,7 +30,6 @@ jobs: make build-npm - name: publish new release - if: contains(github.ref, 'refs/tags/v') run: | make publish-npm env: From f53bb9be931d9f76b08dfa22b2df12387b7b5529 Mon Sep 17 00:00:00 2001 From: Mario Sarcher Date: Mon, 7 Jun 2021 13:33:30 +0200 Subject: [PATCH 08/33] Publish NPM package on push to master branch (#2973) Signed-off-by: Mario Sarcher --- .github/workflows/publish-master-npm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-master-npm.yml b/.github/workflows/publish-master-npm.yml index c6c4284e6a..daa3d1abe3 100644 --- a/.github/workflows/publish-master-npm.yml +++ b/.github/workflows/publish-master-npm.yml @@ -1,6 +1,6 @@ name: Publish NPM Package `master` on: - pull_request: + push: branches: - master types: From c272ccb48a8c8eed332f2b50fe1607d096f9cc2a Mon Sep 17 00:00:00 2001 From: Mario Sarcher Date: Mon, 7 Jun 2021 13:43:52 +0200 Subject: [PATCH 09/33] Publish npm master remove type (#2974) * Publish NPM package on push to master branch Signed-off-by: Mario Sarcher * Remove type closed to run properly on master branch commits Signed-off-by: Mario Sarcher --- .github/workflows/publish-master-npm.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish-master-npm.yml b/.github/workflows/publish-master-npm.yml index daa3d1abe3..2be9721d3e 100644 --- a/.github/workflows/publish-master-npm.yml +++ b/.github/workflows/publish-master-npm.yml @@ -3,8 +3,6 @@ on: push: branches: - master - types: - - closed jobs: publish: name: Publish NPM Package `master` From d21f99e6096b89fb5d296942089f64a7da006ce9 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 7 Jun 2021 08:08:39 -0400 Subject: [PATCH 10/33] Await `onActivate`/`onDeactive` of extensions (#2963) Signed-off-by: Sebastian Malton --- src/extensions/extension-loader.ts | 7 ++- src/extensions/lens-extension.ts | 73 ++++++++++++------------------ 2 files changed, 32 insertions(+), 48 deletions(-) diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 6a4af088e8..da2669550f 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -26,7 +26,7 @@ import { action, computed, makeObservable, observable, reaction, when } from "mo import path from "path"; import { getHostedCluster } from "../common/cluster-store"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; -import { Singleton, toJS } from "../common/utils"; +import { Disposer, Singleton, toJS } from "../common/utils"; import logger from "../main/logger"; import type { InstalledExtension } from "./extension-discovery"; import { ExtensionsStore } from "./extensions-store"; @@ -296,7 +296,7 @@ export class ExtensionLoader extends Singleton { }); } - protected autoInitExtensions(register: (ext: LensExtension) => Promise) { + protected autoInitExtensions(register: (ext: LensExtension) => Promise) { return reaction(() => this.toJSON(), installedExtensions => { for (const [extId, extension] of installedExtensions) { const alreadyInit = this.instances.has(extId); @@ -311,8 +311,7 @@ export class ExtensionLoader extends Singleton { const instance = new LensExtensionClass(extension); - instance.whenEnabled(() => register(instance)); - instance.enable(); + instance.enable(register); this.instances.set(extId, instance); } catch (err) { logger.error(`${logModule}: activation extension error`, { ext: extension, err }); diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 6f03829121..f3910308fd 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -20,11 +20,11 @@ */ import type { InstalledExtension } from "./extension-discovery"; -import { action, observable, reaction, makeObservable } from "mobx"; +import { action, observable, makeObservable } from "mobx"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; import type { ProtocolHandlerRegistration } from "./registries"; -import { disposer } from "../common/utils"; +import { Disposer, disposer } from "../common/utils"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; @@ -83,59 +83,44 @@ export class LensExtension { } @action - async enable() { - if (this.isEnabled) return; - this.isEnabled = true; - this.onActivate?.(); - logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); + async enable(register: (ext: LensExtension) => Promise) { + if (this.isEnabled) { + return; + } + + try { + await this.onActivate(); + this.isEnabled = true; + + this[Disposers].push(...await register(this)); + logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); + } catch (error) { + logger.error(`[EXTENSION]: failed to activate ${this.name}@${this.version}: ${error}`); + } } @action async disable() { - if (!this.isEnabled) return; - this.isEnabled = false; - this.onDeactivate?.(); - this[Disposers](); - logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); - } + if (!this.isEnabled) { + return; + } - toggle(enable?: boolean) { - if (typeof enable === "boolean") { - enable ? this.enable() : this.disable(); - } else { - this.isEnabled ? this.disable() : this.enable(); + this.isEnabled = false; + + try { + await this.onDeactivate(); + this[Disposers](); + logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); + } catch (error) { + logger.error(`[EXTENSION]: disabling ${this.name}@${this.version} threw an error: ${error}`); } } - async whenEnabled(handlers: () => Promise) { - const disposers: Function[] = []; - const unregisterHandlers = () => { - disposers.forEach(unregister => unregister()); - disposers.length = 0; - }; - const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => { - if (isEnabled) { - const handlerDisposers = await handlers(); - - disposers.push(...handlerDisposers); - } else { - unregisterHandlers(); - } - }, { - fireImmediately: true - }); - - return () => { - unregisterHandlers(); - cancelReaction(); - }; - } - - protected onActivate(): void { + protected onActivate(): Promise | void { return; } - protected onDeactivate(): void { + protected onDeactivate(): Promise | void { return; } } From cb4a7497dd7b26abd20eb089535362061129c176 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 7 Jun 2021 08:13:26 -0400 Subject: [PATCH 11/33] Remove Namespace filter from ItemListLayout (#2952) --- .../+namespaces/namespace-select-filter.tsx | 4 +- .../item-object-list/filter-icon.tsx | 3 - .../item-object-list/item-list-layout.tsx | 20 +-- .../item-object-list/page-filters-select.tsx | 136 ------------------ .../item-object-list/page-filters.store.ts | 1 - 5 files changed, 2 insertions(+), 162 deletions(-) delete mode 100644 src/renderer/components/item-object-list/page-filters-select.tsx diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 25b14ae9e6..8ae2e97958 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -26,8 +26,6 @@ import { observer } from "mobx-react"; import { components, PlaceholderProps } from "react-select"; import { Icon } from "../icon"; -import { FilterIcon } from "../item-object-list/filter-icon"; -import { FilterType } from "../item-object-list/page-filters.store"; import { NamespaceSelect } from "./namespace-select"; import { namespaceStore } from "./namespace.store"; @@ -63,7 +61,7 @@ export class NamespaceSelectFilter extends React.Component { return (
- + {namespace} {isSelected && }
diff --git a/src/renderer/components/item-object-list/filter-icon.tsx b/src/renderer/components/item-object-list/filter-icon.tsx index f5534b2f71..87f41c524f 100644 --- a/src/renderer/components/item-object-list/filter-icon.tsx +++ b/src/renderer/components/item-object-list/filter-icon.tsx @@ -31,9 +31,6 @@ export function FilterIcon(props: Props) { const { type, ...iconProps } = props; switch (type) { - case FilterType.NAMESPACE: - return ; - case FilterType.SEARCH: return ; diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index f894f90a4a..647cbcaec9 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -35,7 +35,6 @@ import type { ItemObject, ItemStore } from "../../item.store"; import { SearchInputUrl } from "../input"; import { Filter, FilterType, pageFilters } from "./page-filters.store"; import { PageFiltersList } from "./page-filters-list"; -import { PageFiltersSelect } from "./page-filters-select"; import { ThemeStore } from "../../theme.store"; import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; @@ -179,16 +178,6 @@ export class ItemListLayout extends React.Component { return items; }, - - [FilterType.NAMESPACE]: items => { - const filterValues = pageFilters.getValues(FilterType.NAMESPACE); - - if (filterValues.length > 0) { - return items.filter(item => filterValues.includes(item.getNs())); - } - - return items; - }, }; @computed get isReady() { @@ -401,14 +390,7 @@ export class ItemListLayout extends React.Component { const placeholders: IHeaderPlaceholders = { title:
{title}
, info: this.renderInfo(), - filters: ( - <> - {showNamespaceSelectFilter && } - - - ), + filters: showNamespaceSelectFilter && , search: , }; let header = this.renderHeaderContent(placeholders); diff --git a/src/renderer/components/item-object-list/page-filters-select.tsx b/src/renderer/components/item-object-list/page-filters-select.tsx deleted file mode 100644 index 418c82045b..0000000000 --- a/src/renderer/components/item-object-list/page-filters-select.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import React from "react"; -import { observer } from "mobx-react"; -import { computed, makeObservable } from "mobx"; -import { GroupSelectOption, Select, SelectOption, SelectProps } from "../select"; -import { FilterType, pageFilters } from "./page-filters.store"; -import { namespaceStore } from "../+namespaces/namespace.store"; -import { Icon } from "../icon"; -import { FilterIcon } from "./filter-icon"; - -export interface SelectOptionFilter extends SelectOption { - type: FilterType; - selected?: boolean; -} - -interface Props extends SelectProps { - allowEmpty?: boolean; - disableFilters?: { - [filterType: string]: boolean; - }; -} - -@observer -export class PageFiltersSelect extends React.Component { - static defaultProps: Props = { - allowEmpty: true, - disableFilters: {}, - }; - - constructor(props: Props) { - super(props); - makeObservable(this); - } - - @computed get groupedOptions() { - const options: GroupSelectOption[] = []; - const { disableFilters } = this.props; - - if (!disableFilters[FilterType.NAMESPACE]) { - const selectedValues = pageFilters.getValues(FilterType.NAMESPACE); - - options.push({ - label: "Namespace", - options: namespaceStore.items.map(ns => { - const name = ns.getName(); - - return { - type: FilterType.NAMESPACE, - value: name, - icon: , - selected: selectedValues.includes(name), - }; - }) - }); - } - - return options; - } - - @computed get options(): SelectOptionFilter[] { - return this.groupedOptions.reduce((options, optGroup) => { - options.push(...optGroup.options); - - return options; - }, []); - } - - private formatLabel = (option: SelectOptionFilter) => { - const { label, value, type, selected } = option; - - return ( -
- - {label || String(value)} - {selected && } -
- ); - }; - - private onSelect = (option: SelectOptionFilter) => { - const { type, value, selected } = option; - const { addFilter, removeFilter } = pageFilters; - const filter = { type, value }; - - if (!selected) { - addFilter(filter); - } - else { - removeFilter(filter); - } - }; - - render() { - const { groupedOptions, formatLabel, onSelect, options } = this; - - if (!options.length && this.props.allowEmpty) { - return null; - } - const { allowEmpty, disableFilters, ...selectProps } = this.props; - const selectedOptions = options.filter(opt => opt.selected); - - return ( - this.proxyServer = value} - theme="round-black" - /> - - {"A HTTP proxy server URL (format: http://
:)."} - -
+ {this.allErrors.length > 0 && ( + <> +

KubeConfig Yaml Validation Errors:

+ {...this.allErrors.map(error =>
{error}
)} + )} - {this.error && ( -
{this.error}
- )} -
diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 2d4ccf3a38..c821f2874e 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -19,21 +19,23 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy"; - import "./cluster-status.scss"; -import React from "react"; -import { observer } from "mobx-react"; + import { ipcRenderer } from "electron"; import { computed, observable, makeObservable } from "mobx"; -import { requestMain, subscribeToBroadcast } from "../../../common/ipc"; -import { Icon } from "../icon"; -import { Button } from "../button"; -import { cssNames, IClassName } from "../../utils"; -import type { Cluster } from "../../../main/cluster"; -import { ClusterId, ClusterStore } from "../../../common/cluster-store"; -import { CubeSpinner } from "../spinner"; +import { observer } from "mobx-react"; +import React from "react"; import { clusterActivateHandler } from "../../../common/cluster-ipc"; +import { ClusterId, ClusterStore } from "../../../common/cluster-store"; +import { requestMain, subscribeToBroadcast } from "../../../common/ipc"; +import type { Cluster } from "../../../main/cluster"; +import { cssNames, IClassName } from "../../utils"; +import { Button } from "../button"; +import { Icon } from "../icon"; +import { CubeSpinner } from "../spinner"; +import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy"; +import { navigate } from "../../navigation"; +import { entitySettingsURL } from "../+entity-settings"; interface Props { className?: IClassName; @@ -82,6 +84,15 @@ export class ClusterStatus extends React.Component { this.isReconnecting = false; }; + manageProxySettings = () => { + navigate(entitySettingsURL({ + params: { + entityId: this.props.clusterId, + }, + fragment: "http-proxy", + })); + }; + renderContent() { const { authOutput, cluster, hasErrors } = this; const failureReason = cluster.failureReason; @@ -89,7 +100,7 @@ export class ClusterStatus extends React.Component { if (!hasErrors || this.isReconnecting) { return ( <> - +
             

{this.isReconnecting ? "Reconnecting..." : "Connecting..."}

{authOutput.map(({ data, error }, index) => { @@ -102,7 +113,7 @@ export class ClusterStatus extends React.Component { return ( <> - +

{cluster.preferences.clusterName}

@@ -121,6 +132,12 @@ export class ClusterStatus extends React.Component { onClick={this.reconnect} waiting={this.isReconnecting} /> +