diff --git a/package.json b/package.json index ebe48438c4..1ad83a1e35 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.0.0-beta.12", + "version": "5.0.0-beta.13", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index baa7fc1409..b637bddac3 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -72,7 +72,7 @@ export interface ClusterModel { workspace?: string; /** User context in kubeconfig */ - contextName?: string; + contextName: string; /** Preferences */ preferences?: ClusterPreferences; diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index be7c9e5c17..af69a6bef8 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -192,6 +192,10 @@ export class ExtensionLoader extends Singleton { } } + setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) { + this.extensions.get(lensExtensionId).isEnabled = isEnabled; + } + protected async initMain() { this.isLoaded = true; this.loadOnMain(); diff --git a/src/migrations/cluster-store/5.0.0-beta.13.ts b/src/migrations/cluster-store/5.0.0-beta.13.ts new file mode 100644 index 0000000000..09628ed234 --- /dev/null +++ b/src/migrations/cluster-store/5.0.0-beta.13.ts @@ -0,0 +1,120 @@ +/** + * 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 { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-store"; +import { MigrationDeclaration, migrationLog } from "../helpers"; +import { generateNewIdFor } from "../utils"; +import path from "path"; +import { app } from "electron"; +import { moveSync, removeSync } from "fs-extra"; + +function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { + if (left.prometheus && left.prometheusProvider) { + return { + prometheus: left.prometheus, + prometheusProvider: left.prometheusProvider, + }; + } + + if (right.prometheus && right.prometheusProvider) { + return { + prometheus: right.prometheus, + prometheusProvider: right.prometheusProvider, + }; + } + + return {}; +} + +function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences { + return { + terminalCWD: left.terminalCWD || right.terminalCWD || undefined, + clusterName: left.clusterName || right.clusterName || undefined, + iconOrder: left.iconOrder || right.iconOrder || undefined, + icon: left.icon || right.icon || undefined, + httpsProxy: left.httpsProxy || right.httpsProxy || undefined, + hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []), + ...mergePrometheusPreferences(left, right), + }; +} + +function mergeLabels(left: Record, right: Record): Record { + return { + ...right, + ...left + }; +} + +function mergeSet(left: Iterable, right: Iterable): string[] { + return [...new Set([...left, ...right])]; +} + +function mergeClusterModel(prev: ClusterModel, right: Omit): ClusterModel { + return { + id: prev.id, + kubeConfigPath: prev.id, + contextName: prev.contextName, + preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}), + metadata: prev.metadata, + labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}), + accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []), + }; +} + +function moveStorageFolder({ folder, newId, oldId }: { folder: string, newId: string, oldId: string }): void { + const oldPath = path.resolve(folder, `${oldId}.json`); + const newPath = path.resolve(folder, `${newId}.json`); + + try { + moveSync(oldPath, newPath); + } catch (error) { + if (String(error).includes("dest already exists")) { + migrationLog(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`); + removeSync(oldPath); + } + } +} + +export default { + version: "5.0.0-beta.13", + run(store) { + const folder = path.resolve(app.getPath("userData"), "lens-local-storage"); + + const oldClusters: ClusterModel[] = store.get("clusters"); + const clusters = new Map(); + + for (const { id: oldId, ...cluster } of oldClusters) { + const newId = generateNewIdFor(cluster); + + if (clusters.has(newId)) { + clusters.set(newId, mergeClusterModel(clusters.get(newId), cluster)); + } else { + clusters.set(newId, { + ...cluster, + id: newId, + }); + moveStorageFolder({ folder, newId, oldId }); + } + } + + store.set("clusters", [...clusters.values()]); + } +} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts index 9bf870825c..b68e098a16 100644 --- a/src/migrations/cluster-store/index.ts +++ b/src/migrations/cluster-store/index.ts @@ -31,6 +31,7 @@ import version270Beta0 from "./2.7.0-beta.0"; import version270Beta1 from "./2.7.0-beta.1"; import version360Beta1 from "./3.6.0-beta.1"; import version500Beta10 from "./5.0.0-beta.10"; +import version500Beta13 from "./5.0.0-beta.13"; import snap from "./snap"; export default joinMigrations( @@ -42,5 +43,6 @@ export default joinMigrations( version270Beta1, version360Beta1, version500Beta10, + version500Beta13, snap, ); diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index 1586f5c700..41b4aeb081 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -19,16 +19,16 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { createHash } from "crypto"; import { app } from "electron"; import fse from "fs-extra"; -import { isNull } from "lodash"; +import { isNull, uniqBy } from "lodash"; import path from "path"; import * as uuid from "uuid"; import type { ClusterStoreModel } from "../../common/cluster-store"; import { defaultHotbarCells, Hotbar, HotbarStore } from "../../common/hotbar-store"; import { catalogEntity } from "../../main/catalog-sources/general"; import type { MigrationDeclaration } from "../helpers"; +import { generateNewIdFor } from "../utils"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -47,6 +47,7 @@ export default { const workspaceStoreData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json")); const { clusters }: ClusterStoreModel = fse.readJSONSync(path.join(userDataPath, "lens-cluster-store.json")); const workspaceHotbars = new Map(); // mapping from WorkspaceId to HotBar + const uniqueClusters = uniqBy(clusters, "metadata.id"); // Filtering out duplicated clusters for (const { id, name } of workspaceStoreData.workspaces) { workspaceHotbars.set(id, { @@ -68,13 +69,13 @@ export default { }); } - for (const cluster of clusters) { + for (const cluster of uniqueClusters) { const workspaceHotbar = workspaceHotbars.get(cluster.workspace); if (workspaceHotbar?.items.length < defaultHotbarCells) { workspaceHotbar.items.push({ entity: { - uid: createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex"), + uid: generateNewIdFor(cluster), name: cluster.preferences.clusterName || cluster.contextName, } }); diff --git a/src/migrations/utils.ts b/src/migrations/utils.ts new file mode 100644 index 0000000000..f9f9985e38 --- /dev/null +++ b/src/migrations/utils.ts @@ -0,0 +1,26 @@ +/** + * 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 { createHash } from "crypto"; + +export function generateNewIdFor(cluster: { kubeConfigPath: string, contextName: string }): string { + return createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex"); +} diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 20707eac18..b135487343 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -278,7 +278,7 @@ async function unpackExtension(request: InstallRequestValidated, disposeDownload await when(() => ExtensionLoader.getInstance().userExtensions.has(id)); // Enable installed extensions by default. - ExtensionLoader.getInstance().userExtensions.get(id).isEnabled = true; + ExtensionLoader.getInstance().setIsEnabled(id, true); Notifications.ok(

Extension {displayName} successfully installed!

diff --git a/src/renderer/components/hotbar/hotbar-icon.scss b/src/renderer/components/hotbar/hotbar-icon.scss index c12c68f26f..2f9ad6a696 100644 --- a/src/renderer/components/hotbar/hotbar-icon.scss +++ b/src/renderer/components/hotbar/hotbar-icon.scss @@ -49,6 +49,10 @@ cursor: default; filter: grayscale(0.7); + &.contextMenuAvailable { + cursor: context-menu; + } + &:hover { &:not(.active) { box-shadow: none; diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 85cd9b6df2..4b28220958 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -97,7 +97,7 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, interactive = tr }; return ( -
+
0 })}> {tooltip && {tooltip}}
{renderIcon()} @@ -111,19 +111,17 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, interactive = tr toggleEvent="contextmenu" position={{right: true, bottom: true }} // FIXME: position does not work open={() => { - if (!disabled) { - onMenuOpen?.(); - toggleMenu(); - } + onMenuOpen?.(); + toggleMenu(); }} close={() => toggleMenu()}> - { menuItems.map((menuItem) => { - return ( - onMenuItemClick(menuItem) }> + { + menuItems.map((menuItem) => ( + onMenuItemClick(menuItem)}> {menuItem.title} - ); - })} + )) + }
); diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 5c05244e70..56fa7a25fc 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -27,7 +27,7 @@ import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store"; -import { CatalogEntity, CatalogEntityContextMenu, catalogEntityRunContext } from "../../api/catalog-entity"; +import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { HotbarSelector } from "./hotbar-selector"; import { HotbarCell } from "./hotbar-cell"; @@ -110,12 +110,6 @@ export class HotbarMenu extends React.Component { renderGrid() { return this.items.map((item, index) => { const entity = this.getEntity(item); - const disabledMenuItems: CatalogEntityContextMenu[] = [ - { - title: "Unpin from Hotbar", - onClick: () => this.removeItem(item.entity.uid) - } - ]; return ( @@ -163,8 +157,13 @@ export class HotbarMenu extends React.Component { uid={`hotbar-icon-${item.entity.uid}`} title={item.entity.name} source={item.entity.source} - menuItems={disabledMenuItems} tooltip={`${item.entity.name} (${item.entity.source})`} + menuItems={[ + { + title: "Unpin from Hotbar", + onClick: () => this.removeItem(item.entity.uid) + } + ]} disabled size={40} />