From 06041e6169e3928b536063f13aa8118c5587f344 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 18 Dec 2020 12:41:17 +0200 Subject: [PATCH 1/3] Test different kube versions (#1806) * test different kube versions Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen --- .azure-pipelines-k8s-matrix.yml | 57 +++++++++++++++++++++++++++++++++ Makefile | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .azure-pipelines-k8s-matrix.yml diff --git a/.azure-pipelines-k8s-matrix.yml b/.azure-pipelines-k8s-matrix.yml new file mode 100644 index 0000000000..77129460a3 --- /dev/null +++ b/.azure-pipelines-k8s-matrix.yml @@ -0,0 +1,57 @@ +variables: + YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn + node_version: 12.x +pr: + branches: + include: + - master + - releases/* + paths: + exclude: + - .github/* + - docs/* + - mkdocs/* +trigger: none +jobs: + - job: Linux + pool: + vmImage: ubuntu-16.04 + strategy: + matrix: + kube_1.16: + kubernetes_version: v1.16.15 + kube_1.17: + kubernetes_version: v1.17.15 + kube_1.18: + kubernetes_version: v1.18.13 + kube_1.19: + kubernetes_version: v1.19.5 + kube_1.20: + kubernetes_version: v1.20.0 + steps: + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: Install Node.js + - task: Cache@2 + inputs: + key: 'yarn | "$(Agent.OS)" | yarn.lock' + restoreKeys: | + yarn | "$(Agent.OS)" + path: $(YARN_CACHE_FOLDER) + displayName: Cache Yarn packages + - bash: | + sudo apt-get update + sudo apt-get install libgconf-2-4 conntrack -y + curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 + sudo install minikube-linux-amd64 /usr/local/bin/minikube + sudo minikube start --driver=none --kubernetes-version $(kubernetes_version) + # Although the kube and minikube config files are in placed $HOME they are owned by root + sudo chown -R $USER $HOME/.kube $HOME/.minikube + displayName: Install integration test dependencies + - script: make node_modules + displayName: Install dependencies + - script: make -j2 build + displayName: Run build + - script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' yarn integration + displayName: Run integration tests for Kubernetes $(kubernetes_version) diff --git a/Makefile b/Makefile index 1f8d4f5392..40eff8445f 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ binaries/client: yarn download-bins node_modules: - yarn install --frozen-lockfile --verbose + yarn install --frozen-lockfile yarn check --verify-tree --integrity static/build/LensDev.html: From 6fe5bfae5c4d7f733b6fe73be29e673244c09297 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 18 Dec 2020 15:03:04 +0200 Subject: [PATCH 2/3] Generate metadata.selfLink if response does not have it (#1804) * generate metadata.selfLink if response does not have it Signed-off-by: Jari Kolehmainen * fix watches Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * fix Signed-off-by: Jari Kolehmainen --- src/renderer/api/api-manager.ts | 4 ++++ src/renderer/api/kube-api.ts | 34 ++++++++++++++++++++++++------ src/renderer/api/kube-watch-api.ts | 8 ++++--- src/renderer/kube-object.store.ts | 5 ++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 68d4773540..01e5ceb228 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -17,6 +17,10 @@ export class ApiManager { return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true)); } + getApiByKind(kind: string, apiVersion: string) { + return Array.from(this.apis.values()).find((api) => api.kind === kind && api.apiVersion === apiVersion); + } + registerApi(apiBase: string, api: KubeApi) { if (!this.apis.has(apiBase)) { this.apis.set(apiBase, api); diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index e7934675c6..8a3a2517c2 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -79,6 +79,18 @@ export function forCluster(cluster: IKubeApiCluster, kubeC }); } +export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) { + if (!object.metadata.selfLink) { + object.metadata.selfLink = createKubeApiURL({ + apiPrefix: api.apiPrefix, + apiVersion: api.apiVersionWithGroup, + resource: api.apiResource, + namespace: api.isNamespaced ? object.metadata.namespace : undefined, + name: object.metadata.name, + }); + } +} + export class KubeApi { static parseApi = parseKubeApi; @@ -260,7 +272,11 @@ export class KubeApi { const KubeObjectConstructor = this.objectConstructor; if (KubeObject.isJsonApiData(data)) { - return new KubeObjectConstructor(data); + const object = new KubeObjectConstructor(data); + + ensureObjectSelfLink(this, object); + + return object; } // process items list response @@ -270,11 +286,17 @@ export class KubeApi { this.setResourceVersion(namespace, metadata.resourceVersion); this.setResourceVersion("", metadata.resourceVersion); - return items.map(item => new KubeObjectConstructor({ - kind: this.kind, - apiVersion, - ...item, - })); + return items.map((item) => { + const object = new KubeObjectConstructor({ + kind: this.kind, + apiVersion, + ...item, + }); + + ensureObjectSelfLink(this, object); + + return object; + }); } // custom apis might return array for list response, e.g. users, groups, etc. diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 58665a11a1..78ca25256e 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -5,7 +5,7 @@ import { stringify } from "querystring"; import { autobind, EventEmitter } from "../utils"; import { KubeJsonApiData } from "./kube-json-api"; import type { KubeObjectStore } from "../kube-object.store"; -import { KubeApi } from "./kube-api"; +import { ensureObjectSelfLink, KubeApi } from "./kube-api"; import { apiManager } from "./api-manager"; import { apiPrefix, isDevelopment } from "../../common/vars"; import { getHostedCluster } from "../../common/cluster-store"; @@ -158,12 +158,14 @@ export class KubeWatchApi { addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { const listener = (evt: IKubeWatchEvent) => { - const { selfLink, namespace, resourceVersion } = evt.object.metadata; - const api = apiManager.getApi(selfLink); + const { namespace, resourceVersion } = evt.object.metadata; + const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion); api.setResourceVersion(namespace, resourceVersion); api.setResourceVersion("", resourceVersion); + ensureObjectSelfLink(api, evt.object); + if (store == apiManager.getStore(api)) { callback(evt); } diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index e23adf3566..bb2fffd819 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -195,10 +195,9 @@ export abstract class KubeObjectStore extends ItemSt const items = this.items.toJS(); for (const {type, object} of this.eventsBuffer.clear()) { - const { uid, selfLink } = object.metadata; - const index = items.findIndex(item => item.getId() === uid); + const index = items.findIndex(item => item.getId() === object.metadata?.uid); const item = items[index]; - const api = apiManager.getApi(selfLink); + const api = apiManager.getApiByKind(object.kind, object.apiVersion); switch (type) { case "ADDED": From 42817a6d972781e26a8700164684223235b529d4 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 18 Dec 2020 16:42:10 +0300 Subject: [PATCH 3/3] 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} - - ); - } -}