From fa5fe9e617ace5b1d95ba2344cbc21b9d8df88cd Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 18 Dec 2020 16:42:10 +0300 Subject: [PATCH 01/19] Fix: expand/collapse state for CRD sidebar items (#1751) * Moving SidebarNavItem component to its own file Signed-off-by: Alex Andreev * Using id prop to preserve expanding state Signed-off-by: Alex Andreev --- .../components/layout/sidebar-context.ts | 7 ++ .../components/layout/sidebar-nav-item.scss | 76 ++++++++++++ .../components/layout/sidebar-nav-item.tsx | 83 +++++++++++++ src/renderer/components/layout/sidebar.scss | 94 +------------- src/renderer/components/layout/sidebar.tsx | 116 +++--------------- 5 files changed, 190 insertions(+), 186 deletions(-) create mode 100644 src/renderer/components/layout/sidebar-context.ts create mode 100644 src/renderer/components/layout/sidebar-nav-item.scss create mode 100644 src/renderer/components/layout/sidebar-nav-item.tsx diff --git a/src/renderer/components/layout/sidebar-context.ts b/src/renderer/components/layout/sidebar-context.ts new file mode 100644 index 0000000000..fff192cba3 --- /dev/null +++ b/src/renderer/components/layout/sidebar-context.ts @@ -0,0 +1,7 @@ +import React from "react"; + +export const SidebarContext = React.createContext({ pinned: false }); + +export type SidebarContextValue = { + pinned: boolean; +}; \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-nav-item.scss b/src/renderer/components/layout/sidebar-nav-item.scss new file mode 100644 index 0000000000..24469dbd0f --- /dev/null +++ b/src/renderer/components/layout/sidebar-nav-item.scss @@ -0,0 +1,76 @@ +.SidebarNavItem { + $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); + + width: 100%; + user-select: none; + flex-shrink: 0; + + .nav-item { + cursor: pointer; + width: inherit; + display: flex; + align-items: center; + text-decoration: none; + border: none; + padding: $itemSpacing; + + &.active, &:hover { + background: $lensBlue; + color: $sidebarActiveColor; + } + } + + .expand-icon { + --size: 20px; + } + + .sub-menu { + border-left: 4px solid transparent; + + &.active { + border-left-color: $lensBlue; + } + + a, .SidebarNavItem { + display: block; + border: none; + text-decoration: none; + color: $textColorPrimary; + font-weight: normal; + padding-left: 40px; // parent icon width + overflow: hidden; + text-overflow: ellipsis; + line-height: 0px; // hidden by default + max-height: 0px; + opacity: 0; + transition: 125ms line-height ease-out, 200ms 100ms opacity; + + &.visible { + line-height: 28px; + max-height: 1000px; + opacity: 1; + } + + &.active, &:hover { + color: $sidebarSubmenuActiveColor; + } + } + + .sub-menu-parent { + padding-left: 27px; + font-weight: 500; + + .nav-item { + &:hover { + background: transparent; + } + } + + .sub-menu { + a { + padding-left: $padding * 3; + } + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-nav-item.tsx b/src/renderer/components/layout/sidebar-nav-item.tsx new file mode 100644 index 0000000000..7dfcbb50e6 --- /dev/null +++ b/src/renderer/components/layout/sidebar-nav-item.tsx @@ -0,0 +1,83 @@ +import "./sidebar-nav-item.scss"; + +import React from "react"; +import { computed, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { NavLink } from "react-router-dom"; + +import { createStorage, cssNames } from "../../utils"; +import { Icon } from "../icon"; +import { SidebarContext } from "./sidebar-context"; + +import type { TabLayoutRoute } from "./tab-layout"; +import type { SidebarContextValue } from "./sidebar-context"; + +interface SidebarNavItemProps { + id: string; // Used to save nav item collapse/expand state in local storage + url: string; + text: React.ReactNode | string; + className?: string; + icon?: React.ReactNode; + isHidden?: boolean; + isActive?: boolean; + subMenus?: TabLayoutRoute[]; +} + +const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []); +const navItemState = observable.map(navItemStorage.get()); + +reaction(() => [...navItemState], (value) => navItemStorage.set(value)); + +@observer +export class SidebarNavItem extends React.Component { + static contextType = SidebarContext; + public context: SidebarContextValue; + + @computed get isExpanded() { + return navItemState.get(this.props.id); + } + + toggleSubMenu = () => { + navItemState.set(this.props.id, !this.isExpanded); + }; + + render() { + const { isHidden, isActive, subMenus = [], icon, text, url, children, className, id } = this.props; + + if (isHidden) { + return null; + } + const extendedView = (subMenus.length > 0 || children) && this.context.pinned; + + if (extendedView) { + return ( +
+
+ {icon} + {text} + +
+
    + {subMenus.map(({ title, url }) => ( + + {title} + + ))} + {React.Children.toArray(children).map((child: React.ReactElement) => { + return React.cloneElement(child, { + className: cssNames(child.props.className, { visible: this.isExpanded }), + }); + })} +
+
+ ); + } + + return ( + isActive}> + {icon} + {text} + + ); + } +} diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index 1c5932b8d9..1379c87d9e 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -1,22 +1,7 @@ .Sidebar { $iconSize: 24px; - $activeBgc: $lensBlue; - $activeTextColor: $sidebarActiveColor; $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); - @mixin activeLinkState { - &.active { - background: $activeBgc; - color: $activeTextColor; - } - @media (hover: hover) { // only for devices supported "true" hover (with mouse or similar) - &:hover { - background: $activeBgc; - color: $activeTextColor; - } - } - } - &.pinned { .sidebar-nav { overflow: auto; @@ -77,13 +62,16 @@ } > a { - @include activeLinkState; - display: flex; align-items: center; text-decoration: none; border: none; padding: $itemSpacing; + + &.active, &:hover { + background: $lensBlue; + color: $sidebarActiveColor; + } } hr { @@ -91,78 +79,6 @@ } } - .SidebarNavItem { - width: 100%; - user-select: none; - flex-shrink: 0; - - .nav-item { - @include activeLinkState; - - cursor: pointer; - width: inherit; - display: flex; - align-items: center; - text-decoration: none; - border: none; - padding: $itemSpacing; - } - - .expand-icon { - --size: 20px; - } - - .sub-menu { - border-left: 4px solid transparent; - - &.active { - border-left-color: $activeBgc; - } - - a, .SidebarNavItem { - display: block; - border: none; - text-decoration: none; - color: $textColorPrimary; - font-weight: normal; - padding-left: 40px; // parent icon width - overflow: hidden; - text-overflow: ellipsis; - line-height: 0px; // hidden by default - max-height: 0px; - opacity: 0; - transition: 125ms line-height ease-out, 200ms 100ms opacity; - - &.visible { - line-height: 28px; - max-height: 1000px; - opacity: 1; - } - - &.active, &:hover { - color: $sidebarSubmenuActiveColor; - } - } - - .sub-menu-parent { - padding-left: 27px; - font-weight: 500; - - .nav-item { - &:hover { - background: transparent; - } - } - - .sub-menu { - a { - padding-left: $padding * 3; - } - } - } - } - } - .loading { padding: $padding; text-align: center; diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index f9f8494318..5386d9fb52 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -1,12 +1,11 @@ -import type { TabLayoutRoute } from "./tab-layout"; import "./sidebar.scss"; import React from "react"; -import { computed, observable, reaction } from "mobx"; +import type { TabLayoutRoute } from "./tab-layout"; import { observer } from "mobx-react"; import { NavLink } from "react-router-dom"; import { Trans } from "@lingui/macro"; -import { createStorage, cssNames } from "../../utils"; +import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route"; import { namespacesRoute, namespacesURL } from "../+namespaces/namespaces.route"; @@ -30,12 +29,8 @@ import { isActiveRoute } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac"; import { Spinner } from "../spinner"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries"; - -const SidebarContext = React.createContext({ pinned: false }); - -type SidebarContextValue = { - pinned: boolean; -}; +import { SidebarNavItem } from "./sidebar-nav-item"; +import { SidebarContext } from "./sidebar-context"; interface Props { className?: string; @@ -69,6 +64,7 @@ export class Sidebar extends React.Component { return ( { return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => { const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target); const tabRoutes = this.getTabLayoutRoutes(menuItem); + const id = `registered-item-${index}`; let pageUrl: string; let isActive = false; @@ -122,7 +119,8 @@ export class Sidebar extends React.Component { return ( } @@ -155,7 +153,7 @@ export class Sidebar extends React.Component {
{ icon={} /> { icon={} /> { icon={} /> { icon={} /> { icon={} /> { text={Storage} /> { text={Namespaces} /> { text={Events} /> { text={Apps} /> { text={Access Control} /> { ); } } - -interface SidebarNavItemProps { - url: string; - text: React.ReactNode | string; - className?: string; - icon?: React.ReactNode; - isHidden?: boolean; - isActive?: boolean; - subMenus?: TabLayoutRoute[]; - testId?: string; // data-test-id="" property for integration tests -} - -const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []); -const navItemState = observable.map(navItemStorage.get()); - -reaction(() => [...navItemState], (value) => navItemStorage.set(value)); - -@observer -class SidebarNavItem extends React.Component { - static contextType = SidebarContext; - public context: SidebarContextValue; - - get itemId() { - const url = new URL(this.props.url, `${window.location.protocol}//${window.location.host}`); - - return url.pathname; // pathname without get params - } - - @computed get isExpanded() { - return navItemState.get(this.itemId); - } - - toggleSubMenu = () => { - navItemState.set(this.itemId, !this.isExpanded); - }; - - render() { - const { isHidden, isActive, subMenus = [], icon, text, url, children, className, testId } = this.props; - - if (isHidden) { - return null; - } - const extendedView = (subMenus.length > 0 || children) && this.context.pinned; - - if (extendedView) { - return ( -
-
- {icon} - {text} - -
-
    - {subMenus.map(({ title, url }) => ( - - {title} - - ))} - {React.Children.toArray(children).map((child: React.ReactElement) => { - return React.cloneElement(child, { - className: cssNames(child.props.className, { visible: this.isExpanded }), - }); - })} -
-
- ); - } - - return ( - isActive}> - {icon} - {text} - - ); - } -} From 404ad8024268ed8b71b85103fd4e7acbe74b6a00 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 21 Dec 2020 17:35:28 +0200 Subject: [PATCH 02/19] Kube state metrics 1.9.7 (#1821) * kube-state-metrics v1.9.7 Signed-off-by: Jari Kolehmainen * bump metrics-cluster-feature version Signed-off-by: Jari Kolehmainen --- .../resources/14-kube-state-metrics-deployment.yml.hb | 2 +- extensions/metrics-cluster-feature/src/metrics-feature.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb index cb13c8112d..763649f4f1 100644 --- a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb +++ b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb @@ -39,7 +39,7 @@ spec: serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics - image: quay.io/coreos/kube-state-metrics:v1.9.5 + image: quay.io/coreos/kube-state-metrics:v1.9.7 ports: - name: metrics containerPort: 8080 diff --git a/extensions/metrics-cluster-feature/src/metrics-feature.ts b/extensions/metrics-cluster-feature/src/metrics-feature.ts index e29a9156bd..886cabe6dd 100644 --- a/extensions/metrics-cluster-feature/src/metrics-feature.ts +++ b/extensions/metrics-cluster-feature/src/metrics-feature.ts @@ -26,7 +26,7 @@ export interface MetricsConfiguration { export class MetricsFeature extends ClusterFeature.Feature { name = "metrics"; - latestVersion = "v2.17.2-lens1"; + latestVersion = "v2.17.2-lens2"; templateContext: MetricsConfiguration = { persistence: { From d682c3b91fd2d8f78fc4a562ce6ea80371349333 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 21 Dec 2020 22:10:32 +0200 Subject: [PATCH 03/19] Fix proxy retry counter cleanup on success (#1825) Signed-off-by: Jari Kolehmainen --- src/main/lens-proxy.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 5770429a7e..e4f6ab4a34 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -120,6 +120,14 @@ export class LensProxy { protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); + proxy.on("proxyRes", (proxyRes, req) => { + const retryCounterId = this.getRequestId(req); + + if (this.retryCounters.has(retryCounterId)) { + this.retryCounters.delete(retryCounterId); + } + }); + proxy.on("error", (error, req, res, target) => { if (this.closed) { return; From c7371d4cf1dfa27d4af71b9c25b8061e4b17686c Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 21 Dec 2020 22:15:08 +0200 Subject: [PATCH 04/19] Render node list before metrics are available (#1827) Signed-off-by: Jari Kolehmainen --- src/renderer/components/+nodes/nodes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/nodes.tsx index da0e9e8c4c..32885a4c66 100644 --- a/src/renderer/components/+nodes/nodes.tsx +++ b/src/renderer/components/+nodes/nodes.tsx @@ -134,7 +134,7 @@ export class Nodes extends React.Component { Date: Mon, 21 Dec 2020 22:16:57 +0200 Subject: [PATCH 05/19] Parse jsonPath expressions (#1793) * Fix jsonPath calls by removing \ characters and using $..[] notation Signed-off-by: Lauri Nevala * Parse jsonPath properly Signed-off-by: Lauri Nevala * Cleanup Signed-off-by: Lauri Nevala * More cleanup Signed-off-by: Lauri Nevala * Improve parsing Signed-off-by: Lauri Nevala * Finetuning Signed-off-by: Lauri Nevala * Stringify children only if value is object or array Signed-off-by: Lauri Nevala * Test other escaped characters do not cause issues Signed-off-by: Lauri Nevala --- .../crd-resource-details.tsx | 3 +- .../+custom-resources/crd-resources.tsx | 19 ++++++--- .../utils/__tests__/jsonPath.test.tsx | 41 +++++++++++++++++++ src/renderer/utils/jsonPath.ts | 35 ++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/renderer/utils/__tests__/jsonPath.test.tsx create mode 100644 src/renderer/utils/jsonPath.ts diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index 6610fb4b56..412293eee1 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -13,6 +13,7 @@ import { crdStore } from "./crd.store"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { Input } from "../input"; import { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api"; +import { parseJsonPath } from "../../utils/jsonPath"; interface Props extends KubeObjectDetailsProps { } @@ -46,7 +47,7 @@ export class CrdResourceDetails extends React.Component { renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) { return columns.map(({ name, jsonPath: jp }) => ( - {convertSpecValue(jsonPath.value(crd, jp.slice(1)))} + {convertSpecValue(jsonPath.value(crd, parseJsonPath(jp.slice(1))))} )); } diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index cca7ac7015..72209c80f7 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -12,6 +12,7 @@ import { autorun, computed } from "mobx"; import { crdStore } from "./crd.store"; import { TableSortCallback } from "../table"; import { apiManager } from "../../api/api-manager"; +import { parseJsonPath } from "../../utils/jsonPath"; interface Props extends RouteComponentProps { } @@ -61,7 +62,7 @@ export class CrdResources extends React.Component { }; extraColumns.forEach(column => { - sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, column.jsonPath.slice(1)); + sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, parseJsonPath(column.jsonPath.slice(1))); }); return ( @@ -91,10 +92,18 @@ export class CrdResources extends React.Component { renderTableContents={(crdInstance: KubeObject) => [ crdInstance.getName(), isNamespaced && crdInstance.getNs(), - ...extraColumns.map(column => ({ - renderBoolean: true, - children: JSON.stringify(jsonPath.value(crdInstance, column.jsonPath.slice(1))), - })), + ...extraColumns.map((column) => { + let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1))); + + if (Array.isArray(value) || typeof value === "object") { + value = JSON.stringify(value); + } + + return { + renderBoolean: true, + children: value, + }; + }), crdInstance.getAge(), ]} /> diff --git a/src/renderer/utils/__tests__/jsonPath.test.tsx b/src/renderer/utils/__tests__/jsonPath.test.tsx new file mode 100644 index 0000000000..53ef16dc05 --- /dev/null +++ b/src/renderer/utils/__tests__/jsonPath.test.tsx @@ -0,0 +1,41 @@ +import { parseJsonPath } from "../jsonPath"; + +describe("parseJsonPath", () => { + test("should convert \\. to use indexed notation", () => { + const res = parseJsonPath(".metadata.labels.kubesphere\\.io/alias-name"); + + expect(res).toBe(".metadata.labels['kubesphere.io/alias-name']"); + }); + + test("should convert keys with escpaped charatecrs to use indexed notation", () => { + const res = parseJsonPath(".metadata.labels.kubesphere\\\"io/alias-name"); + + expect(res).toBe(".metadata.labels['kubesphere\"io/alias-name']"); + }); + + test("should convert '-' to use indexed notation", () => { + const res = parseJsonPath(".metadata.labels.alias-name"); + + expect(res).toBe(".metadata.labels['alias-name']"); + }); + + test("should handle scenario when both \\. and indexed notation are present", () => { + const rest = parseJsonPath(".metadata.labels\\.serving['some.other.item']"); + + expect(rest).toBe(".metadata['labels.serving']['some.other.item']"); + }); + + + test("should not touch given jsonPath if no invalid characters present", () => { + const res = parseJsonPath(".status.conditions[?(@.type=='Ready')].status"); + + expect(res).toBe(".status.conditions[?(@.type=='Ready')].status"); + }); + + test("strips '\\' away from the result", () => { + const res = parseJsonPath(".metadata.labels['serving\\.knative\\.dev/configuration']"); + + expect(res).toBe(".metadata.labels['serving.knative.dev/configuration']"); + }); + +}); diff --git a/src/renderer/utils/jsonPath.ts b/src/renderer/utils/jsonPath.ts new file mode 100644 index 0000000000..ea31ffa80e --- /dev/null +++ b/src/renderer/utils/jsonPath.ts @@ -0,0 +1,35 @@ +// Helper to convert strings used for jsonPath where \. or - is present to use indexed notation, +// for example: .metadata.labels.kubesphere\.io/alias-name -> .metadata.labels['kubesphere\.io/alias-name'] + +export function parseJsonPath(jsonPath: string) { + let pathExpression = jsonPath; + + if (jsonPath.match(/[\\-]/g)) { // search for '\' and '-' + const [first, ...rest] = jsonPath.split(/(?<=\w)\./); // split jsonPath by '.' (\. cases are ignored) + + pathExpression = `${convertToIndexNotation(first, true)}${rest.map(value => convertToIndexNotation(value)).join("")}`; + } + + // strip '\' characters from the result + return pathExpression.replace(/\\/g, ""); +} + +function convertToIndexNotation(key: string, firstItem = false) { + if (key.match(/[\\-]/g)) { // check if found '\' and '-' in key + if (key.includes("[")) { // handle cases where key contains [...] + const keyToConvert = key.match(/^.*(?=\[)/g); // get the text from the key before '[' + + if (keyToConvert && keyToConvert[0].match(/[\\-]/g)) { // check if that part contains illegal characters + return key.replace(keyToConvert[0], `['${keyToConvert[0]}']`); // surround key with '[' and ']' + } else { + return `.${key}`; // otherwise return as is with leading '.' + } + } + + return `['${key}']`; + } else { // no illegal chracters found, do not touch + const prefix = firstItem ? "" : "."; + + return `${prefix}${key}`; + } +} \ No newline at end of file From 0c4c98e20617e43a1e1c00a9bf77baf34079c69c Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 22 Dec 2020 07:43:34 +0200 Subject: [PATCH 06/19] Fix namespace store subscribe (#1826) Signed-off-by: Jari Kolehmainen --- src/renderer/components/+namespaces/namespace.store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 79c82bd48d..0f0d79e47d 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -43,10 +43,10 @@ export class NamespaceStore extends KubeObjectStore { } subscribe(apis = [this.api]) { - const { allowedNamespaces } = getHostedCluster(); + const { accessibleNamespaces } = getHostedCluster(); // if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted - if (allowedNamespaces.length > 0) { + if (accessibleNamespaces.length > 0) { return () => { return; }; } From bd1ed27619aaf5bc42fa44dec679acb3b0e5d8bd Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 22 Dec 2020 14:38:06 +0200 Subject: [PATCH 07/19] Fix: missing dependent types for Select and Slider components (#1828) * Types for Select and Slider components are not fully exported, fix #1824 Signed-off-by: Roman * remove unused @types/material-ui package Signed-off-by: Roman --- package.json | 1 - src/extensions/npm/extensions/package.json | 6 ++++-- yarn.lock | 15 --------------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 1631c98a87..30ebd93f78 100644 --- a/package.json +++ b/package.json @@ -290,7 +290,6 @@ "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.155", "@types/marked": "^0.7.4", - "@types/material-ui": "^0.21.7", "@types/md5-file": "^4.0.2", "@types/mini-css-extract-plugin": "^0.9.1", "@types/mock-fs": "^4.10.0", diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json index a4c3f13d6e..5b2b4475ae 100644 --- a/src/extensions/npm/extensions/package.json +++ b/src/extensions/npm/extensions/package.json @@ -16,8 +16,10 @@ "name": "Mirantis, Inc.", "email": "info@k8slens.dev" }, - "devDependencies": { - "@types/node": "^14.14.6", + "dependencies": { + "@types/node": "*", + "@types/react-select": "*", + "@material-ui/core": "*", "conf": "^7.0.1" } } diff --git a/yarn.lock b/yarn.lock index 8df1169b83..cbfe4a3ccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2116,14 +2116,6 @@ resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.7.4.tgz#607685669bb1bbde2300bc58ba43486cbbee1f0a" integrity sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw== -"@types/material-ui@^0.21.7": - version "0.21.7" - resolved "https://registry.yarnpkg.com/@types/material-ui/-/material-ui-0.21.7.tgz#2a4ab77a56a16adef044ba607edde5214151a5d8" - integrity sha512-OxGu+Jfm3d8IVYu5w2cqosSFU+8KJYCeVjw1jLZ7DzgoE7KpSFFpbDJKWhV1FAf/HEQXzL1IpX6PmLwINlE4Xg== - dependencies: - "@types/react" "*" - "@types/react-addons-linked-state-mixin" "*" - "@types/md5-file@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.2.tgz#c7241e88f4aa17218c774befb0fc34f33f21fe36" @@ -2266,13 +2258,6 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/react-addons-linked-state-mixin@*": - version "0.14.21" - resolved "https://registry.yarnpkg.com/@types/react-addons-linked-state-mixin/-/react-addons-linked-state-mixin-0.14.21.tgz#3abf296fe09d036c233ebe55f4562f3e6233af49" - integrity sha512-3UF7Szd3JyuU+z90kqu8L4VdDWp7SUC0eRjV2QmMEliaHODGLi5XyO5ctS50K/lG6fjC0dSAPVbvnqv0nPoGMQ== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" From dfffb27f68679cb9ad36b1679fc21a85eeb30712 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 22 Dec 2020 16:09:52 +0200 Subject: [PATCH 08/19] Optimise Cluster.getAllowedResources() (#1830) * optimise Cluster.getAllowedResources() Signed-off-by: Jari Kolehmainen * make it faster (max 5 concurrent requests) Signed-off-by: Jari Kolehmainen --- package.json | 1 + src/main/cluster.ts | 38 ++++++++++++++++++++++++++++---------- yarn.lock | 12 ++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 30ebd93f78..b20ce96b2e 100644 --- a/package.json +++ b/package.json @@ -239,6 +239,7 @@ "node-pty": "^0.9.0", "npm": "^6.14.8", "openid-client": "^3.15.2", + "p-limit": "^3.1.0", "path-to-regexp": "^6.1.0", "proper-lockfile": "^4.1.1", "request": "^2.88.2", diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 79e28fa9c4..b9ff62e8ac 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -11,10 +11,11 @@ import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; import { loadConfig } from "../common/kube-helpers"; import request, { RequestPromiseOptions } from "request-promise-native"; -import { apiResources } from "../common/rbac"; +import { apiResources, KubeApiResource } from "../common/rbac"; import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; +import plimit from "p-limit"; export enum ClusterStatus { AccessGranted = 2, @@ -78,6 +79,7 @@ export class Cluster implements ClusterModel, ClusterState { protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; + private resourceAccessStatuses: Map = new Map(); whenInitialized = when(() => this.initialized); whenReady = when(() => this.ready); @@ -379,6 +381,7 @@ export class Cluster implements ClusterModel, ClusterState { this.accessible = false; this.ready = false; this.activated = false; + this.resourceAccessStatuses.clear(); this.pushState(); } @@ -484,6 +487,8 @@ export class Cluster implements ClusterModel, ClusterState { this.metadata.version = versionData.value; + this.failureReason = null; + return ClusterStatus.AccessGranted; } catch (error) { logger.error(`Failed to connect cluster "${this.contextName}": ${error}`); @@ -643,17 +648,30 @@ export class Cluster implements ClusterModel, ClusterState { if (!this.allowedNamespaces.length) { return []; } - const resourceAccessStatuses = await Promise.all( - apiResources.map(apiResource => this.canI({ - resource: apiResource.resource, - group: apiResource.group, - verb: "list", - namespace: this.allowedNamespaces[0] - })) - ); + const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined); + const apiLimit = plimit(5); // 5 concurrent api requests + const requests = []; + + for (const apiResource of resources) { + requests.push(apiLimit(async () => { + for (const namespace of this.allowedNamespaces.slice(0, 10)) { + if (!this.resourceAccessStatuses.get(apiResource)) { + const result = await this.canI({ + resource: apiResource.resource, + group: apiResource.group, + verb: "list", + namespace + }); + + this.resourceAccessStatuses.set(apiResource, result); + } + } + })); + } + await Promise.all(requests); return apiResources - .filter((resource, i) => resourceAccessStatuses[i]) + .filter((resource) => this.resourceAccessStatuses.get(resource)) .map(apiResource => apiResource.resource); } catch (error) { return []; diff --git a/yarn.lock b/yarn.lock index cbfe4a3ccd..aba182b040 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11194,6 +11194,13 @@ p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: dependencies: p-try "^2.0.0" +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -15557,6 +15564,11 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zip-stream@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" From 3f518ef452c01ce015c2d56b3d958f4a332e90c0 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 08:30:48 +0200 Subject: [PATCH 09/19] Fix huawei distro detect (#1819) Signed-off-by: Jari Kolehmainen --- src/main/cluster-detectors/distribution-detector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 041a8b9158..62c4dcacd5 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -40,6 +40,10 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "mirantis", accuracy: 90}; } + if (this.isHuawei()) { + return { value: "huawei", accuracy: 90}; + } + if (this.isMinikube()) { return { value: "minikube", accuracy: 80}; } @@ -127,6 +131,10 @@ export class DistributionDetector extends BaseClusterDetector { return this.version.includes("+k3s"); } + protected isHuawei() { + return this.version.includes("-CCE"); + } + protected async isOpenshift() { try { const response = await this.k8sRequest(""); From 6339a361e7513e1899de09c6affd2bae99f50962 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 08:51:47 +0200 Subject: [PATCH 10/19] Add kubectl v1.20 to version map (#1809) Signed-off-by: Jari Kolehmainen --- src/main/kubectl.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 0a96ee354b..fc35f4f70d 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -22,10 +22,11 @@ const kubectlMap: Map = new Map([ ["1.13", "1.13.12"], ["1.14", "1.14.10"], ["1.15", "1.15.11"], - ["1.16", "1.16.14"], + ["1.16", "1.16.15"], ["1.17", bundledVersion], - ["1.18", "1.18.8"], - ["1.19", "1.19.0"] + ["1.18", "1.18.15"], + ["1.19", "1.19.5"], + ["1.20", "1.20.0"] ]); const packageMirrors: Map = new Map([ ["default", "https://storage.googleapis.com/kubernetes-release/release"], From 30611c1892db92cee43e06644a60d199c04293d5 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 23 Dec 2020 11:51:43 +0300 Subject: [PATCH 11/19] Fixing logs scrolling state (#1847) * Passing raw logs to PodLogs child components Signed-off-by: Alex Andreev * Avoid autoscrolling while user is scrolling Signed-off-by: Alex Andreev * Removing status panel from log controls Signed-off-by: Alex Andreev --- src/renderer/components/dock/info-panel.tsx | 12 +++-- .../components/dock/pod-log-controls.tsx | 13 +++-- src/renderer/components/dock/pod-log-list.tsx | 53 +++++++++++++------ .../components/dock/pod-log-search.tsx | 5 +- .../components/dock/pod-logs.store.ts | 44 ++++++++++----- src/renderer/components/dock/pod-logs.tsx | 27 +++------- 6 files changed, 95 insertions(+), 59 deletions(-) diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 4f36d47fc3..34e456fdd6 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -27,6 +27,7 @@ interface OptionalProps { showSubmitClose?: boolean; showInlineInfo?: boolean; showNotifications?: boolean; + showStatusPanel?: boolean; } @observer @@ -38,6 +39,7 @@ export class InfoPanel extends Component { showSubmitClose: true, showInlineInfo: true, showNotifications: true, + showStatusPanel: true, }; @observable error = ""; @@ -93,7 +95,7 @@ export class InfoPanel extends Component { } render() { - const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props; + const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose, showStatusPanel } = this.props; const { submit, close, submitAndClose, waiting } = this; const isDisabled = !!(disableSubmit || waiting || error); @@ -102,9 +104,11 @@ export class InfoPanel extends Component {
{controls}
-
- {waiting ? <> {submittingMessage} : this.renderErrorIcon()} -
+ {showStatusPanel && ( +
+ {waiting ? <> {submittingMessage} : this.renderErrorIcon()} +
+ )} {showButtons && ( <>
); diff --git a/src/renderer/components/dock/pod-log-list.tsx b/src/renderer/components/dock/pod-log-list.tsx index a3ab172a16..3d17c61f13 100644 --- a/src/renderer/components/dock/pod-log-list.tsx +++ b/src/renderer/components/dock/pod-log-list.tsx @@ -5,7 +5,7 @@ import AnsiUp from "ansi_up"; import DOMPurify from "dompurify"; import debounce from "lodash/debounce"; import { Trans } from "@lingui/macro"; -import { action, observable } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Align, ListOnScrollProps } from "react-window"; @@ -15,7 +15,7 @@ import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; import { VirtualList } from "../virtual-list"; -import { logRange } from "./pod-logs.store"; +import { podLogsStore } from "./pod-logs.store"; interface Props { logs: string[] @@ -47,23 +47,25 @@ export class PodLogList extends React.Component { return; } + if (logs == prevProps.logs || !this.virtualListDiv.current) return; + const newLogsLoaded = prevProps.logs.length < logs.length; const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0; - const fewLogsLoaded = logs.length < logRange; - if (this.isLastLineVisible) { + if (this.isLastLineVisible || prevProps.logs.length == 0) { this.scrollToBottom(); // Scroll down to keep user watching/reading experience return; } if (scrolledToBeginning && newLogsLoaded) { - this.virtualListDiv.current.scrollTop = (logs.length - prevProps.logs.length) * this.lineHeight; - } + const firstLineContents = prevProps.logs[0]; + const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents); - if (fewLogsLoaded) { - this.isJumpButtonVisible = false; + if (lineToScroll !== -1) { + this.scrollToItem(lineToScroll, "start"); + } } if (!logs.length) { @@ -71,6 +73,20 @@ export class PodLogList extends React.Component { } } + /** + * Returns logs with or without timestamps regarding to showTimestamps prop + */ + @computed + get logs() { + const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps; + + if (!showTimestamps) { + return podLogsStore.logsWithoutTimestamps; + } + + return this.props.logs; + } + /** * Checks if JumpToBottom button should be visible and sets its observable * @param props Scrolling props from virtual list core @@ -115,7 +131,6 @@ export class PodLogList extends React.Component { @action scrollToBottom = () => { if (!this.virtualListDiv.current) return; - this.isJumpButtonVisible = false; this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight; }; @@ -123,7 +138,13 @@ export class PodLogList extends React.Component { this.virtualListRef.current.scrollToItem(index, align); }; - onScroll = debounce((props: ListOnScrollProps) => { + onScroll = (props: ListOnScrollProps) => { + if (!this.virtualListDiv.current) return; + this.isLastLineVisible = false; + this.onScrollDebounced(props); + }; + + onScrollDebounced = debounce((props: ListOnScrollProps) => { if (!this.virtualListDiv.current) return; this.setButtonVisibility(props); this.setLastLineVisibility(props); @@ -137,7 +158,7 @@ export class PodLogList extends React.Component { */ getLogRow = (rowIndex: number) => { const { searchQuery, isActiveOverlay } = searchStore; - const item = this.props.logs[rowIndex]; + const item = this.logs[rowIndex]; const contents: React.ReactElement[] = []; const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi)); @@ -179,15 +200,15 @@ export class PodLogList extends React.Component { }; render() { - const { logs, isLoading } = this.props; - const isInitLoading = isLoading && !logs.length; - const rowHeights = new Array(logs.length).fill(this.lineHeight); + const { isLoading } = this.props; + const isInitLoading = isLoading && !this.logs.length; + const rowHeights = new Array(this.logs.length).fill(this.lineHeight); if (isInitLoading) { return ; } - if (!logs.length) { + if (!this.logs.length) { return (
There are no logs available for container @@ -198,7 +219,7 @@ export class PodLogList extends React.Component { return (
void toPrevOverlay: () => void toNextOverlay: () => void +} + +interface Props extends PodLogSearchProps { logs: string[] } -export const PodLogSearch = observer((props: PodLogSearchProps) => { +export const PodLogSearch = observer((props: Props) => { const { logs, onSearch, toPrevOverlay, toNextOverlay } = props; const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore; const jumpDisabled = !searchQuery || !occurrences.length; diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/pod-logs.store.ts index 057b8eea15..2f3574d115 100644 --- a/src/renderer/components/dock/pod-logs.store.ts +++ b/src/renderer/components/dock/pod-logs.store.ts @@ -27,11 +27,11 @@ export class PodLogsStore extends DockTabStore { private refresher = interval(10, () => { const id = dockStore.selectedTabId; - if (!this.logs.get(id)) return; + if (!this.podLogs.get(id)) return; this.loadMore(id); }); - @observable logs = observable.map(); + @observable podLogs = observable.map(); @observable newLogSince = observable.map(); // Timestamp after which all logs are considered to be new constructor() { @@ -48,7 +48,7 @@ export class PodLogsStore extends DockTabStore { } }, { delay: 500 }); - reaction(() => this.logs.get(dockStore.selectedTabId), () => { + reaction(() => this.podLogs.get(dockStore.selectedTabId), () => { this.setNewLogSince(dockStore.selectedTabId); }); @@ -72,7 +72,7 @@ export class PodLogsStore extends DockTabStore { }); this.refresher.start(); - this.logs.set(tabId, logs); + this.podLogs.set(tabId, logs); } catch ({error}) { const message = [ _i18n._(t`Failed to load logs: ${error.message}`), @@ -80,7 +80,7 @@ export class PodLogsStore extends DockTabStore { ]; this.refresher.stop(); - this.logs.set(tabId, message); + this.podLogs.set(tabId, message); } }; @@ -91,14 +91,14 @@ export class PodLogsStore extends DockTabStore { * @param tabId */ loadMore = async (tabId: TabId) => { - if (!this.logs.get(tabId).length) return; - const oldLogs = this.logs.get(tabId); + if (!this.podLogs.get(tabId).length) return; + const oldLogs = this.podLogs.get(tabId); const logs = await this.loadLogs(tabId, { sinceTime: this.getLastSinceTime(tabId) }); // Add newly received logs to bottom - this.logs.set(tabId, [...oldLogs, ...logs]); + this.podLogs.set(tabId, [...oldLogs, ...logs]); }; /** @@ -134,7 +134,7 @@ export class PodLogsStore extends DockTabStore { * @param tabId */ setNewLogSince(tabId: TabId) { - if (!this.logs.has(tabId) || !this.logs.get(tabId).length || this.newLogSince.has(tabId)) return; + if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return; const timestamp = this.getLastSinceTime(tabId); this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string @@ -147,18 +147,38 @@ export class PodLogsStore extends DockTabStore { @computed get lines() { const id = dockStore.selectedTabId; - const logs = this.logs.get(id); + const logs = this.podLogs.get(id); return logs ? logs.length : 0; } + + /** + * Returns logs with timestamps for selected tab + */ + get logs() { + const id = dockStore.selectedTabId; + + if (!this.podLogs.has(id)) return []; + + return this.podLogs.get(id); + } + + /** + * Removes timestamps from each log line and returns changed logs + * @returns Logs without timestamps + */ + get logsWithoutTimestamps() { + return this.logs.map(item => this.removeTimestamps(item)); + } + /** * It gets timestamps from all logs then returns last one + 1 second * (this allows to avoid getting the last stamp in the selection) * @param tabId */ getLastSinceTime(tabId: TabId) { - const logs = this.logs.get(tabId); + const logs = this.podLogs.get(tabId); const timestamps = this.getTimestamps(logs[logs.length - 1]); const stamp = new Date(timestamps ? timestamps[0] : null); @@ -176,7 +196,7 @@ export class PodLogsStore extends DockTabStore { } clearLogs(tabId: TabId) { - this.logs.delete(tabId); + this.podLogs.delete(tabId); } clearData(tabId: TabId) { diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx index 5b9c2860c4..696c2bf0ab 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/pod-logs.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { computed, observable, reaction } from "mobx"; +import { observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { searchStore } from "../../../common/search-store"; @@ -79,31 +79,15 @@ export class PodLogs extends React.Component { }, 100); } - /** - * Computed prop which returns logs with or without timestamps added to each line - * @returns {Array} An array log items - */ - @computed - get logs(): string[] { - if (!podLogsStore.logs.has(this.tabId)) return []; - const logs = podLogsStore.logs.get(this.tabId); - const { getData, removeTimestamps } = podLogsStore; - const { showTimestamps } = getData(this.tabId); - - if (!showTimestamps) { - return logs.map(item => removeTimestamps(item)); - } - - return logs; - } - render() { + const logs = podLogsStore.logs; + const controls = ( { controls={controls} showSubmitClose={false} showButtons={false} + showStatusPanel={false} /> From ecf930f5f56a7f8e98c60c0d99b28d843ad7dba1 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 11:56:11 +0200 Subject: [PATCH 12/19] Fix extension loader race conditions (#1815) * fix extension loader race conditions Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * fix tests Signed-off-by: Jari Kolehmainen * fix remove Signed-off-by: Jari Kolehmainen * ensure symlinked (dev) extensions are installed on boot Signed-off-by: Jari Kolehmainen --- .../__tests__/extension-discovery.test.ts | 6 +- src/extensions/extension-discovery.ts | 67 ++++++++++--------- src/extensions/extension-installer.ts | 64 +++++++++++------- src/extensions/extension-loader.ts | 12 +++- src/main/index.ts | 6 +- 5 files changed, 95 insertions(+), 60 deletions(-) diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index 0317319329..d0066c3a7e 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -10,7 +10,7 @@ jest.mock("chokidar", () => ({ jest.mock("../extension-installer", () => ({ extensionInstaller: { extensionPackagesRoot: "", - installPackages: jest.fn() + installPackage: jest.fn() } })); @@ -41,7 +41,7 @@ describe("ExtensionDiscovery", () => { // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; - await extensionDiscovery.initMain(); + await extensionDiscovery.watchExtensions(); extensionDiscovery.events.on("add", (extension: InstalledExtension) => { expect(extension).toEqual({ @@ -81,7 +81,7 @@ describe("ExtensionDiscovery", () => { // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; - await extensionDiscovery.initMain(); + await extensionDiscovery.watchExtensions(); const onAdd = jest.fn(); diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 7d0da112bb..0994f89995 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -55,6 +55,7 @@ export class ExtensionDiscovery { protected bundledFolderPath: string; private loadStarted = false; + private extensions: Map = new Map(); // True if extensions have been loaded from the disk after app startup @observable isLoaded = false; @@ -69,13 +70,6 @@ export class ExtensionDiscovery { this.events = new EventEmitter(); } - // Each extension is added as a single dependency to this object, which is written as package.json. - // Each dependency key is the name of the dependency, and - // each dependency value is the non-symlinked path to the dependency (folder). - protected packagesJson: PackageJson = { - dependencies: {} - }; - get localFolderPath(): string { return path.join(os.homedir(), ".k8slens", "extensions"); } @@ -119,7 +113,6 @@ export class ExtensionDiscovery { } async initMain() { - this.watchExtensions(); handleRequest(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); reaction(() => this.toJSON(), () => { @@ -141,6 +134,7 @@ export class ExtensionDiscovery { watch(this.localFolderPath, { // For adding and removing symlinks to work, the depth has to be 1. depth: 1, + ignoreInitial: true, // Try to wait until the file has been completely copied. // The OS might emit an event for added file even it's not completely written to the filesysten. awaitWriteFinish: { @@ -176,8 +170,9 @@ export class ExtensionDiscovery { await this.removeSymlinkByManifestPath(manifestPath); // Install dependencies for the new extension - await this.installPackages(); + await this.installPackage(extension.absolutePath); + this.extensions.set(extension.id, extension); logger.info(`${logModule} Added extension ${extension.manifest.name}`); this.events.emit("add", extension); } @@ -197,23 +192,19 @@ export class ExtensionDiscovery { const extensionFolderName = path.basename(filePath); if (path.relative(this.localFolderPath, filePath) === extensionFolderName) { - const extensionName: string | undefined = Object - .entries(this.packagesJson.dependencies) - .find(([, extensionFolder]) => filePath === extensionFolder)?.[0]; + const extension = Array.from(this.extensions.values()).find((extension) => extension.absolutePath === filePath); + + if (extension) { + const extensionName = extension.manifest.name; - if (extensionName !== undefined) { // If the extension is deleted manually while the application is running, also remove the symlink await this.removeSymlinkByPackageName(extensionName); - delete this.packagesJson.dependencies[extensionName]; - - // Reinstall dependencies to remove the extension from package.json - await this.installPackages(); - // The path to the manifest file is the lens extension id // Note that we need to use the symlinked path - const lensExtensionId = path.join(this.nodeModulesPath, extensionName, manifestFilename); + const lensExtensionId = extension.manifestPath; + this.extensions.delete(extension.id); logger.info(`${logModule} removed extension ${extensionName}`); this.events.emit("remove", lensExtensionId as LensExtensionId); } else { @@ -296,7 +287,7 @@ export class ExtensionDiscovery { await fs.ensureDir(this.nodeModulesPath); await fs.ensureDir(this.localFolderPath); - const extensions = await this.loadExtensions(); + const extensions = await this.ensureExtensions(); this.isLoaded = true; @@ -335,7 +326,6 @@ export class ExtensionDiscovery { manifestJson = __non_webpack_require__(manifestPath); const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); - this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); return { @@ -347,29 +337,46 @@ export class ExtensionDiscovery { isEnabled }; } catch (error) { - logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson }); + logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`, { manifestJson }); return null; } } - async loadExtensions(): Promise> { + async ensureExtensions(): Promise> { const bundledExtensions = await this.loadBundledExtensions(); - await this.installPackages(); // install in-tree as a separate step - const localExtensions = await this.loadFromFolder(this.localFolderPath); + await this.installBundledPackages(this.packageJsonPath, bundledExtensions); - await this.installPackages(); - const extensions = bundledExtensions.concat(localExtensions); + const userExtensions = await this.loadFromFolder(this.localFolderPath); - return new Map(extensions.map(extension => [extension.id, extension])); + for (const extension of userExtensions) { + if (await fs.pathExists(extension.manifestPath) === false) { + await this.installPackage(extension.absolutePath); + } + } + const extensions = bundledExtensions.concat(userExtensions); + + return this.extensions = new Map(extensions.map(extension => [extension.id, extension])); } /** * Write package.json to file system and install dependencies. */ - installPackages() { - return extensionInstaller.installPackages(this.packageJsonPath, this.packagesJson); + async installBundledPackages(packageJsonPath: string, extensions: InstalledExtension[]) { + const packagesJson: PackageJson = { + dependencies: {} + }; + + extensions.forEach((extension) => { + packagesJson.dependencies[extension.manifest.name] = extension.absolutePath; + }); + + return await extensionInstaller.installPackages(packageJsonPath, packagesJson); + } + + async installPackage(name: string) { + return extensionInstaller.installPackage(name); } async loadBundledExtensions() { diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts index 75b30d0b9a..04b78bbe1a 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer.ts @@ -30,12 +30,49 @@ export class ExtensionInstaller { return __non_webpack_require__.resolve("npm/bin/npm-cli"); } - installDependencies(): Promise { - return new Promise((resolve, reject) => { + /** + * Write package.json to the file system and execute npm install for it. + */ + async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { + // Mutual exclusion to install packages in sequence + await this.installLock.acquireAsync(); + + try { + // Write the package.json which will be installed in .installDependencies() + await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), { + mode: 0o600 + }); + logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`); - const child = child_process.fork(this.npmPath, ["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { + await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]); + logger.info(`${logModule} dependencies installed at ${extensionPackagesRoot()}`); + } finally { + this.installLock.release(); + } + } + + /** + * Install single package using npm + */ + async installPackage(name: string): Promise { + // Mutual exclusion to install packages in sequence + await this.installLock.acquireAsync(); + + try { + logger.info(`${logModule} installing package from ${name} to ${extensionPackagesRoot()}`); + await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]); + logger.info(`${logModule} package ${name} installed to ${extensionPackagesRoot()}`); + } finally { + this.installLock.release(); + } + } + + private npm(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = child_process.fork(this.npmPath, args, { cwd: extensionPackagesRoot(), - silent: true + silent: true, + env: {} }); let stderr = ""; @@ -56,25 +93,6 @@ export class ExtensionInstaller { }); }); } - - /** - * Write package.json to the file system and execute npm install for it. - */ - async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { - // Mutual exclusion to install packages in sequence - await this.installLock.acquireAsync(); - - try { - // Write the package.json which will be installed in .installDependencies() - await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), { - mode: 0o600 - }); - - await this.installDependencies(); - } finally { - this.installLock.release(); - } - } } export const extensionInstaller = new ExtensionInstaller(); diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 966289f157..98697d252c 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -12,6 +12,7 @@ import type { LensExtension, LensExtensionConstructor, LensExtensionId } from ". import type { LensMainExtension } from "./lens-main-extension"; import type { LensRendererExtension } from "./lens-renderer-extension"; import * as registries from "./registries"; +import fs from "fs"; // lazy load so that we get correct userData export function extensionPackagesRoot() { @@ -71,7 +72,7 @@ export class ExtensionLoader { } await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]); - + // save state on change `extension.isEnabled` reaction(() => this.storeState, extensionsState => { extensionsStore.mergeState(extensionsState); @@ -115,7 +116,6 @@ export class ExtensionLoader { protected async initMain() { this.isLoaded = true; this.loadOnMain(); - this.broadcastExtensions(); reaction(() => this.toJSON(), () => { this.broadcastExtensions(); @@ -136,7 +136,7 @@ export class ExtensionLoader { this.syncExtensions(extensions); const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); - + // Remove deleted extensions in renderer side only this.extensions.forEach((_, lensExtensionId) => { if (!receivedExtensionIds.includes(lensExtensionId)) { @@ -276,6 +276,12 @@ export class ExtensionLoader { } if (extEntrypoint !== "") { + if (!fs.existsSync(extEntrypoint)) { + console.log(`${logModule}: entrypoint ${extEntrypoint} not found, skipping ...`); + + return; + } + return __non_webpack_require__(extEntrypoint).default; } } catch (err) { diff --git a/src/main/index.ts b/src/main/index.ts index 8da9be1a01..b0ba60d029 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -103,7 +103,6 @@ app.on("ready", async () => { } extensionLoader.init(); - extensionDiscovery.init(); windowManager = WindowManager.getInstance(proxyPort); @@ -111,6 +110,9 @@ app.on("ready", async () => { try { const extensions = await extensionDiscovery.load(); + // Start watching after bundled extensions are loaded + extensionDiscovery.watchExtensions(); + // Subscribe to extensions that are copied or deleted to/from the extensions folder extensionDiscovery.events.on("add", (extension: InstalledExtension) => { extensionLoader.addExtension(extension); @@ -122,6 +124,8 @@ app.on("ready", async () => { extensionLoader.initExtensions(extensions); } catch (error) { dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); + console.error(error); + console.trace(); } setTimeout(() => { From 52c642b3a4154feaf40bc6386a4ebd82a85e83c6 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 11:58:15 +0200 Subject: [PATCH 13/19] Disable oh-my-zsh auto-update prompt when resolving shell environment (#1848) Signed-off-by: Jari Kolehmainen --- src/main/shell-session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 19170695fc..be04649a31 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -120,6 +120,7 @@ export class ShellSession extends EventEmitter { if(path.basename(env["PTYSHELL"]) === "zsh") { env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME; env["ZDOTDIR"] = this.kubectlBinDir; + env["DISABLE_AUTO_UPDATE"] = "true"; } env["PTYPID"] = process.pid.toString(); From beaf2c69ff57fead4963e6c4e6b0e8f1f91881c7 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 23 Dec 2020 11:58:32 +0200 Subject: [PATCH 14/19] Fix Kubectl 1.18 version in version map (#1846) Signed-off-by: Lauri Nevala --- src/main/kubectl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index fc35f4f70d..ebfd2a6a98 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -24,7 +24,7 @@ const kubectlMap: Map = new Map([ ["1.15", "1.15.11"], ["1.16", "1.16.15"], ["1.17", bundledVersion], - ["1.18", "1.18.15"], + ["1.18", "1.18.14"], ["1.19", "1.19.5"], ["1.20", "1.20.0"] ]); From 8bc42e91b4dfaae3f47edc9491f689bb1b943f26 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 13:12:05 +0200 Subject: [PATCH 15/19] Workloads overview: don't block on store load (#1829) * workloads overview: don't block on store load Signed-off-by: Jari Kolehmainen * subscribe after loadAll Signed-off-by: Jari Kolehmainen --- .../+workloads-overview/overview.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index bed38f99a3..318ad53f77 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -14,7 +14,6 @@ import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; -import { Spinner } from "../spinner"; import { Events } from "../+events"; import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; @@ -24,7 +23,6 @@ interface Props extends RouteComponentProps { @observer export class WorkloadsOverview extends React.Component { - @observable isReady = false; @observable isUnmounting = false; async componentDidMount() { @@ -61,10 +59,13 @@ export class WorkloadsOverview extends React.Component { if (isAllowedResource("events")) { stores.push(eventStore); } - this.isReady = stores.every(store => store.isLoaded); - await Promise.all(stores.map(store => store.loadAll())); - this.isReady = true; - const unsubscribeList = stores.map(store => store.subscribe()); + + const unsubscribeList: Array<() => void> = []; + + for (const store of stores) { + await store.loadAll(); + unsubscribeList.push(store.subscribe()); + } await when(() => this.isUnmounting); unsubscribeList.forEach(dispose => dispose()); @@ -74,11 +75,7 @@ export class WorkloadsOverview extends React.Component { this.isUnmounting = true; } - renderContents() { - if (!this.isReady) { - return ; - } - + get contents() { return ( <> @@ -94,7 +91,7 @@ export class WorkloadsOverview extends React.Component { render() { return (
- {this.renderContents()} + {this.contents}
); } From dc5d29eb900f71a4530ab6adcb59ac38437cf97c Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 13:12:25 +0200 Subject: [PATCH 16/19] Fix vmware distro detect (#1817) * fix vmware distro detect Signed-off-by: Jari Kolehmainen * Change method orders to avoid conflicts Signed-off-by: Lauri Nevala Co-authored-by: Lauri Nevala --- src/main/cluster-detectors/distribution-detector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 62c4dcacd5..86cf24e6f4 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -36,6 +36,10 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "digitalocean", accuracy: 90}; } + if (this.isVMWare()) { + return { value: "vmware", accuracy: 90}; + } + if (this.isMirantis()) { return { value: "mirantis", accuracy: 90}; } @@ -123,6 +127,10 @@ export class DistributionDetector extends BaseClusterDetector { return this.version.includes("+"); } + protected isVMWare() { + return this.version.includes("+vmware"); + } + protected isRke() { return this.version.includes("-rancher"); } From 8ab86b600fa0d17a06335cbf1371e60bd54a03ce Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 13:12:44 +0200 Subject: [PATCH 17/19] Fix tencent distribution detect (#1816) * fix tencent distribution detect Signed-off-by: Jari Kolehmainen * Change method orders to avoid conflicts Signed-off-by: Lauri Nevala Co-authored-by: Lauri Nevala --- src/main/cluster-detectors/distribution-detector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 86cf24e6f4..a23a1a27d3 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -48,6 +48,10 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "huawei", accuracy: 90}; } + if (this.isTke()) { + return { value: "tencent", accuracy: 90}; + } + if (this.isMinikube()) { return { value: "minikube", accuracy: 80}; } @@ -123,6 +127,10 @@ export class DistributionDetector extends BaseClusterDetector { return this.cluster.contextName === "docker-desktop"; } + protected isTke() { + return this.version.includes("-tke."); + } + protected isCustom() { return this.version.includes("+"); } From 4d6c6eded118d184c31fe742cfac79e3acf06edb Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 13:13:42 +0200 Subject: [PATCH 18/19] Fix alibaba distro detect (#1818) Signed-off-by: Jari Kolehmainen Co-authored-by: Lauri Nevala --- src/main/cluster-detectors/distribution-detector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index a23a1a27d3..eb0680dd00 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -44,6 +44,10 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "mirantis", accuracy: 90}; } + if (this.isAlibaba()) { + return { value: "alibaba", accuracy: 90}; + } + if (this.isHuawei()) { return { value: "huawei", accuracy: 90}; } @@ -147,6 +151,10 @@ export class DistributionDetector extends BaseClusterDetector { return this.version.includes("+k3s"); } + protected isAlibaba() { + return this.version.includes("-aliyun"); + } + protected isHuawei() { return this.version.includes("-CCE"); } From f6781b9ba980acf3a72319fae2bef51400be3a6b Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 23 Dec 2020 13:52:45 +0200 Subject: [PATCH 19/19] v4.0.5 Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b20ce96b2e..a7af4123d1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.0.4", + "version": "4.0.5", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 9b97cea5e4..f77d068934 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,9 +2,19 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.0.4 (current version) +## 4.0.5 (current version) -We are aware some users are encountering issues and regressions from previous version. Many of these issues are something we have not seen as part of our automated or manual testing process. To make it worse, some of them are really difficult to reproduce. We want to ensure we are putting all our energy and effort trying to resolve these issues. We hope you are patient. Expect to see new patch releases still in the coming days! Fixes in this version: +- Fix: add missing Kubernetes distro detectors +- Fix: improve how Workloads Overview is loaded +- Fix: race conditions on extension loader +- Fix: pod logs scrolling issues +- Fix: render node list before metrics are available +- Fix: kube-state-metrics v1.9.7 +- Fix: CRD sidebar expand/collapse +- Fix: disable oh-my-zsh auto-update prompt when resolving shell environment +- Add kubectl 1.20 support to Lens Smart Terminal +- Optimise performance during cluster connect +## 4.0.4 - Fix errors on Kubernetes v1.20 - Update bundled kubectl to v1.17.15