diff --git a/Makefile b/Makefile index 54dcd6fc94..8123e75e07 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ endif .PHONY: init dev build test clean -init: download-bins install-deps compile-dev +init: install-deps download-bins compile-dev echo "Init done" download-bins: @@ -46,7 +46,7 @@ integration-win: test-app: yarn test -build: install-deps +build: install-deps download-bins yarn install ifeq "$(DETECTED_OS)" "Windows" yarn dist:win diff --git a/README.md b/README.md index fa8c476799..149706c99f 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Lens is the only IDE you’ll ever need to take control of your Kubernetes clust ## What makes Lens special? -* Amazing usability and end user experience -* Multi cluster management; Support for hundreds of clusters -* Standalone application; No need to install anything in-cluster +* Amazing usability and end-user experience +* Multi cluster management: support for hundreds of clusters +* Standalone application: no need to install anything in-cluster * Real-time cluster state visualization * Resource utilization charts and trends with history powered by built-in Prometheus * Terminal access to nodes and containers @@ -38,7 +38,7 @@ brew cask install lens ## Development (advanced) -Allows faster separately re-run some of involved processes: +Allows for faster separate re-runs of some of the more involved processes: 1. `yarn dev:main` compiles electron's main process part and start watching files 1. `yarn dev:renderer` compiles electron's renderer part and start watching files @@ -54,4 +54,4 @@ Allows faster separately re-run some of involved processes: ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens. +Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens. \ No newline at end of file diff --git a/src/main/logger.ts b/src/main/logger.ts index 0d720b65ac..4068aaacd2 100644 --- a/src/main/logger.ts +++ b/src/main/logger.ts @@ -11,7 +11,7 @@ const fileOptions: winston.transports.FileTransportOptions = { handleExceptions: false, level: isDebugging ? "debug" : "info", filename: "lens.log", - dirname: (app || remote.app).getPath("logs"), + dirname: (app ?? remote?.app)?.getPath("logs"), maxsize: 16 * 1024, maxFiles: 16, tailable: true, diff --git a/src/main/menu.ts b/src/main/menu.ts index 121967b6a4..1b0f3434d7 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -42,7 +42,7 @@ export function buildMenu(windowManager: WindowManager) { `${appName}: ${app.getVersion()}`, `Electron: ${process.versions.electron}`, `Chrome: ${process.versions.chrome}`, - `Copyright 2020 Copyright 2020 Mirantis, Inc.`, + `Copyright 2020 Mirantis, Inc.`, ] dialog.showMessageBoxSync(browserWindow, { title: `${isWindows ? " ".repeat(2) : ""}${appName}`, diff --git a/src/renderer/api/endpoints/crd.api.ts b/src/renderer/api/endpoints/crd.api.ts index 829719a3cb..ebacf832ca 100644 --- a/src/renderer/api/endpoints/crd.api.ts +++ b/src/renderer/api/endpoints/crd.api.ts @@ -1,13 +1,28 @@ import { KubeObject } from "../kube-object"; -import { KubeApi } from "../kube-api"; +import { VersionedKubeApi } from "../kube-api-versioned"; import { crdResourcesURL } from "../../components/+custom-resources/crd.route"; +type AdditionalPrinterColumnsCommon = { + name: string; + type: "integer" | "number" | "string" | "boolean" | "date"; + priority: number; + description: string; +} + +type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & { + jsonPath: string; +} + +type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & { + JSONPath: string; +} + export class CustomResourceDefinition extends KubeObject { static kind = "CustomResourceDefinition"; spec: { group: string; - version: string; + version?: string; // deprecated in v1 api names: { plural: string; singular: string; @@ -20,18 +35,14 @@ export class CustomResourceDefinition extends KubeObject { name: string; served: boolean; storage: boolean; + schema?: unknown; // required in v1 but not present in v1beta + additionalPrinterColumns?: AdditionalPrinterColumnsV1[] }[]; conversion: { strategy?: string; webhook?: any; }; - additionalPrinterColumns?: { - name: string; - type: "integer" | "number" | "string" | "boolean" | "date"; - priority: number; - description: string; - JSONPath: string; - }[]; + additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1 } status: { conditions: { @@ -61,8 +72,8 @@ export class CustomResourceDefinition extends KubeObject { } getResourceApiBase() { - const { version, group } = this.spec; - return `/apis/${group}/${version}/${this.getPluralName()}` + const { group } = this.spec; + return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}` } getPluralName() { @@ -87,7 +98,8 @@ export class CustomResourceDefinition extends KubeObject { } getVersion() { - return this.spec.version; + // v1 has removed the spec.version property, if it is present it must match the first version + return this.spec.versions[0]?.name ?? this.spec.version; } isNamespaced() { @@ -107,14 +119,16 @@ export class CustomResourceDefinition extends KubeObject { } getPrinterColumns(ignorePriority = true) { - const columns = this.spec.additionalPrinterColumns || []; + const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns + ?? this.spec.additionalPrinterColumns?.map(({JSONPath, ...rest}) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape + ?? []; return columns .filter(column => column.name != "Age") .filter(column => ignorePriority ? true : !column.priority); } getValidation() { - return JSON.stringify(this.spec.validation, null, 2); + return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2); } getConditions() { @@ -130,16 +144,10 @@ export class CustomResourceDefinition extends KubeObject { } } -export const crdBetaApi = new KubeApi({ - kind: CustomResourceDefinition.kind, - apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", - isNamespaced: false, - objectConstructor: CustomResourceDefinition, -}); - -export const crdApi = new KubeApi({ +export const crdApi = new VersionedKubeApi({ kind: CustomResourceDefinition.kind, apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions", isNamespaced: false, - objectConstructor: CustomResourceDefinition, + objectConstructor: CustomResourceDefinition }); + diff --git a/src/renderer/api/kube-api-versioned.ts b/src/renderer/api/kube-api-versioned.ts new file mode 100644 index 0000000000..f6aa94c089 --- /dev/null +++ b/src/renderer/api/kube-api-versioned.ts @@ -0,0 +1,56 @@ +import { stringify } from "querystring"; +import { KubeObject } from "./kube-object"; +import { createKubeApiURL } from "./kube-api-parse"; +import { KubeApi, IKubeApiQueryParams, IKubeApiOptions } from "./kube-api"; +import { apiManager } from "./api-manager"; + +export class VersionedKubeApi extends KubeApi { + private preferredVersion?: string; + + constructor(opts: IKubeApiOptions) { + super(opts); + + this.getPreferredVersion().then(() => { + if (this.apiBase != opts.apiBase) + apiManager.registerApi(this.apiBase, this); + }); + } + + // override this property to make read-write + apiBase: string + + async getPreferredVersion() { + if (this.preferredVersion) return; + + const apiGroupVersion = await this.request.get<{ preferredVersion?: { version: string; }; }>(`${this.apiPrefix}/${this.apiGroup}`); + + if (!apiGroupVersion?.preferredVersion) return; + + this.preferredVersion = apiGroupVersion.preferredVersion.version; + + // update apiBase + this.apiBase = this.getUrl(); + } + + async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise { + await this.getPreferredVersion(); + return await super.list({namespace}, query); + } + async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise { + await this.getPreferredVersion(); + return super.get({ name, namespace }, query); + } + + getUrl({ name = "", namespace = "" } = {}, query?: Partial) { + const { apiPrefix, apiGroup, apiVersion, apiResource, preferredVersion, isNamespaced } = this; + + const resourcePath = createKubeApiURL({ + apiPrefix: apiPrefix, + apiVersion: `${apiGroup}/${preferredVersion ?? apiVersion}`, + resource: apiResource, + namespace: isNamespaced ? namespace : undefined, + name: name, + }); + return resourcePath + (query ? `?` + stringify(query) : ""); + } +} diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.scss b/src/renderer/components/+apps-helm-charts/helm-chart-details.scss index c6d0a73786..212e9d723a 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.scss +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.scss @@ -25,7 +25,6 @@ .version { .Select { - width: 80px; min-width: 80px; white-space: nowrap; } diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 7f1f0382fc..085ebdd37a 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -1,7 +1,6 @@ import "./cluster-settings.scss"; import React from "react"; -import { Link } from "react-router-dom"; import { observer } from "mobx-react"; import { Features } from "./features"; import { Removal } from "./removal"; @@ -15,6 +14,25 @@ import { navigate } from "../../navigation"; @observer export class ClusterSettings extends React.Component { + async componentDidMount() { + window.addEventListener('keydown', this.onEscapeKey); + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.onEscapeKey); + } + + onEscapeKey = (evt: KeyboardEvent) => { + if (evt.code === "Escape") { + evt.stopPropagation(); + this.close(); + } + } + + close() { + navigate("/"); + } + render() { const cluster = getMatchedCluster(); if (!cluster) return null; @@ -26,7 +44,7 @@ export class ClusterSettings extends React.Component { showTooltip={false} />

{cluster.preferences.clusterName}

- navigate("/")} big/> + ); return ( diff --git a/src/renderer/components/+cluster-settings/status.tsx b/src/renderer/components/+cluster-settings/status.tsx index d722fe8ea8..d79d4e4969 100644 --- a/src/renderer/components/+cluster-settings/status.tsx +++ b/src/renderer/components/+cluster-settings/status.tsx @@ -22,7 +22,7 @@ export class Status extends React.Component { const rows = [ ["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`], ["Distribution", cluster.distribution], - ["Kerbel Version", cluster.version], + ["Kernel Version", cluster.version], ["API Address", cluster.apiUrl], ["Nodes Count", cluster.nodes || "0"] ]; diff --git a/src/renderer/components/+custom-resources/crd-details.tsx b/src/renderer/components/+custom-resources/crd-details.tsx index fdf8dd3968..3270868732 100644 --- a/src/renderer/components/+custom-resources/crd-details.tsx +++ b/src/renderer/components/+custom-resources/crd-details.tsx @@ -102,13 +102,13 @@ export class CRDDetails extends React.Component { { printerColumns.map((column, index) => { - const { name, type, JSONPath } = column; + const { name, type, jsonPath } = column; return ( {name} {type} - + ) diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index 71b517ad5a..201d5e6e4a 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -57,7 +57,7 @@ export class CrdResourceDetails extends React.Component { {extraColumns.map(column => { const { name } = column; - const value = jsonPath.query(object, column.JSONPath.slice(1)); + const value = jsonPath.query(object, (column.jsonPath).slice(1)); return ( diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 81a10cfd6f..036b0bec20 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -57,7 +57,7 @@ export class CrdResources extends React.Component { [sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp, } extraColumns.forEach(column => { - sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.JSONPath.slice(1)) + sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.jsonPath.slice(1)) }) // todo: merge extra columns and other params to predefined view const { List } = apiManager.getViews(crd.getResourceApiBase()); @@ -88,9 +88,9 @@ export class CrdResources extends React.Component { renderTableContents={(crdInstance: KubeObject) => [ crdInstance.getName(), isNamespaced && crdInstance.getNs(), - ...extraColumns.map(column => - jsonPath.query(crdInstance, column.JSONPath.slice(1)) - ), + ...extraColumns.map(column => { + return jsonPath.query(crdInstance, (column.jsonPath).slice(1)) + }), crdInstance.getAge(), ]} renderItemMenu={(item: KubeObject) => { diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index bfcabd1a44..3938023d71 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -23,6 +23,7 @@ export class Preferences extends React.Component { @observable helmLoading = false; @observable helmRepos: HelmRepo[] = []; @observable helmAddedRepos = observable.map(); + @observable httpProxy = userStore.preferences.httpsProxy || ""; @computed get themeOptions(): SelectOption[] { return themeStore.themes.map(theme => ({ @@ -39,9 +40,21 @@ export class Preferences extends React.Component { } async componentDidMount() { + window.addEventListener('keydown', this.onEscapeKey); await this.loadHelmRepos(); } + componentWillUnmount() { + window.removeEventListener('keydown', this.onEscapeKey); + } + + onEscapeKey = (evt: KeyboardEvent) => { + if (evt.code === "Escape") { + evt.stopPropagation(); + history.goBack(); + } + } + @action async loadHelmRepos() { this.helmLoading = true; @@ -121,11 +134,13 @@ export class Preferences extends React.Component { preferences.httpsProxy = v} + value={this.httpProxy} + onChange={v => this.httpProxy = v} + onBlur={() => preferences.httpsProxy = this.httpProxy} /> Proxy is used only for non-cluster communication. + Proxy is used only for non-cluster communication. diff --git a/src/renderer/components/dock/install-chart.scss b/src/renderer/components/dock/install-chart.scss index 38dbea7f8b..4ef4b9df81 100644 --- a/src/renderer/components/dock/install-chart.scss +++ b/src/renderer/components/dock/install-chart.scss @@ -5,7 +5,7 @@ } &.chart-version { - min-width: 80px; + min-width: 130px; } } } diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index a6cb7e0bbb..4f71ed87ac 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -70,11 +70,15 @@ html { &__menu { background: var(--select-menu-bgc); box-shadow: inset 0 0 0 1px var(--select-menu-border-color); + width: max-content; + min-width: 100%; &-list { @include custom-scrollbar; padding-right: 1px; padding-left: 1px; + width: max-content; + min-width: 100%; } &-notice { @@ -83,6 +87,8 @@ html { } &__option { + white-space: nowrap; + &:active { background: $primary; }