diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index d544cd5eec..ca4f7daa9c 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -13,7 +13,6 @@ trigger: - "*" jobs: - job: Windows - condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" pool: vmImage: windows-2019 strategy: @@ -34,10 +33,14 @@ jobs: inputs: key: yarn | $(Agent.OS) | yarn.lock path: $(YARN_CACHE_FOLDER) + cacheHitVar: CACHE_RESTORED displayName: Cache Yarn packages - script: make deps displayName: Install dependencies + - script: make integration-win + displayName: Run integration tests - script: make build + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" displayName: Build env: WIN_CSC_LINK: $(WIN_CSC_LINK) @@ -53,6 +56,7 @@ jobs: steps: - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG" displayName: Set the tag name as an environment variable + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" - task: NodeTool@0 inputs: versionSpec: $(node_version) @@ -138,6 +142,7 @@ jobs: SNAP_LOGIN: $(SNAP_LOGIN) - script: make build displayName: Build + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" env: GH_TOKEN: $(GH_TOKEN) - bash: | diff --git a/README.md b/README.md index 9a594e3ad6..8e62151a1d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Lens | The Kubernetes IDE [![Build Status](https://dev.azure.com/lensapp/lensapp/_apis/build/status/lensapp.lens?branchName=master)](https://dev.azure.com/lensapp/lensapp/_build/latest?definitionId=1&branchName=master) +[![Releases](https://img.shields.io/github/downloads/lensapp/lens/total.svg)](https://github.com/lensapp/lens/releases) +[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) Lens is the only IDE you’ll ever need to take control of your Kubernetes clusters. It is a standalone application for MacOS, Windows and Linux operating systems. It is open source and free. diff --git a/build/icon.ico b/build/icon.ico index baac2aac70..a203847c7c 100644 Binary files a/build/icon.ico and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png index b0f61b8541..762a3ed50f 100644 Binary files a/build/icon.png and b/build/icon.png differ diff --git a/build/icons/512x512.png b/build/icons/512x512.png index b0f61b8541..762a3ed50f 100644 Binary files a/build/icons/512x512.png and b/build/icons/512x512.png differ diff --git a/package.json b/package.json index 728678f3e3..65fc1976c4 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,12 @@ "description": "Lens - The Kubernetes IDE", "version": "3.5.0-beta.1", "main": "dist/main.js", + "copyright": "© 2020, Lakend Labs, Inc.", + "license": "MIT", + "author": { + "name": "Lakend Labs, Inc.", + "email": "info@lakendlabs.com" + }, "scripts": { "dev": "concurrently 'yarn dev:main' 'yarn dev:renderer'", "dev-run": "electron --inspect .", @@ -30,9 +36,6 @@ "download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:helm": "yarn run ts-node build/download_helm.ts" }, - "author": "Lakend Labs, Inc. ", - "copyright": "© 2020, Lakend Labs, Inc.", - "license": "MIT", "config": { "bundledKubectlVersion": "1.17.4", "bundledHelmVersion": "3.1.2" diff --git a/spec/integration/helpers/utils.ts b/spec/integration/helpers/utils.ts index ee138be56e..33aeda65c0 100644 --- a/spec/integration/helpers/utils.ts +++ b/spec/integration/helpers/utils.ts @@ -3,7 +3,7 @@ import { Application } from "spectron"; let appPath = "" switch(process.platform) { case "win32": - appPath = "./dist/win-unpacked/Lens.exe" + appPath = "./dist/win-unpacked/LensDev.exe" break case "linux": appPath = "./dist/linux-unpacked/kontena-lens" diff --git a/spec/integration/specs/app_spec.ts b/spec/integration/specs/app_spec.ts index 414c3288bd..4b52f3b115 100644 --- a/spec/integration/specs/app_spec.ts +++ b/spec/integration/specs/app_spec.ts @@ -5,6 +5,8 @@ import { stat } from "fs" jest.setTimeout(20000) +const BACKSPACE = "\uE003" + describe("app start", () => { let app: Application const clickWhatsNew = async (app: Application) => { @@ -13,6 +15,24 @@ describe("app start", () => { await app.client.waitUntilTextExists("h1", "Welcome") } + const addMinikubeCluster = async (app: Application) => { + await app.client.click("a#add-cluster") + await app.client.waitUntilTextExists("legend", "Choose config:") + await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)") + await app.client.click("button.btn-primary") + } + + const waitForMinikubeDashboard = async (app: Application) => { + await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started") + let windowCount = await app.client.getWindowCount() + // wait for webview to appear on window count + while (windowCount == 1) { + windowCount = await app.client.getWindowCount() + } + await app.client.windowByIndex(windowCount - 1) + await app.client.waitUntilTextExists("span.link-text", "Cluster") + } + beforeEach(async () => { app = util.setup() await app.start() @@ -32,22 +52,48 @@ describe("app start", () => { return } await clickWhatsNew(app) - await app.client.click("a#add-cluster") - await app.client.waitUntilTextExists("legend", "Choose config:") - await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)") - await app.client.click("button.btn-primary") - await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started") - let windowCount = await app.client.getWindowCount() - // wait for webview to appear on window count - while (windowCount == 1) { - windowCount = await app.client.getWindowCount() - } - await app.client.windowByIndex(windowCount - 1) - await app.client.waitUntilTextExists("span.link-text", "Cluster") + await addMinikubeCluster(app) + await waitForMinikubeDashboard(app) await app.client.click('a[href="/nodes"]') await app.client.waitUntilTextExists("div.TableCell", "minikube") }) + it('allows to create a pod', async () => { + const status = spawnSync("minikube status", {shell: true}) + if (status.status !== 0) { + console.warn("minikube not running, skipping test") + return + } + await clickWhatsNew(app) + await addMinikubeCluster(app) + await waitForMinikubeDashboard(app) + await app.client.click(".sidebar-nav #workloads span.link-text") + await app.client.waitUntilTextExists('a[href="/pods"]', "Pods") + await app.client.click('a[href="/pods"]') + await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver-minikube") + await app.client.click('.Icon.new-dock-tab') + await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource") + await app.client.click("li.MenuItem.create-resource-tab") + await app.client.waitForVisible(".CreateResource div.ace_content") + // Write pod manifest to editor + await app.client.keys("apiVersion: v1\n") + await app.client.keys("kind: Pod\n") + await app.client.keys("metadata:\n") + await app.client.keys(" name: nginx\n") + await app.client.keys(BACKSPACE + "spec:\n") + await app.client.keys(" containers:\n") + await app.client.keys("- name: nginx\n") + await app.client.keys(" image: nginx:alpine\n") + // Create deployent + await app.client.waitForEnabled("button.Button=Create & Close") + await app.client.click("button.Button=Create & Close") + // Wait until first bits of pod appears on dashboard + await app.client.waitForExist(".name=nginx") + // Open pod details + await app.client.click(".name=nginx") + await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx") + }) + afterEach(async () => { if (app && app.isRunning()) { return util.tearDown(app) diff --git a/src/main/index.ts b/src/main/index.ts index b6fc87c820..5d4069c6f2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -29,6 +29,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") { const promiseIpc = new PromiseIpc({ timeout: 2000 }) let windowManager: WindowManager = null; let clusterManager: ClusterManager = null; +let proxyServer: proxy.LensProxy = null; const vmURL = formatUrl({ pathname: path.join(__dirname, `${vueAppName}.html`), @@ -64,10 +65,9 @@ async function main() { // create cluster manager clusterManager = new ClusterManager(clusterStore.getAllClusterObjects(), port) - // run proxy try { - proxy.listen(port, clusterManager) + proxyServer = proxy.listen(port, clusterManager) } catch (error) { logger.error(`Could not start proxy (127.0.0:${port}): ${error.message}`) await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${port}): ${error.message || "unknown error"}`) @@ -87,7 +87,7 @@ async function main() { }, showPreferencesHook: async () => { // IPC send needs webContents as we're sending it to renderer - promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => { + promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => { logger.debug("navigate: preferences IPC sent"); }) }, @@ -110,10 +110,10 @@ async function main() { } app.on("ready", main) -app.on('window-all-closed', function () { +app.on('window-all-closed', function() { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q - if (!isMac) { + if (process.platform != 'darwin') { app.quit(); } else { windowManager = null @@ -130,5 +130,6 @@ app.on("activate", () => { app.on("will-quit", async (event) => { event.preventDefault(); // To allow mixpanel sending to be executed if (clusterManager) clusterManager.stop() + if (proxyServer) proxyServer.close() app.exit(0); }) diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts index e4c7d67d25..4db70bf45d 100644 --- a/src/main/prometheus/lens.ts +++ b/src/main/prometheus/lens.ts @@ -18,7 +18,7 @@ export class PrometheusLens implements PrometheusProvider { port: service.spec.ports[0].port } } catch(error) { - logger.warn(`PrometheusLens: failed to list services: ${error.toString()}`) + logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`) } } diff --git a/src/main/proxy.ts b/src/main/proxy.ts index dfdeac05ac..4ca2f54539 100644 --- a/src/main/proxy.ts +++ b/src/main/proxy.ts @@ -18,6 +18,8 @@ export class LensProxy { protected clusterManager: ClusterManager protected retryCounters: Map = new Map() protected router: Router + protected proxyServer: http.Server + protected closed = false constructor(port: number, clusterManager: ClusterManager) { this.port = port @@ -28,6 +30,13 @@ export class LensProxy { public run() { const proxyServer = this.buildProxyServer(); proxyServer.listen(this.port, "127.0.0.1") + this.proxyServer = proxyServer + } + + public close() { + logger.info("Closing proxy server") + this.proxyServer.close() + this.closed = true } protected buildProxyServer() { @@ -68,6 +77,9 @@ export class LensProxy { } }) proxy.on("error", (error, req, res, target) => { + if(this.closed) { + return + } if (target) { logger.debug("Failed proxy to target: " + JSON.stringify(target)) if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index ed5a42241b..dbcddcb125 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -11,7 +11,7 @@ import { helmCli } from "./helm-cli" import { isWindows } from "../common/vars"; export class ShellSession extends EventEmitter { - static shellEnv: any + static shellEnvs: Map = new Map() protected websocket: WebSocket protected shellProcess: pty.IPty @@ -22,6 +22,7 @@ export class ShellSession extends EventEmitter { protected helmBinDir: string; protected preferences: ClusterPreferences; protected running = false; + protected clusterId: string; constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster) { super() @@ -29,6 +30,7 @@ export class ShellSession extends EventEmitter { this.kubeconfigPath = pathToKubeconfig this.kubectl = new Kubectl(cluster.version) this.preferences = cluster.preferences || {} + this.clusterId = cluster.id } public async open() { @@ -77,16 +79,14 @@ export class ShellSession extends EventEmitter { } protected async getCachedShellEnv() { - let env: any - if (!ShellSession.shellEnv) { + let env = ShellSession.shellEnvs.get(this.clusterId) + if (!env) { env = await this.getShellEnv() - ShellSession.shellEnv = env + ShellSession.shellEnvs.set(this.clusterId, env) } else { - env = ShellSession.shellEnv - // refresh env in the background this.getShellEnv().then((shellEnv: any) => { - ShellSession.shellEnv = shellEnv + ShellSession.shellEnvs.set(this.clusterId, shellEnv) }) } diff --git a/src/renderer/_vue/components/PreferencesPage.vue b/src/renderer/_vue/components/PreferencesPage.vue index da377b2e0d..b780d15f8a 100644 --- a/src/renderer/_vue/components/PreferencesPage.vue +++ b/src/renderer/_vue/components/PreferencesPage.vue @@ -122,7 +122,6 @@ id="checkbox-allow-telemetry" switch v-model="preferences.allowTelemetry" - :disabled="licenceData && licenceData.status === 'valid'" @input="onSave" > Allow telemetry & usage tracking diff --git a/src/renderer/_vue/store/index.js b/src/renderer/_vue/store/index.js index cbce51c291..85b6e0eac8 100644 --- a/src/renderer/_vue/store/index.js +++ b/src/renderer/_vue/store/index.js @@ -47,6 +47,11 @@ export default new Vuex.Store({ this.commit("savePreferences", userStore.getPreferences()); }, savePreferences(state, prefs) { + if (prefs.allowTelemetry) { + tracker.event("telemetry", "enabled") + } else { + tracker.event("telemetry", "disabled") + } state.preferences = prefs; userStore.setPreferences(prefs); this.dispatch("destroyWebviews") diff --git a/src/renderer/api/__test__/parseAPI.test.ts b/src/renderer/api/__test__/parseAPI.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 91ed843edb..831569ef3f 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -156,9 +156,12 @@ export class HelmRelease implements ItemObject { } getChart(withVersion = false) { - return withVersion ? - this.chart : - this.chart.substr(0, this.chart.lastIndexOf("-")); + let chart = this.chart + if(!withVersion && this.getVersion() != "" ) { + const search = new RegExp(`-${this.getVersion()}`) + chart = chart.replace(search, ""); + } + return chart } getRevision() { @@ -170,7 +173,7 @@ export class HelmRelease implements ItemObject { } getVersion() { - const versions = this.chart.match(/(\d+)[^-]*$/) + const versions = this.chart.match(/(v?\d+)[^-].*$/) if (versions) { return versions[0] } diff --git a/src/renderer/api/endpoints/job.api.ts b/src/renderer/api/endpoints/job.api.ts index dce9d85c8d..d4657605f6 100644 --- a/src/renderer/api/endpoints/job.api.ts +++ b/src/renderer/api/endpoints/job.api.ts @@ -3,6 +3,7 @@ import { autobind } from "../../utils"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IPodContainer } from "./pods.api"; import { KubeApi } from "../kube-api"; +import { JsonApiParams } from "../json-api"; @autobind() export class Job extends WorkloadKubeObject { @@ -88,6 +89,13 @@ export class Job extends WorkloadKubeObject { const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []) return [...containers].map(container => container.image) } + + delete() { + const params: JsonApiParams = { + query: { propagationPolicy: "Background" } + } + return super.delete(params) + } } export const jobApi = new KubeApi({ diff --git a/src/renderer/api/endpoints/metrics.api.ts b/src/renderer/api/endpoints/metrics.api.ts index e000b665f8..edb840ba77 100644 --- a/src/renderer/api/endpoints/metrics.api.ts +++ b/src/renderer/api/endpoints/metrics.api.ts @@ -56,7 +56,21 @@ export const metricsApi = { }; export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { + if (!metrics?.data?.result) { + return { + data: { + resultType: "", + result: [{ + metric: {}, + values: [] + } as IMetricsResult], + }, + status: "", + } + } + const { result } = metrics.data; + if (result.length) { if (frames > 0) { // fill the gaps @@ -81,13 +95,16 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { } export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) { - return Object.values(metrics).every(metric => !metric.data.result.length); + return Object.values(metrics).every(metric => !metric?.data?.result?.length); } -export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string) { +export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } { if (!metrics) return; const itemMetrics = { ...metrics }; for (const metric in metrics) { + if (!metrics[metric]?.data?.result) { + continue + } const results = metrics[metric].data.result; const result = results.find(res => Object.values(res.metric)[0] == itemName); itemMetrics[metric].data.result = result ? [result] : []; diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index 9ce8d2be94..be8016c68c 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -7,6 +7,7 @@ import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } fro import { apiKube } from "./index"; import { kubeWatchApi } from "./kube-watch-api"; import { apiManager } from "./api-manager"; +import { split } from "../utils/arrays"; export interface IKubeApiOptions { kind: string; // resource type within api-group, e.g. "Namespace" @@ -32,20 +33,45 @@ export interface IKubeApiLinkRef { namespace?: string; } -export class KubeApi { - static matcher = /(\/apis?.*?)\/(?:(.*?)\/)?(v.*?)(?:\/namespaces\/(.+?))?\/([^\/]+)(?:\/([^\/?]+))?.*$/ +export interface IKubeApiLinkBase extends IKubeApiLinkRef { + apiBase: string; + apiGroup: string; + apiVersionWithGroup: string; +} - static parseApi(apiPath = "") { +export class KubeApi { + static parseApi(apiPath = ""): IKubeApiLinkBase { apiPath = new URL(apiPath, location.origin).pathname; - const [, apiPrefix, apiGroup = "", apiVersion, namespace, resource, name] = apiPath.match(KubeApi.matcher) || []; + const [, prefix, ...parts] = apiPath.split("/"); + const apiPrefix = `/${prefix}`; + + const [left, right, found] = split(parts, "namespaces"); + let apiGroup, apiVersion, namespace, resource, name; + + if (found) { + if (left.length == 0) { + throw new Error(`invalid apiPath: ${apiPath}`) + } + + apiVersion = left.pop(); + apiGroup = left.join("/"); + [namespace, resource, name] = right; + } else { + [apiGroup, apiVersion, resource] = left; + } const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/"); const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); + + if (!apiBase) { + throw new Error(`invalid apiPath: ${apiPath}`) + } + return { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, namespace, resource, name, - } + }; } static createLink(ref: IKubeApiLinkRef): string { @@ -55,7 +81,7 @@ export class KubeApi { namespace = `namespaces/${namespace}` } return [apiPrefix, apiVersion, namespace, resource, name] - .filter(v => !!v) + .filter(v => v) .join("/") } @@ -130,8 +156,9 @@ export class KubeApi { if (KubeObject.isJsonApiData(data)) { return new KubeObjectConstructor(data); } + // process items list response - else if (KubeObject.isJsonApiDataList(data)) { + if (KubeObject.isJsonApiDataList(data)) { const { apiVersion, items, metadata } = data; this.setResourceVersion(namespace, metadata.resourceVersion); this.setResourceVersion("", metadata.resourceVersion); @@ -141,10 +168,12 @@ export class KubeApi { ...item, })) } + // custom apis might return array for list response, e.g. users, groups, etc. - else if (Array.isArray(data)) { + if (Array.isArray(data)) { return data.map(data => new KubeObjectConstructor(data)); } + return data; } @@ -162,16 +191,19 @@ export class KubeApi { async create({ name = "", namespace = "default" } = {}, data?: Partial): Promise { const apiUrl = this.getUrl({ namespace }); - return this.request.post(apiUrl, { - data: merge({ - kind: this.kind, - apiVersion: this.apiVersionWithGroup, - metadata: { - name, - namespace - } - }, data) - }).then(this.parseResponse); + + return this.request + .post(apiUrl, { + data: merge({ + kind: this.kind, + apiVersion: this.apiVersionWithGroup, + metadata: { + name, + namespace + } + }, data) + }) + .then(this.parseResponse); } async update({ name = "", namespace = "default" } = {}, data?: Partial): Promise { diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index dcc6c6e61d..abb92dfe58 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -5,6 +5,7 @@ import { KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api"; import { autobind, formatDuration } from "../utils"; import { ItemObject } from "../item.store"; import { apiKube } from "./index"; +import { JsonApiParams } from "./json-api"; import { resourceApplierApi } from "./endpoints/resource-applier.api"; export type IKubeObjectConstructor = (new (data: KubeJsonApiData | any) => T) & { @@ -153,7 +154,7 @@ export class KubeObject implements ItemObject { }); } - delete() { - return apiKube.del(this.selfLink); + delete(params?: JsonApiParams) { + return apiKube.del(this.selfLink, params); } } \ No newline at end of file diff --git a/src/renderer/components/+cluster/cluster.store.ts b/src/renderer/components/+cluster/cluster.store.ts index 066ecad2f2..01c4e0f013 100644 --- a/src/renderer/components/+cluster/cluster.store.ts +++ b/src/renderer/components/+cluster/cluster.store.ts @@ -82,15 +82,15 @@ export class ClusterStore extends KubeObjectStore { this.liveMetrics = await this.loadMetrics({ start, end, step, range }); } - getMetricsValues(source: Partial) { - const metrics = - this.metricType === MetricType.CPU ? source.cpuUsage : - this.metricType === MetricType.MEMORY ? source.memoryUsage - : null; - if (!metrics) { + getMetricsValues(source: Partial): [number, string][] { + switch (this.metricType) { + case MetricType.CPU: + return normalizeMetrics(source.cpuUsage).data.result[0].values + case MetricType.MEMORY: + return normalizeMetrics(source.memoryUsage).data.result[0].values + default: return []; } - return normalizeMetrics(metrics).data.result[0].values; } resetMetrics() { diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index b4b5fd15cb..925835ff19 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -34,7 +34,7 @@ export class NamespaceSelect extends React.Component { private unsubscribe = noop; async componentDidMount() { - if (isAllowedResource("namespaces") && !namespaceStore.isLoaded) { + if (!namespaceStore.isLoaded) { await namespaceStore.loadAll(); } this.unsubscribe = namespaceStore.subscribe(); diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 53ea00f19f..61797212f8 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -4,6 +4,7 @@ import { KubeObjectStore } from "../../kube-object.store"; import { Namespace, namespacesApi } from "../../api/endpoints"; import { IQueryParams, navigation, setQueryParams } from "../../navigation"; import { apiManager } from "../../api/api-manager"; +import { isAllowedResource } from "../..//api/rbac"; @autobind() export class NamespaceStore extends KubeObjectStore { @@ -43,6 +44,16 @@ export class NamespaceStore extends KubeObjectStore { } protected loadItems(namespaces?: string[]) { + if (!isAllowedResource("namespaces")) { + if (namespaces) { + return Promise.all(namespaces.map(name => this.getDummyNamespace(name))) + } + else { + return new Promise(() => { + return [] + }) + } + } if (namespaces) { return Promise.all(namespaces.map(name => this.api.get({ name }))) } @@ -51,6 +62,19 @@ export class NamespaceStore extends KubeObjectStore { } } + protected getDummyNamespace(name: string) { + return new Namespace({ + kind: "Namespace", + apiVersion: "v1", + metadata: { + name: name, + uid: "", + resourceVersion: "", + selfLink: `/api/v1/namespaces/${name}` + } + }) + } + setContext(namespaces: string[]) { this.contextNs.replace(namespaces); } diff --git a/src/renderer/components/+network-ingresses/ingress-charts.tsx b/src/renderer/components/+network-ingresses/ingress-charts.tsx index a3aec31880..89ceb86b3d 100644 --- a/src/renderer/components/+network-ingresses/ingress-charts.tsx +++ b/src/renderer/components/+network-ingresses/ingress-charts.tsx @@ -18,9 +18,9 @@ export const IngressCharts = observer(() => { if (!metrics) return null; if (isMetricsEmpty(metrics)) return ; - const values = Object.values(metrics).map(metric => - normalizeMetrics(metric).data.result[0].values - ); + const values = Object.values(metrics) + .map(normalizeMetrics) + .map(({ data }) => data.result[0].values); const [ bytesSentSuccess, bytesSentFailure, diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx index 8fbc47cd78..d421f4692e 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx @@ -96,7 +96,7 @@ export class DeploymentScaleDialog extends Component { Desired number of replicas: {desiredReplicas}
- +
{warning && diff --git a/src/renderer/components/+workloads-pods/container-charts.tsx b/src/renderer/components/+workloads-pods/container-charts.tsx index 138b659403..f879fa4662 100644 --- a/src/renderer/components/+workloads-pods/container-charts.tsx +++ b/src/renderer/components/+workloads-pods/container-charts.tsx @@ -17,9 +17,9 @@ export const ContainerCharts = () => { if (!metrics) return null; if (isMetricsEmpty(metrics)) return ; - const values = Object.values(metrics).map(metric => - normalizeMetrics(metric).data.result[0].values - ); + const values = Object.values(metrics) + .map(normalizeMetrics) + .map(({ data }) => data.result[0].values); const [ cpuUsage, cpuRequests, diff --git a/src/renderer/components/+workloads-pods/pod-charts.tsx b/src/renderer/components/+workloads-pods/pod-charts.tsx index b46a25d010..39439d9678 100644 --- a/src/renderer/components/+workloads-pods/pod-charts.tsx +++ b/src/renderer/components/+workloads-pods/pod-charts.tsx @@ -28,9 +28,9 @@ export const PodCharts = observer(() => { if (isMetricsEmpty(metrics)) return ; const options = tabId == 0 ? cpuOptions : memoryOptions; - const values = Object.values(metrics).map(metric => - normalizeMetrics(metric).data.result[0].values - ); + const values = Object.values(metrics) + .map(normalizeMetrics) + .map(({ data }) => data.result[0].values); const [ cpuUsage, cpuRequests, diff --git a/src/renderer/components/+workloads-pods/pod-container-env.tsx b/src/renderer/components/+workloads-pods/pod-container-env.tsx index 2896a06215..f5cce6fa8c 100644 --- a/src/renderer/components/+workloads-pods/pod-container-env.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-env.tsx @@ -120,8 +120,11 @@ const SecretKey = (props: SecretKeyProps) => { setSecret(secret) } - if (!secret) { - return ( + if (secret?.data?.[key]) { + return <>{base64.decode(secret.data[key])} + } + + return ( <> secretKeyRef({name}.{key})  { onClick={showKey} /> - ) - } - return <>{base64.decode(secret.data[key])} -} \ No newline at end of file + ) +} diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index 56d7d95999..affc93f317 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -139,7 +139,7 @@ export function BarChart(props: Props) { } }; const options = merge(barOptions, customOptions); - if (!chartData.datasets.length) { + if (chartData.datasets.length == 0) { return } return ( @@ -159,10 +159,15 @@ export const memoryOptions: ChartOptions = { scales: { yAxes: [{ ticks: { - callback: value => { - value = parseFloat(String(value)); - if (!value) return 0; - return value < 1 ? value.toFixed(3) : bytesToUnits(value); + callback: (value: number | string): string => { + if (typeof value == "string") { + const float = parseFloat(value); + if (float < 1) { + return float.toFixed(3); + } + return bytesToUnits(parseInt(value)); + } + return `${value}`; }, stepSize: 1 } @@ -184,11 +189,12 @@ export const cpuOptions: ChartOptions = { scales: { yAxes: [{ ticks: { - callback: (value: number) => { - if (value == 0) return 0; - if (value < 10) return value.toFixed(3); - if (value < 100) return value.toFixed(2); - return value.toFixed(1); + callback: (value: number | string): string => { + const float = parseFloat(`${value}`); + if (float == 0) return "0"; + if (float < 10) return float.toFixed(3); + if (float < 100) return float.toFixed(2); + return float.toFixed(1); } } }] diff --git a/src/renderer/components/chart/chart.tsx b/src/renderer/components/chart/chart.tsx index 68117314e5..45510b40f9 100644 --- a/src/renderer/components/chart/chart.tsx +++ b/src/renderer/components/chart/chart.tsx @@ -1,6 +1,6 @@ import "./chart.scss"; import React from "react"; -import ChartJS from "chart.js"; +import ChartJS, {ChartData, ChartOptions} from "chart.js"; import { isEqual, remove } from "lodash"; import { cssNames } from "../../utils"; import { StatusBrick } from "../status-brick"; @@ -17,7 +17,7 @@ export interface ChartDataSets extends ChartJS.ChartDataSets { export interface ChartProps { data: ChartData; - options?: ChartJS.ChartOptions; // Passed to ChartJS instance + options?: ChartOptions; // Passed to ChartJS instance width?: number | string; height?: number | string; type?: ChartKind; diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx index 5be33e7300..4ff6967769 100644 --- a/src/renderer/components/chart/pie-chart.tsx +++ b/src/renderer/components/chart/pie-chart.tsx @@ -5,7 +5,10 @@ import { Chart, ChartProps } from "./chart"; import { cssNames } from "../../utils"; import { themeStore } from "../../theme.store"; -export class PieChart extends React.Component { +interface Props extends ChartProps { +} + +export class PieChart extends React.Component { render() { const { data, className, options, ...chartProps } = this.props const { contentColor } = themeStore.activeTheme.colors; diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index ff47c3f727..d384c36608 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -119,12 +119,12 @@ export class Dock extends React.Component { />
- New tab }} closeOnScroll={false}> - createTerminalTab()}> + New tab }} closeOnScroll={false}> + createTerminalTab()}> Terminal session - createResourceTab()}> + createResourceTab()}> Create resource diff --git a/src/renderer/components/icon/logo-full.svg b/src/renderer/components/icon/logo-full.svg index 46e28e1164..b3b553f865 100644 --- a/src/renderer/components/icon/logo-full.svg +++ b/src/renderer/components/icon/logo-full.svg @@ -1,161 +1 @@ - - - - + diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index eb5dac108c..80c1f76d70 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -117,12 +117,11 @@ export class ItemListLayout extends React.Component { try { await Promise.all(stores.map(store => store.loadAll())); const subscriptions = stores.map(store => store.subscribe()); - + await when(() => this.isUnmounting); subscriptions.forEach(dispose => dispose()); // unsubscribe all } catch(error) { console.log("catched", error) } - await when(() => this.isUnmounting); } componentWillUnmount() { diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index f337ecc402..0939da2a82 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -226,7 +226,7 @@ class SidebarNavItem extends React.Component { } render() { - const { isHidden, subMenus = [], icon, text, url, children, className } = this.props; + const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props; if (isHidden) { return null; } @@ -234,7 +234,7 @@ class SidebarNavItem extends React.Component { if (extendedView) { const isActive = this.isActive(); return ( -
+
{icon} {text} diff --git a/src/renderer/utils/__tests__/arrays.test.ts b/src/renderer/utils/__tests__/arrays.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/renderer/utils/arrays.ts b/src/renderer/utils/arrays.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 1b688e944a..18bcfe72cd 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,15 +2,25 @@ 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 your might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 3.5.0-beta.1 (current version) +## 3.5.0 (current version) -- Dynamic dashboard UI based on RBAC rules +- Dynamic dashboard UI based on RBAC rules (hides non-accessible menus) - Show object reference for all objects - Unify scrollbars/paddings +- New logo +- Remove Helm release update checker +- Improve Helm release version detection +- Show owner reference on all resource details - Fix: add arch node selector for hybrid clusters - Fix pod shell command on Windows +- Fix app freeze after closing terminal on Windows +- Fix: use correct kubeconfig context on terminal when switching cluster +- Fix error when closing Lens on Windows +- Fix: deploy kube-state-metrics component only to amd64 nodes - Translation correction: transit to transmit - Remove Kontena reference from Lens logo +- Track telemetry pref changed event +- Integration tests using spectron ## 3.4.0 @@ -280,4 +290,3 @@ Here you can find description of changes we've built into each release. While we ## 2.0.0 Initial release of the Lens desktop application. Basic functionality with auto-import of users local kubeconfig for cluster access. -