diff --git a/package.json b/package.json index e211e4baba..490f9accda 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "http-proxy": "^1.18.1", "immer": "^9.0.6", "joi": "^17.4.2", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "jsdom": "^16.7.0", "jsonpath": "^1.1.1", "lodash": "^4.17.15", @@ -267,7 +267,7 @@ "@types/chart.js": "^2.9.34", "@types/color": "^3.0.2", "@types/crypto-js": "^3.1.47", - "@types/dompurify": "^2.0.2", + "@types/dompurify": "^2.3.1", "@types/electron-devtools-installer": "^2.2.0", "@types/fs-extra": "^9.0.1", "@types/glob-to-regexp": "^0.4.1", @@ -276,7 +276,7 @@ "@types/html-webpack-plugin": "^3.2.6", "@types/http-proxy": "^1.17.7", "@types/jest": "^26.0.24", - "@types/js-yaml": "^3.12.4", + "@types/js-yaml": "^4.0.2", "@types/jsdom": "^16.2.13", "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.155", @@ -346,7 +346,7 @@ "jest": "26.6.3", "jest-canvas-mock": "^2.3.1", "jest-fetch-mock": "^3.0.3", - "jest-mock-extended": "^1.0.16", + "jest-mock-extended": "^1.0.18", "make-plural": "^6.2.2", "mini-css-extract-plugin": "^1.6.2", "node-gyp": "7.1.2", @@ -361,7 +361,7 @@ "raw-loader": "^4.0.2", "react-beautiful-dnd": "^13.1.0", "react-refresh": "^0.9.0", - "react-router-dom": "^5.2.0", + "react-router-dom": "^5.3.0", "react-select": "3.2.0", "react-select-event": "^5.1.0", "react-table": "^7.7.0", diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 620debbb0b..8bb920de73 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -426,7 +426,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => it("replaces array format access token and expiry into string", async () => { const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath; const config = fs.readFileSync(file, "utf8"); - const kc = yaml.safeLoad(config); + const kc = yaml.load(config) as Record; expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string"); expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string"); diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 8207aaafac..e029835006 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -324,6 +324,15 @@ describe("HotbarStore", () => { console.error = error; console.warn = warn; }); + + it("checks if entity already pinned to hotbar", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + + expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy(); + expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy(); + }); }); describe("pre beta-5 migrations", () => { diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index 8309945f24..a613a7e44e 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -35,8 +35,11 @@ export type WebLinkSpec = { }; export class WebLink extends CatalogEntity { - public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; - public readonly kind = "WebLink"; + public static readonly apiVersion = "entity.k8slens.dev/v1alpha1"; + public static readonly kind = "WebLink"; + + public readonly apiVersion = WebLink.apiVersion; + public readonly kind = WebLink.kind; async onRun() { window.open(this.spec.url, "_blank"); diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 5bfb1ac73a..f7b7de642d 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -171,7 +171,7 @@ export class HotbarStore extends BaseStore { }}; - if (hotbar.items.find(i => i?.entity.uid === uid)) { + if (this.isAddedToActive(item)) { return; } @@ -274,6 +274,14 @@ export class HotbarStore extends BaseStore { hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id; } + + /** + * Checks if entity already pinned to hotbar + * @returns boolean + */ + isAddedToActive(entity: CatalogEntity) { + return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); + } } /** diff --git a/src/common/k8s-api/endpoints/helm-releases.api.ts b/src/common/k8s-api/endpoints/helm-releases.api.ts index 301e077e4e..7d6f45fa5c 100644 --- a/src/common/k8s-api/endpoints/helm-releases.api.ts +++ b/src/common/k8s-api/endpoints/helm-releases.api.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import jsYaml from "js-yaml"; +import yaml from "js-yaml"; import { autoBind, formatDuration } from "../../utils"; import capitalize from "lodash/capitalize"; import { apiBase } from "../index"; @@ -113,21 +113,31 @@ export async function getRelease(name: string, namespace: string): Promise { - const { repo, ...data } = payload; + const { repo, chart: rawChart, values: rawValues, ...data } = payload; + const chart = `${repo}/${rawChart}`; + const values = yaml.load(rawValues); - data.chart = `${repo}/${data.chart}`; - data.values = jsYaml.safeLoad(data.values); - - return apiBase.post(endpoint(), { data }); + return apiBase.post(endpoint(), { + data: { + chart, + values, + ...data, + } + }); } export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise { - const { repo, ...data } = payload; + const { repo, chart: rawChart, values: rawValues, ...data } = payload; + const chart = `${repo}/${rawChart}`; + const values = yaml.load(rawValues); - data.chart = `${repo}/${data.chart}`; - data.values = jsYaml.safeLoad(data.values); - - return apiBase.put(endpoint({ name, namespace }), { data }); + return apiBase.put(endpoint({ name, namespace }), { + data: { + chart, + values, + ...data, + } + }); } export async function deleteRelease(name: string, namespace: string): Promise { diff --git a/src/common/k8s-api/endpoints/resource-applier.api.ts b/src/common/k8s-api/endpoints/resource-applier.api.ts index 14b5d5ad46..ea86c266a2 100644 --- a/src/common/k8s-api/endpoints/resource-applier.api.ts +++ b/src/common/k8s-api/endpoints/resource-applier.api.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import jsYaml from "js-yaml"; +import yaml from "js-yaml"; import type { KubeJsonApiData } from "../kube-json-api"; import { apiBase } from "../index"; import type { Patch } from "rfc6902"; @@ -30,7 +30,13 @@ export const annotations = [ export async function update(resource: object | string): Promise { if (typeof resource === "string") { - resource = jsYaml.safeLoad(resource); + const parsed = yaml.load(resource); + + if (typeof parsed !== "object") { + throw new Error("Cannot update resource to string or number"); + } + + resource = parsed; } return apiBase.post("/stack", { data: resource }); diff --git a/src/common/k8s/resource-stack.ts b/src/common/k8s/resource-stack.ts index 9ea9d2609f..650650ff92 100644 --- a/src/common/k8s/resource-stack.ts +++ b/src/common/k8s/resource-stack.ts @@ -133,7 +133,7 @@ export class ResourceStack { if (!resourceData.trim()) continue; - const resourceArray = yaml.safeLoadAll(resourceData.toString()); + const resourceArray = yaml.loadAll(resourceData.toString()); resourceArray.forEach((resource) => { if (resource?.metadata) { @@ -143,7 +143,7 @@ export class ResourceStack { resource.metadata.labels["app.kubernetes.io/created-by"] = "resource-stack"; } - resources.push(yaml.safeDump(resource)); + resources.push(yaml.dump(resource)); }); } diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index 16046b4676..1b91a35e56 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -111,7 +111,7 @@ interface OptionsResult { } function loadToOptions(rawYaml: string): OptionsResult { - const parsed = yaml.safeLoad(rawYaml); + const parsed = yaml.load(rawYaml); const { error } = kubeConfigSchema.validate(parsed, { abortEarly: false, allowUnknown: true, @@ -248,7 +248,7 @@ export function dumpConfigYaml(kubeConfig: Partial): string { logger.debug("Dumping KubeConfig:", config); // skipInvalid: true makes dump ignore undefined values - return yaml.safeDump(config, { skipInvalid: true }); + return yaml.dump(config, { skipInvalid: true }); } /** diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index b8261845db..e99554dcd0 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -29,6 +29,11 @@ import { helmCli } from "./helm-cli"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; import { sortCharts } from "../../common/utils"; +export interface HelmCacheFile { + apiVersion: string; + entries: RepoHelmChartList; +} + export class HelmChartManager { static #cache = new Map(); @@ -82,7 +87,11 @@ export class HelmChartManager { protected async cachedYaml(): Promise { if (!HelmChartManager.#cache.has(this.repo.name)) { const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8"); - const { entries } = yaml.safeLoad(cacheFile) as { entries: RepoHelmChartList }; + const data = yaml.load(cacheFile) as string | number | HelmCacheFile; + + if (typeof data !== "object" || !data) { + return {}; + } /** * Do some initial preprocessing on the data, so as to avoid needing to do it later @@ -92,7 +101,7 @@ export class HelmChartManager { */ const normalized = Object.fromEntries( - Object.entries(entries) + Object.entries(data.entries) .map(([name, charts]) => [ name, sortCharts( diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 571628789e..d0ca2e8427 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -53,7 +53,7 @@ export async function installChart(chart: string, values: any, name: string | un const helm = await helmCli.binaryPath(); const fileName = tempy.file({ name: "values.yaml" }); - await fse.writeFile(fileName, yaml.safeDump(values)); + await fse.writeFile(fileName, yaml.dump(values)); try { let generateName = ""; @@ -83,7 +83,7 @@ export async function upgradeRelease(name: string, chart: string, values: any, n const helm = await helmCli.binaryPath(); const fileName = tempy.file({ name: "values.yaml" }); - await fse.writeFile(fileName, yaml.safeDump(values)); + await fse.writeFile(fileName, yaml.dump(values)); try { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index 9e4038a10f..2da7cfff18 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -101,18 +101,28 @@ export class HelmRepoManager extends Singleton { return repos.find(repo => repo.name === name); } + private async readConfig(): Promise { + try { + const rawConfig = await readFile(this.helmEnv.HELM_REPOSITORY_CONFIG, "utf8"); + const parsedConfig = yaml.load(rawConfig); + + if (typeof parsedConfig === "object" && parsedConfig) { + return parsedConfig as HelmRepoConfig; + } + } catch { } + + return { + repositories: [] + }; + } + public async repositories(): Promise { try { if (!this.initialized) { await this.init(); } - const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG; - const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8") - .then((yamlContent: string) => yaml.safeLoad(yamlContent)) - .catch(() => ({ - repositories: [] - })); + const { repositories } = await this.readConfig(); if (!repositories.length) { await HelmRepoManager.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" }); diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index a305ff1091..e6200b9d30 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -78,7 +78,7 @@ export class ResourceApplier { resource = this.sanitizeObject(resource); appEventBus.emit({ name: "resource", action: "apply" }); - return this.kubectlApply(yaml.safeDump(resource)); + return this.kubectlApply(yaml.dump(resource)); } protected async kubectlApply(content: string): Promise { diff --git a/src/migrations/cluster-store/2.6.0-beta.3.ts b/src/migrations/cluster-store/2.6.0-beta.3.ts index 75662582e5..60373d756a 100644 --- a/src/migrations/cluster-store/2.6.0-beta.3.ts +++ b/src/migrations/cluster-store/2.6.0-beta.3.ts @@ -32,9 +32,13 @@ export default { const cluster = value[1]; if (!cluster.kubeConfig) continue; - const kubeConfig = yaml.safeLoad(cluster.kubeConfig); + const config = yaml.load(cluster.kubeConfig); - if (!kubeConfig.hasOwnProperty("users")) continue; + if (!config || typeof config !== "object" || !config.hasOwnProperty("users")) { + continue; + } + + const kubeConfig = config as Record; const userObj = kubeConfig.users[0]; if (userObj) { @@ -56,7 +60,7 @@ export default { name: userObj.name, user }]; - cluster.kubeConfig = yaml.safeDump(kubeConfig); + cluster.kubeConfig = yaml.dump(kubeConfig); store.set(clusterKey, cluster); } } diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx index ac4d31092a..2d180bf9db 100644 --- a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx +++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx @@ -28,9 +28,9 @@ 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"; import type { CatalogEntityItem } from "./catalog-entity-item"; +import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; export interface CatalogEntityDrawerMenuProps extends MenuActionsProps { item: CatalogEntityItem | null | undefined; @@ -70,10 +70,6 @@ export class CatalogEntityDrawerMenu extends React.Comp } } - addToHotbar(entity: CatalogEntity): void { - HotbarStore.getInstance().addToHotbar(entity); - } - getMenuItems(entity: T): React.ReactChild[] { if (!entity) { return []; @@ -99,9 +95,12 @@ export class CatalogEntityDrawerMenu extends React.Comp } items.push( - this.addToHotbar(entity) }> - - + } + removeContent={} + /> ); return items; @@ -109,7 +108,7 @@ export class CatalogEntityDrawerMenu extends React.Comp render() { const { className, item: entity, ...menuProps } = this.props; - + if (!this.contextMenu || !entity.enabled) { return null; } diff --git a/src/renderer/components/+catalog/catalog.module.css b/src/renderer/components/+catalog/catalog.module.css index 4c529a0934..fd730ae1e7 100644 --- a/src/renderer/components/+catalog/catalog.module.css +++ b/src/renderer/components/+catalog/catalog.module.css @@ -19,10 +19,36 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +.list :global(.TableRow) { + .entityName { + position: relative; + width: min-content; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 24px; + + .pinIcon { + position: absolute; + right: 0; + opacity: 0; + transition: none; + + &:hover { + /* Drop styles defined for */ + background-color: transparent; + box-shadow: none; + } + } + } + + &:hover .pinIcon { + opacity: 1; + } +} + .iconCell { - max-width: 40px; - display: flex; - align-items: center; + @apply flex items-center max-w-[40px]; } .iconCell > div * { diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 417496a038..3357a7e7f3 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -46,6 +46,12 @@ jest.mock("@electron/remote", () => { }; }); +jest.mock("./hotbar-toggle-menu-item", () => { + return { + HotbarToggleMenuItem: () =>
menu item
+ }; +}); + class MockCatalogEntity extends CatalogEntity { public apiVersion = "api"; public kind = "kind"; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 5a479d437d..594bb610e3 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -37,13 +37,15 @@ import { CatalogAddButton } from "./catalog-add-button"; import type { RouteComponentProps } from "react-router"; import { Notifications } from "../notifications"; import { MainLayout } from "../layout/main-layout"; -import { createAppStorage, cssNames } from "../../utils"; +import { createAppStorage, cssNames, prevDefault } from "../../utils"; import { makeCss } from "../../../common/utils/makeCss"; import { CatalogEntityDetails } from "./catalog-entity-details"; import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes"; import { CatalogMenu } from "./catalog-menu"; import { HotbarIcon } from "../hotbar/hotbar-icon"; import { RenderDelay } from "../render-delay/render-delay"; +import { Icon } from "../icon"; +import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; export const previousActiveTab = createAppStorage("catalog-previous-active-tab", browseCatalogTab); @@ -129,6 +131,10 @@ export class Catalog extends React.Component { HotbarStore.getInstance().addToHotbar(item.entity); } + removeFromHotbar(item: CatalogEntityItem): void { + HotbarStore.getInstance().removeFromHotbar(item.getId()); + } + onDetails = (item: CatalogEntityItem) => { if (this.catalogEntityStore.selectedItemId) { this.catalogEntityStore.selectedItemId = null; @@ -194,13 +200,34 @@ export class Catalog extends React.Component { )) } - this.addToHotbar(item)}> - Pin to Hotbar - + ); }; + renderName(item: CatalogEntityItem) { + const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity); + + return ( +
+ {item.name} + isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))} + /> +
+ ); + } + renderIcon(item: CatalogEntityItem) { return ( @@ -231,7 +258,7 @@ export class Catalog extends React.Component { renderHeaderTitle={activeCategory?.metadata.name || "Browse All"} isSelectable={false} isConfigurable={true} - className="CatalogItemList" + className={styles.list} store={this.catalogEntityStore} sortingCallbacks={{ [sortBy.name]: item => item.name, @@ -255,7 +282,7 @@ export class Catalog extends React.Component { })} renderTableContents={item => [ this.renderIcon(item), - item.name, + this.renderName(item), !activeCategory && item.kind, item.source, item.getLabelBadges(), diff --git a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx new file mode 100644 index 0000000000..7d679e4d81 --- /dev/null +++ b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx @@ -0,0 +1,42 @@ +/** + * 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, { ReactNode, useState } from "react"; + +import { HotbarStore } from "../../../common/hotbar-store"; +import { MenuItem } from "../menu"; + +import type { CatalogEntity } from "../../api/catalog-entity"; + +export function HotbarToggleMenuItem(props: { entity: CatalogEntity, addContent: ReactNode, removeContent: ReactNode }) { + const store = HotbarStore.getInstance(false); + const add = () => store.addToHotbar(props.entity); + const remove = () => store.removeFromHotbar(props.entity.getId()); + const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity)); + + return ( + { + itemInHotbar ? remove() : add(); + setItemInHotbar(!itemInHotbar); + }}> + {itemInHotbar ? props.removeContent : props.addContent } + + ); +} diff --git a/src/renderer/components/+workloads-pods/pod-details-affinities.tsx b/src/renderer/components/+workloads-pods/pod-details-affinities.tsx index 9f9d99c343..81660ca965 100644 --- a/src/renderer/components/+workloads-pods/pod-details-affinities.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-affinities.tsx @@ -20,7 +20,7 @@ */ import React from "react"; -import jsYaml from "js-yaml"; +import yaml from "js-yaml"; import { DrawerItem, DrawerParamToggler } from "../drawer"; import type { DaemonSet, Deployment, Job, Pod, ReplicaSet, StatefulSet } from "../../../common/k8s-api/endpoints"; import { MonacoEditor } from "../monaco-editor"; @@ -41,7 +41,7 @@ export class PodDetailsAffinities extends React.Component {
- +
diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index c0b2874144..15fcdcbe8c 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -21,13 +21,12 @@ import React from "react"; import { GroupSelectOption, Select, SelectOption } from "../select"; -import jsYaml from "js-yaml"; +import yaml from "js-yaml"; import { computed, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { createResourceStore } from "./create-resource.store"; import { InfoPanel, InfoPanelProps } from "./info-panel"; import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api"; -import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api"; import { Notifications } from "../notifications"; import { TabKind } from "./dock.store"; import { dockViewsManager } from "./dock.views-manager"; @@ -58,36 +57,38 @@ export class CreateResourceInfoPanel extends React.Component { return createResourceStore.getData(this.tabId); } - create = async (): Promise => { - if (!this.draft) return; // skip: empty draft - - // skip empty documents if "---" pasted at the beginning or end - const resources = jsYaml.safeLoadAll(this.draft).filter(Boolean); - const createdResources: string[] = []; - const errors: string[] = []; - - await Promise.all( - resources.map(data => { - return resourceApplierApi.update(data) - .then(item => createdResources.push(item.metadata.name)) - .catch((err: JsonApiErrorParsed) => errors.push(err.toString())); - }) - ); - - if (errors.length) { - errors.forEach(error => Notifications.error(error)); - if (!createdResources.length) throw errors[0]; + create = async (): Promise => { + if (this.error || !this.data.trim()) { + // do not save when field is empty or there is an error + return null; } - const successMessage = ( -

- {createdResources.length === 1 ? "Resource" : "Resources"}{" "} - {createdResources.join(", ")} successfully created -

- ); - Notifications.ok(successMessage); + // skip empty documents + const resources = yaml.loadAll(this.data).filter(Boolean); + const createdResources: string[] = []; - return successMessage; + if (resources.length === 0) { + return void logger.info("Nothing to create"); + } + + for (const result of await Promise.allSettled(resources.map(resourceApplierApi.update))) { + if (result.status === "fulfilled") { + createdResources.push(result.value.metadata.name); + } else { + Notifications.error(result.reason.toString()); + } + } + + if (createdResources.length > 0) { + Notifications.ok(( +

+ {createdResources.length === 1 ? "Resource" : "Resources"}{" "} + {createdResources.join(", ")} successfully created +

+ )); + } + + return undefined; }; renderControls() { diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index 6a1acea562..a12553c954 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -42,29 +42,54 @@ export class EditResourceInfoPanel extends React.Component { return editResourceStore.getResource(this.tabId); } - get draft(): string { - return editResourceStore.getData(this.tabId)?.draft; - } - - saveDraft(draft: string | object) { - if (typeof draft === "object") { - draft = draft ? jsYaml.safeDump(draft) : undefined; + @computed get draft(): string { + if (!this.isReadyForEditing) { + return ""; // wait until tab's data and kube-object resource are loaded } - editResourceStore.getData(this.tabId).draft = draft; + const { draft } = editResourceStore.getData(this.tabId); + + if (typeof draft === "string") { + return draft; + } + + return yaml.dump(this.resource.toPlainObject()); // dump resource first time } - save = async () => { - const store = editResourceStore.getStore(this.tabId); - const updatedResource: KubeObject = await store.update(this.resource, jsYaml.safeLoad(this.draft)); + @action + saveDraft(draft: string | object) { + if (typeof draft === "object") { + draft = draft ? yaml.dump(draft) : undefined; + } - this.saveDraft(updatedResource.toPlainObject()); // update with new resourceVersion to avoid further errors on save - const resourceType = updatedResource.kind; - const resourceName = updatedResource.getName(); + editResourceStore.setData(this.tabId, { + firstDraft: draft, // this must be before the next line + ...editResourceStore.getData(this.tabId), + draft, + }); + } + + onChange = (draft: string, error?: string) => { + this.error = error; + this.saveDraft(draft); + }; + + save = async () => { + if (this.error) { + return null; + } + + const store = editResourceStore.getStore(this.tabId); + const currentVersion = yaml.load(this.draft); + const firstVersion = yaml.load(editResourceStore.getData(this.tabId).firstDraft ?? this.draft); + const patches = createPatch(firstVersion, currentVersion); + const updatedResource = await store.patch(this.resource, patches); + + editResourceStore.clearInitialDraft(this.tabId); return (

- {resourceType} {resourceName} updated. + {updatedResource.kind} {updatedResource.getName()} updated.

); }; diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 05ce960a23..24405da556 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -30,7 +30,6 @@ import { navigate } from "../../navigation"; import { cssNames, IClassName } from "../../utils"; import { Icon } from "../icon"; import { HotbarIcon } from "./hotbar-icon"; -import { HotbarStore } from "../../../common/hotbar-store"; import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster"; interface Props extends DOMAttributes { @@ -88,10 +87,6 @@ export class HotbarEntityIcon extends React.Component { return catalogEntityRegistry.activeEntity?.metadata?.uid == item.getId(); } - isPersisted(entity: CatalogEntity) { - return HotbarStore.getInstance().getActive().items.find((item) => item?.entity?.uid === entity.metadata.uid) !== undefined; - } - render() { if (!this.contextMenu) { return null; @@ -107,21 +102,13 @@ export class HotbarEntityIcon extends React.Component { disabled: !entity }); - const isPersisted = this.isPersisted(entity); const onOpen = async () => { const menuItems: CatalogEntityContextMenu[] = []; - if (!isPersisted) { - menuItems.unshift({ - title: "Pin to Hotbar", - onClick: () => add(entity, index) - }); - } else { - menuItems.unshift({ - title: "Unpin from Hotbar", - onClick: () => remove(entity.metadata.uid) - }); - } + menuItems.unshift({ + title: "Remove from Hotbar", + onClick: () => remove(entity.metadata.uid) + }); this.contextMenu.menuItems = menuItems; diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 9055279437..571b978a79 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -138,7 +138,7 @@ export class HotbarMenu extends React.Component { tooltip={`${item.entity.name} (${item.entity.source})`} menuItems={[ { - title: "Unpin from Hotbar", + title: "Remove from Hotbar", onClick: () => this.removeItem(item.entity.uid) } ]} diff --git a/src/renderer/components/icon/unpin.svg b/src/renderer/components/icon/unpin.svg new file mode 100644 index 0000000000..f685eb5e92 --- /dev/null +++ b/src/renderer/components/icon/unpin.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx b/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx index bf505a80a2..a61fb37622 100644 --- a/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx +++ b/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx @@ -24,7 +24,7 @@ import "./kubeconfig-dialog.scss"; import React from "react"; import { makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; -import jsYaml from "js-yaml"; +import yaml from "js-yaml"; import type { ServiceAccount } from "../../../common/k8s-api/endpoints"; import { copyToClipboard, cssNames, saveFileDialog } from "../../utils"; import { Button } from "../button"; @@ -86,7 +86,7 @@ export class KubeConfigDialog extends React.Component { this.close(); }); - this.config = config ? jsYaml.dump(config) : ""; + this.config = config ? yaml.dump(config) : ""; } copyToClipboard = () => { diff --git a/src/renderer/initializers/catalog-entity-detail-registry.tsx b/src/renderer/initializers/catalog-entity-detail-registry.tsx index 8cae26afbf..90605a084f 100644 --- a/src/renderer/initializers/catalog-entity-detail-registry.tsx +++ b/src/renderer/initializers/catalog-entity-detail-registry.tsx @@ -20,7 +20,7 @@ */ import React from "react"; -import { KubernetesCluster } from "../../common/catalog-entities"; +import { KubernetesCluster, WebLink } from "../../common/catalog-entities"; import { CatalogEntityDetailRegistry, CatalogEntityDetailsProps } from "../../extensions/registries"; import { DrawerItem, DrawerTitle } from "../components/drawer"; @@ -45,6 +45,20 @@ export function initCatalogEntityDetailRegistry() { ), }, - } + }, + { + apiVersions: [WebLink.apiVersion], + kind: WebLink.kind, + components: { + Details: ({ entity }: CatalogEntityDetailsProps) => ( + <> + + + {entity.spec.url} + + + ), + }, + }, ]); } diff --git a/yarn.lock b/yarn.lock index 485c0909ca..b736310b49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -272,10 +272,10 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.13.17" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec" - integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" + integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== dependencies: regenerator-runtime "^0.13.4" @@ -1450,10 +1450,10 @@ dependencies: "@types/ms" "*" -"@types/dompurify@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.2.tgz#94b5c05dc9b8a682a0abb4a8d6f0b82df61baeac" - integrity sha512-WHoQQTziRHm5/Fw/KsUKyh2V+wd3k2QUpJyjUXo8K7d9kMJ5i5wQnGDkO4URkwulhY2HuM/gbX25nSooi6+wUA== +"@types/dompurify@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.1.tgz#2934adcd31c4e6b02676f9c22f9756e5091c04dd" + integrity sha512-YJth9qa0V/E6/XPH1Jq4BC8uCMmO8V1fKWn8PCvuZcAhMn7q0ez9LW6naQT04UZzjFfAPhyRMZmI2a2rbMlEFA== dependencies: "@types/trusted-types" "*" @@ -1611,12 +1611,7 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/js-yaml@^3.12.4": - version "3.12.4" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.4.tgz#7d3b534ec35a0585128e2d332db1403ebe057e25" - integrity sha512-fYMgzN+9e28R81weVN49inn/u798ruU91En1ZnGvSZzCRc5jXx9B2EDhlRaWmcO1RIxFHL8AajRXzxDuJu93+A== - -"@types/js-yaml@^4.0.1": +"@types/js-yaml@^4.0.1", "@types/js-yaml@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.2.tgz#4117a7a378593a218e9d6f0ef44ce6d5d9edf7fa" integrity sha512-KbeHS/Y4R+k+5sWXEYzAZKuB1yQlZtEghuhRxrVRLaqhtoG5+26JwQsa4HyS3AWX8v1Uwukma5HheduUDskasA== @@ -8217,12 +8212,12 @@ jest-message-util@^26.6.2: slash "^3.0.0" stack-utils "^2.0.2" -jest-mock-extended@^1.0.16: - version "1.0.16" - resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.16.tgz#f6a96c795acc2f5ae9ee74d7b048c3b4399c6fc8" - integrity sha512-W1lI7cO0ZYmTQUBZH11nSDKyU36SxAyzBjZNxBWcpFnCKHGNWIPdFA8RJMhmobLQ8WLI4y9AMIbYzMN9q/GiRQ== +jest-mock-extended@^1.0.18: + version "1.0.18" + resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.18.tgz#27a40e882c09f8230243fc6765fe7285b03f2b4b" + integrity sha512-qf1n7lIa2dTxxPIBr+FlXrbj3hnV1sG9DPZsrr2H/8W+Jw0wt6OmeOQsPcjRuW8EXIECC9pDXsSIfEdn+HP7JQ== dependencies: - ts-essentials "^4.0.0" + ts-essentials "^7.0.2" jest-mock@^26.6.2: version "26.6.2" @@ -8447,7 +8442,7 @@ js-sha3@^0.8.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.14.0: +js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -11599,25 +11594,25 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== -react-router-dom@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" - integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== +react-router-dom@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" + integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ== dependencies: - "@babel/runtime" "^7.1.2" + "@babel/runtime" "^7.12.13" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.2.0" + react-router "5.2.1" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.2.0, react-router@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" - integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== +react-router@5.2.1, react-router@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d" + integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ== dependencies: - "@babel/runtime" "^7.1.2" + "@babel/runtime" "^7.12.13" history "^4.9.0" hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" @@ -13671,10 +13666,10 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-essentials@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab" - integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ== +ts-essentials@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" + integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== ts-jest@26.5.6: version "26.5.6"