From 21bdc6ba8f94ee766878fd4bcecfd938fa2f2749 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 6 Oct 2020 13:33:23 -0400 Subject: [PATCH 01/15] add resizing capabilities to the side bar (#1009) * add resizing capabilities to the side bar Signed-off-by: Sebastian Malton --- .../components/layout/main-layout.scss | 6 +-- .../components/layout/main-layout.tsx | 40 +++++++++++++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/renderer/components/layout/main-layout.scss b/src/renderer/components/layout/main-layout.scss index 3a02d36717..d423b0388f 100755 --- a/src/renderer/components/layout/main-layout.scss +++ b/src/renderer/components/layout/main-layout.scss @@ -1,6 +1,4 @@ .MainLayout { - --sidebar-max-size: 200px; - display: grid; grid-template-areas: "aside header" @@ -35,7 +33,7 @@ transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1); &.pinned { - width: var(--sidebar-max-size); + width: var(--sidebar-width); } &:not(.pinned) { @@ -45,7 +43,7 @@ overflow: hidden; &.accessible:hover { - width: var(--sidebar-max-size); + width: var(--sidebar-width); transition-delay: 750ms; box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35); z-index: $zIndex-sidebar-hover; diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx index 97f5727a2d..43c0c01670 100755 --- a/src/renderer/components/layout/main-layout.tsx +++ b/src/renderer/components/layout/main-layout.tsx @@ -3,11 +3,12 @@ import "./main-layout.scss"; import React from "react"; import { observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { createStorage, cssNames } from "../../utils"; +import { autobind, createStorage, cssNames } from "../../utils"; import { Sidebar } from "./sidebar"; import { ErrorBoundary } from "../error-boundary"; import { Dock } from "../dock"; import { getHostedCluster } from "../../../common/cluster-store"; +import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; interface Props { className?: any; @@ -18,22 +19,42 @@ interface Props { @observer export class MainLayout extends React.Component { - public storage = createStorage("main_layout", { pinnedSidebar: true }); + public storage = createStorage("main_layout", { + pinnedSidebar: true, + sidebarWidth: 200, + }); @observable isPinned = this.storage.get().pinnedSidebar; @observable isAccessible = true; + @observable sidebarWidth = this.storage.get().sidebarWidth @disposeOnUnmount syncPinnedStateWithStorage = reaction( () => this.isPinned, (isPinned) => this.storage.merge({ pinnedSidebar: isPinned }) ); + @disposeOnUnmount syncWidthStateWithStorage = reaction( + () => this.sidebarWidth, + (sidebarWidth) => this.storage.merge({ sidebarWidth }) + ); + toggleSidebar = () => { this.isPinned = !this.isPinned; this.isAccessible = false; setTimeout(() => (this.isAccessible = true), 250); }; + getSidebarSize = () => { + return { + "--sidebar-width": `${this.sidebarWidth}px`, + } + } + + @autobind() + adjustWidth(newWidth: number): void { + this.sidebarWidth = newWidth + } + render() { const { className, headerClass, footer, footerClass, children } = this.props; const cluster = getHostedCluster(); @@ -41,20 +62,31 @@ export class MainLayout extends React.Component { return null; // fix: skip render when removing active (visible) cluster } return ( -
+
{cluster.preferences.clusterName || cluster.contextName}
{children}
-
{footer === undefined ? : footer}
+
{footer ?? }
); } From 44ce51613b192969ccc2b7cacedaff6109957119 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 7 Oct 2020 10:17:31 +0300 Subject: [PATCH 02/15] Release v3.6.6 (#1033) Signed-off-by: Lauri Nevala --- package.json | 2 +- static/RELEASE_NOTES.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0d1589db30..5b31e6e534 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "3.6.5", + "version": "3.6.6", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 11e3ef4aa7..39d8b45aff 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,12 @@ 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! -## 3.6.5 (current version) +## 3.6.6 (current version) +- Fix labels' word boundary to cover only drawer badges +- Fix cluster dashboard opening not to start authentication proxy twice +- Fix: Refresh cluster connection status also when connection is disconnected + +## 3.6.5 - Prevent drawer close when revealing secret value - Fix app crash when CRD conditions were not present - Add support for Stacklight prometheus metrics From abe6a4e0b1184922b2f1b8c552020dabe650764a Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 7 Oct 2020 15:27:48 +0300 Subject: [PATCH 03/15] Fix cluster dashboard opening when cluster is initally offline (#1042) Signed-off-by: Lauri Nevala --- src/main/cluster.ts | 5 ++++- src/renderer/components/cluster-manager/cluster-view.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 3e3ba3f6b5..9c4afd71d0 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -184,6 +184,9 @@ export class Cluster implements ClusterModel { this.refreshEvents(), this.refreshAllowedResources(), ]); + if (!this.ready) { + this.ready = true + } } this.pushState(); } @@ -234,7 +237,7 @@ export class Cluster implements ClusterModel { const apiUrl = this.kubeProxyUrl + path; return request(apiUrl, { json: true, - timeout: 5000, + timeout: 30000, ...options, headers: { Host: `${this.id}.${new URL(this.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest() diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index f537352c22..7a7c96a856 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -9,7 +9,7 @@ import { hasLoadedView } from "./lens-views"; export class ClusterView extends React.Component { render() { const cluster = getMatchedCluster(); - const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id)) + const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready) return (
{showStatus && ( From 09602592794193a70ce09b25ad799377a727f983 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 9 Oct 2020 11:25:20 -0400 Subject: [PATCH 04/15] Add some basic unit tests for the following files: (#1050) * Add some basic unit tests for cluster, kube-auth-proxy, and kubeconfig-manager - src/main/cluster.ts - src/main/kube-auth-proxy.ts - src/main/kubeconfig-manager.ts Signed-off-by: Sebastian Malton --- integration/__tests__/app.tests.ts | 434 +++++++++---------- integration/helpers/utils.ts | 2 +- package.json | 1 + src/main/__test__/cluster.test.ts | 165 +++++++ src/main/__test__/kube-auth-proxy.test.ts | 130 ++++++ src/main/__test__/kubeconfig-manager.test.ts | 112 +++++ src/main/cluster.ts | 2 +- src/main/kubeconfig-manager.ts | 17 +- yarn.lock | 12 + 9 files changed, 651 insertions(+), 224 deletions(-) create mode 100644 src/main/__test__/cluster.test.ts create mode 100644 src/main/__test__/kube-auth-proxy.test.ts create mode 100644 src/main/__test__/kubeconfig-manager.test.ts diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 5cde34afb5..492f02d1e6 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -1,6 +1,6 @@ /* Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE - namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the + namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube cluster and vice versa. */ @@ -8,8 +8,8 @@ import { Application } from "spectron" import * as util from "../helpers/utils" import { spawnSync } from "child_process" -const describeif = (condition : boolean) => condition ? describe : describe.skip -const itif = (condition : boolean) => condition ? it : it.skip +const describeif = (condition: boolean) => condition ? describe : describe.skip +const itif = (condition: boolean) => condition ? it : it.skip jest.setTimeout(60000) @@ -29,7 +29,7 @@ describe("Lens integration tests", () => { } const clickWhatsNew = async (app: Application) => { - await app.client.waitUntilTextExists("h1", "What's new") + await app.client.waitUntilTextExists("h1", "What's new?") await app.client.click("button.primary") await app.client.waitUntilTextExists("h1", "Welcome") } @@ -140,7 +140,7 @@ describe("Lens integration tests", () => { await addCluster() } } - + describe("cluster pages", () => { beforeAll(appStartAddCluster, 40000) @@ -150,8 +150,8 @@ describe("Lens integration tests", () => { return util.tearDown(app) } }) - - const tests : { + + const tests: { drawer?: string drawerId?: string pages: { @@ -160,232 +160,230 @@ describe("Lens integration tests", () => { expectedSelector: string, expectedText: string }[] - }[] = [ - { - drawer: "", - drawerId: "", - pages: [ { - name: "Cluster", - href: "cluster", - expectedSelector: "div.ClusterNoMetrics p", - expectedText: "Metrics are not available due" - }] + }[] = [{ + drawer: "", + drawerId: "", + pages: [{ + name: "Cluster", + href: "cluster", + expectedSelector: "div.ClusterNoMetrics p", + expectedText: "Metrics are not available due" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Nodes", + href: "nodes", + expectedSelector: "h5.title", + expectedText: "Nodes" + }] + }, + { + drawer: "Workloads", + drawerId: "workloads", + pages: [{ + name: "Overview", + href: "workloads", + expectedSelector: "h5.box", + expectedText: "Overview" }, { - drawer: "", - drawerId: "", - pages: [ { - name: "Nodes", - href: "nodes", - expectedSelector: "h5.title", - expectedText: "Nodes" - }] + name: "Pods", + href: "pods", + expectedSelector: "h5.title", + expectedText: "Pods" }, { - drawer: "Workloads", - drawerId: "workloads", - pages: [ { - name: "Overview", - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - }, - { - name: "Pods", - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - }, - { - name: "Deployments", - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - }, - { - name: "DaemonSets", - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - }, - { - name: "StatefulSets", - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - }, - { - name: "Jobs", - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - }, - { - name: "CronJobs", - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - } ] + name: "Deployments", + href: "deployments", + expectedSelector: "h5.title", + expectedText: "Deployments" }, { - drawer: "Configuration", - drawerId: "config", - pages: [ { - name: "ConfigMaps", - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - }, - { - name: "Secrets", - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - }, - { - name: "Resource Quotas", - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - }, - { - name: "HPA", - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - }, - { - name: "Pod Disruption Budgets", - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - } ] + name: "DaemonSets", + href: "daemonsets", + expectedSelector: "h5.title", + expectedText: "Daemon Sets" }, { - drawer: "Network", - drawerId: "networks", - pages: [ { - name: "Services", - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - }, - { - name: "Endpoints", - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - }, - { - name: "Ingresses", - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - }, - { - name: "Network Policies", - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - } ] + name: "StatefulSets", + href: "statefulsets", + expectedSelector: "h5.title", + expectedText: "Stateful Sets" }, { - drawer: "Storage", - drawerId: "storage", - pages: [ { - name: "Persistent Volume Claims", - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - }, - { - name: "Persistent Volumes", - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - }, - { - name: "Storage Classes", - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - } ] + name: "Jobs", + href: "jobs", + expectedSelector: "h5.title", + expectedText: "Jobs" }, { - drawer: "", - drawerId: "", - pages: [ { - name: "Namespaces", - href: "namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces" - }] + name: "CronJobs", + href: "cronjobs", + expectedSelector: "h5.title", + expectedText: "Cron Jobs" + }] + }, + { + drawer: "Configuration", + drawerId: "config", + pages: [{ + name: "ConfigMaps", + href: "configmaps", + expectedSelector: "h5.title", + expectedText: "Config Maps" }, { - drawer: "", - drawerId: "", - pages: [ { - name: "Events", - href: "events", - expectedSelector: "h5.title", - expectedText: "Events" - }] + name: "Secrets", + href: "secrets", + expectedSelector: "h5.title", + expectedText: "Secrets" }, { - drawer: "Apps", - drawerId: "apps", - pages: [ { - name: "Charts", - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - expectedText: "" - }, - { - name: "Releases", - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - } ] + name: "Resource Quotas", + href: "resourcequotas", + expectedSelector: "h5.title", + expectedText: "Resource Quotas" }, { - drawer: "Access Control", - drawerId: "users", - pages: [ { - name: "Service Accounts", - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - }, - { - name: "Role Bindings", - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - }, - { - name: "Roles", - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - }, - { - name: "Pod Security Policies", - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - } ] + name: "HPA", + href: "hpa", + expectedSelector: "h5.title", + expectedText: "Horizontal Pod Autoscalers" }, { - drawer: "Custom Resources", - drawerId: "custom-resources", - pages: [ { - name: "Definitions", - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - } ] + name: "Pod Disruption Budgets", + href: "poddisruptionbudgets", + expectedSelector: "h5.title", + expectedText: "Pod Disruption Budgets" + }] + }, + { + drawer: "Network", + drawerId: "networks", + pages: [{ + name: "Services", + href: "services", + expectedSelector: "h5.title", + expectedText: "Services" }, - ]; + { + name: "Endpoints", + href: "endpoints", + expectedSelector: "h5.title", + expectedText: "Endpoints" + }, + { + name: "Ingresses", + href: "ingresses", + expectedSelector: "h5.title", + expectedText: "Ingresses" + }, + { + name: "Network Policies", + href: "network-policies", + expectedSelector: "h5.title", + expectedText: "Network Policies" + }] + }, + { + drawer: "Storage", + drawerId: "storage", + pages: [{ + name: "Persistent Volume Claims", + href: "persistent-volume-claims", + expectedSelector: "h5.title", + expectedText: "Persistent Volume Claims" + }, + { + name: "Persistent Volumes", + href: "persistent-volumes", + expectedSelector: "h5.title", + expectedText: "Persistent Volumes" + }, + { + name: "Storage Classes", + href: "storage-classes", + expectedSelector: "h5.title", + expectedText: "Storage Classes" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Namespaces", + href: "namespaces", + expectedSelector: "h5.title", + expectedText: "Namespaces" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Events", + href: "events", + expectedSelector: "h5.title", + expectedText: "Events" + }] + }, + { + drawer: "Apps", + drawerId: "apps", + pages: [{ + name: "Charts", + href: "apps/charts", + expectedSelector: "div.HelmCharts input", + expectedText: "" + }, + { + name: "Releases", + href: "apps/releases", + expectedSelector: "h5.title", + expectedText: "Releases" + }] + }, + { + drawer: "Access Control", + drawerId: "users", + pages: [{ + name: "Service Accounts", + href: "service-accounts", + expectedSelector: "h5.title", + expectedText: "Service Accounts" + }, + { + name: "Role Bindings", + href: "role-bindings", + expectedSelector: "h5.title", + expectedText: "Role Bindings" + }, + { + name: "Roles", + href: "roles", + expectedSelector: "h5.title", + expectedText: "Roles" + }, + { + name: "Pod Security Policies", + href: "pod-security-policies", + expectedSelector: "h5.title", + expectedText: "Pod Security Policies" + }] + }, + { + drawer: "Custom Resources", + drawerId: "custom-resources", + pages: [{ + name: "Definitions", + href: "crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }] + }]; tests.forEach(({ drawer = "", drawerId = "", pages }) => { if (drawer !== "") { it(`shows ${drawer} drawer`, async () => { @@ -393,8 +391,8 @@ describe("Lens integration tests", () => { await app.client.click(`.sidebar-nav #${drawerId} span.link-text`) await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name) }) - } - pages.forEach(({name, href, expectedSelector, expectedText}) => { + } + pages.forEach(({ name, href, expectedSelector, expectedText }) => { it(`shows ${drawer}->${name} page`, async () => { expect(clusterAdded).toBe(true) await app.client.click(`a[href="/${href}"]`) @@ -409,7 +407,7 @@ describe("Lens integration tests", () => { await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow() }) } - }) + }) }) describe("cluster operations", () => { @@ -420,7 +418,7 @@ describe("Lens integration tests", () => { return util.tearDown(app) } }) - + it('shows default namespace', async () => { expect(clusterAdded).toBe(true) await app.client.click('a[href="/namespaces"]') @@ -468,6 +466,6 @@ describe("Lens integration tests", () => { await app.client.click(".name=nginx-create-pod-test") await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test") }) - }) - }) + }) + }) }) diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index a50b13e023..55395f84e3 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -12,7 +12,7 @@ export function setup(): Application { args: [], path: AppPaths[process.platform], startTimeout: 30000, - waitTimeout: 30000, + waitTimeout: 60000, chromeDriverArgs: ['remote-debugging-port=9222'], env: { CICD: "true" diff --git a/package.json b/package.json index 5b31e6e534..ea65b9d2f0 100644 --- a/package.json +++ b/package.json @@ -289,6 +289,7 @@ "identity-obj-proxy": "^3.0.0", "include-media": "^1.4.9", "jest": "^26.0.1", + "jest-mock-extended": "^1.0.10", "make-plural": "^6.2.1", "material-design-icons": "^3.0.1", "mini-css-extract-plugin": "^0.9.0", diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts new file mode 100644 index 0000000000..52302c7064 --- /dev/null +++ b/src/main/__test__/cluster.test.ts @@ -0,0 +1,165 @@ +const logger = { + silly: jest.fn(), + debug: jest.fn(), + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + crit: jest.fn(), +}; + +jest.mock("winston", () => ({ + format: { + colorize: jest.fn(), + combine: jest.fn(), + simple: jest.fn(), + label: jest.fn(), + timestamp: jest.fn(), + printf: jest.fn() + }, + createLogger: jest.fn().mockReturnValue(logger), + transports: { + Console: jest.fn(), + File: jest.fn(), + } +})) + + +jest.mock("../../common/ipc") +jest.mock("../context-handler") +jest.mock("request") +jest.mock("request-promise-native") + +import { Console } from "console"; +import mockFs from "mock-fs"; +import { workspaceStore } from "../../common/workspace-store"; +import { Cluster } from "../cluster" +import { ContextHandler } from "../context-handler"; +import { getFreePort } from "../port"; +import { V1ResourceAttributes } from "@kubernetes/client-node"; +import { apiResources } from "../../common/rbac"; +import request from "request-promise-native" + +const mockedRequest = request as jest.MockedFunction + +console = new Console(process.stdout, process.stderr) // fix mockFS + +describe("create clusters", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + beforeEach(() => { + const mockOpts = { + "minikube-config.yml": JSON.stringify({ + apiVersion: "v1", + clusters: [{ + name: "minikube", + cluster: { + server: "https://192.168.64.3:8443", + }, + }], + contexts: [{ + context: { + cluster: "minikube", + user: "minikube", + }, + name: "minikube", + }], + users: [{ + name: "minikube", + }], + kind: "Config", + preferences: {}, + }) + } + mockFs(mockOpts) + }) + + afterEach(() => { + mockFs.restore() + }) + + it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => { + const c = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + expect(c.apiUrl).toBe("https://192.168.64.3:8443") + }) + + it("init should not throw if everything is in order", async () => { + const c = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + await c.init(await getFreePort()) + expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), { + id: "foo", + apiUrl: "https://192.168.64.3:8443", + context: "minikube", + }) + }) + + it("activating cluster should try to connect to cluster and do a refresh", async () => { + const port = await getFreePort() + jest.spyOn(ContextHandler.prototype, "ensureServer"); + + const mockListNSs = jest.fn() + const mockKC = { + makeApiClient() { + return { + listNamespace: mockListNSs, + } + } + } + + jest.spyOn(Cluster.prototype, "canI") + .mockImplementationOnce((attr: V1ResourceAttributes): Promise => { + expect(attr.namespace).toBe("default") + expect(attr.resource).toBe("pods") + expect(attr.verb).toBe("list") + return Promise.resolve(true) + }) + .mockImplementation((attr: V1ResourceAttributes): Promise => { + expect(attr.namespace).toBe("default") + expect(attr.verb).toBe("list") + return Promise.resolve(true) + }) + jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any) + mockListNSs.mockImplementationOnce(() => ({ + body: { + items: [{ + metadata: { + name: "default", + } + }] + } + })) + + mockedRequest.mockImplementationOnce(((uri: any, _options: any) => { + expect(uri).toBe(`http://localhost:${port}/api-kube/version`) + return Promise.resolve({ gitVersion: "1.2.3" }) + }) as any) + + const c = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + await c.init(port) + await c.activate() + + expect(ContextHandler.prototype.ensureServer).toBeCalled() + expect(mockedRequest).toBeCalled() + expect(c.accessible).toBe(true) + expect(c.allowedNamespaces.length).toBe(1) + expect(c.allowedResources.length).toBe(apiResources.length) + + jest.resetAllMocks() + }) +}) diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts new file mode 100644 index 0000000000..48f430b7c5 --- /dev/null +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -0,0 +1,130 @@ +const logger = { + silly: jest.fn(), + debug: jest.fn(), + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + crit: jest.fn(), +}; + +jest.mock("winston", () => ({ + format: { + colorize: jest.fn(), + combine: jest.fn(), + simple: jest.fn(), + label: jest.fn(), + timestamp: jest.fn(), + printf: jest.fn() + }, + createLogger: jest.fn().mockReturnValue(logger), + transports: { + Console: jest.fn(), + File: jest.fn(), + } +})) + +jest.mock("../../common/ipc") +jest.mock("child_process") +jest.mock("tcp-port-used") + +import { Cluster } from "../cluster" +import { KubeAuthProxy } from "../kube-auth-proxy" +import { getFreePort } from "../port" +import { broadcastIpc } from "../../common/ipc" +import { ChildProcess, spawn, SpawnOptions } from "child_process" +import { Kubectl } from "../kubectl" +import { mock, MockProxy } from 'jest-mock-extended'; +import { waitUntilUsed } from 'tcp-port-used'; +import { Readable } from "stream" + +const mockBroadcastIpc = broadcastIpc as jest.MockedFunction +const mockSpawn = spawn as jest.MockedFunction +const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction + +describe("kube auth proxy tests", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("calling exit multiple times shouldn't throw", async () => { + const port = await getFreePort() + const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}) + kap.exit() + kap.exit() + kap.exit() + }) + + describe("spawn tests", () => { + let port: number + let mockedCP: MockProxy + let listeners: Record void> + + beforeEach(async () => { + port = await getFreePort() + mockedCP = mock() + listeners = {} + + jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true)) + jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false)) + mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => { + listeners[event] = listener + return mockedCP + }) + mockedCP.stderr = mock() + mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => { + listeners[`stderr/${event}`] = listener + return mockedCP.stderr + }) + mockedCP.stdout = mock() + mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => { + listeners[`stdout/${event}`] = listener + return mockedCP.stdout + }) + mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => { + expect(command).toBe(Kubectl.bundledKubectlPath) + return mockedCP + }) + mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()) + }) + + it("should call spawn and broadcast errors", async () => { + const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}) + await kap.run() + listeners["error"]({ message: "foobarbat" }) + + expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "foobarbat", error: true }] }) + }) + + it("should call spawn and broadcast exit", async () => { + const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}) + await kap.run() + listeners["exit"](0) + + expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "proxy exited with code: 0", error: false }] }) + }) + + it("should call spawn and broadcast errors from stderr", async () => { + const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}) + await kap.run() + listeners["stderr/data"]("an error") + + expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "an error", error: true }] }) + }) + + it("should call spawn and broadcast stdout serving info", async () => { + const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}) + await kap.run() + listeners["stdout/data"]("Starting to serve on") + + expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "Authentication proxy started\n" }] }) + }) + + it("should call spawn and broadcast stdout other info", async () => { + const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}) + await kap.run() + listeners["stdout/data"]("some info") + + expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "some info" }] }) + }) + }) +}) diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts new file mode 100644 index 0000000000..a0a4060111 --- /dev/null +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -0,0 +1,112 @@ +const logger = { + silly: jest.fn(), + debug: jest.fn(), + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + crit: jest.fn(), +}; + +jest.mock("winston", () => ({ + format: { + colorize: jest.fn(), + combine: jest.fn(), + simple: jest.fn(), + label: jest.fn(), + timestamp: jest.fn(), + printf: jest.fn() + }, + createLogger: jest.fn().mockReturnValue(logger), + transports: { + Console: jest.fn(), + File: jest.fn(), + } +})) + +import { KubeconfigManager } from "../kubeconfig-manager" +import mockFs from "mock-fs" +import { Cluster } from "../cluster"; +import { workspaceStore } from "../../common/workspace-store"; +import { ContextHandler } from "../context-handler"; +import { getFreePort } from "../port"; +import fse from "fs-extra" +import { loadYaml } from "@kubernetes/client-node"; +import { Console } from "console"; + +console = new Console(process.stdout, process.stderr) // fix mockFS + +describe("kubeconfig manager tests", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + beforeEach(() => { + const mockOpts = { + "minikube-config.yml": JSON.stringify({ + apiVersion: "v1", + clusters: [{ + name: "minikube", + cluster: { + server: "https://192.168.64.3:8443", + }, + }], + contexts: [{ + context: { + cluster: "minikube", + user: "minikube", + }, + name: "minikube", + }], + users: [{ + name: "minikube", + }], + kind: "Config", + preferences: {}, + }) + } + mockFs(mockOpts) + }) + + afterEach(() => { + mockFs.restore() + }) + + it("should create 'temp' kube config with proxy", async () => { + const cluster = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + const contextHandler = new ContextHandler(cluster) + const port = await getFreePort() + const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port) + + expect(logger.error).not.toBeCalled() + expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo") + const file = await fse.readFile(kubeConfManager.getPath()) + const yml = loadYaml(file.toString()) + expect(yml["current-context"]).toBe("minikube") + expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`) + expect(yml["users"][0]["name"]).toBe("proxy") + }) + + it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => { + const cluster = new Cluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath: "minikube-config.yml", + workspace: workspaceStore.currentWorkspaceId + }) + const contextHandler = new ContextHandler(cluster) + const port = await getFreePort() + const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port) + + const configPath = kubeConfManager.getPath() + expect(await fse.pathExists(configPath)).toBe(true) + await kubeConfManager.unlink() + expect(await fse.pathExists(configPath)).toBe(false) + await kubeConfManager.unlink() // doesn't throw + expect(kubeConfManager.getPath()).toBeUndefined() + }) +}) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 9c4afd71d0..f9b3c53ddc 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -93,7 +93,7 @@ export class Cluster implements ClusterModel { async init(port: number) { try { this.contextHandler = new ContextHandler(this); - this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler, port); + this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port); this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; this.initialized = true; logger.info(`[CLUSTER]: "${this.contextName}" init success`, { diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index 59a47c3fbc..fc84d00ddb 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -11,8 +11,12 @@ export class KubeconfigManager { protected configDir = app.getPath("temp") protected tempFile: string; - constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { - this.init(); + private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { } + + static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) { + const kcm = new KubeconfigManager(cluster, contextHandler, port) + await kcm.init() + return kcm } protected async init() { @@ -72,8 +76,13 @@ export class KubeconfigManager { return tempFile; } - unlink() { + async unlink() { + if (!this.tempFile) { + return + } + logger.info('Deleting temporary kubeconfig: ' + this.tempFile) - fs.unlinkSync(this.tempFile) + await fs.unlink(this.tempFile) + this.tempFile = undefined } } diff --git a/yarn.lock b/yarn.lock index f64e36b703..1c92dd0978 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7080,6 +7080,13 @@ jest-message-util@^26.0.1: slash "^3.0.0" stack-utils "^2.0.2" +jest-mock-extended@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.10.tgz#a4b1f5b0bb1121acf7c58cd5423d04c473532702" + integrity sha512-R2wKiOgEUPoHZ2kLsAQeQP2IfVEgo3oQqWLSXKdMXK06t3UHkQirA2Xnsdqg/pX6KPWTsdnrzE2ig6nqNjdgVw== + dependencies: + ts-essentials "^4.0.0" + jest-mock@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.0.1.tgz#7fd1517ed4955397cf1620a771dc2d61fad8fd40" @@ -11382,6 +11389,11 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" +ts-essentials@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab" + integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ== + ts-jest@^26.1.0: version "26.3.0" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.3.0.tgz#6b2845045347dce394f069bb59358253bc1338a9" From f4f0cba6cada4073c90a5d0aa82469ec45270f81 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Sun, 11 Oct 2020 19:36:19 +0300 Subject: [PATCH 05/15] Moving Pod logs into Dock panel (#1043) * Moving pod logs into Dock Signed-off-by: Alex Andreev * Always set up default container Signed-off-by: Alex Andreev * Open existent tab if fount Signed-off-by: Alex Andreev * Moving logs load and properties into store Signed-off-by: Alex Andreev * Setting a refresher Signed-off-by: Alex Andreev * Adding showButtons prop to InfoPanel Signed-off-by: Alex Andreev * Hiding sequence number in log tabs Signed-off-by: Alex Andreev * Removing PodLogsDialog Signed-off-by: Alex Andreev * Removing unused PodLogsDialog import Signed-off-by: Alex Andreev * Tiny cleaning Signed-off-by: Alex Andreev * A bit of cleaning up Signed-off-by: Alex Andreev * Hiding drawer when opening logs Signed-off-by: Alex Andreev --- src/renderer/api/endpoints/pods.api.ts | 1 + .../+workloads-pods/pod-logs-dialog.scss | 110 ------- .../+workloads-pods/pod-logs-dialog.tsx | 307 ------------------ .../components/+workloads-pods/pod-menu.tsx | 15 +- src/renderer/components/app.tsx | 2 - src/renderer/components/dock/dock.store.ts | 1 + src/renderer/components/dock/dock.tsx | 6 + src/renderer/components/dock/info-panel.tsx | 42 ++- src/renderer/components/dock/pod-logs.scss | 43 +++ .../components/dock/pod-logs.store.ts | 126 +++++++ src/renderer/components/dock/pod-logs.tsx | 235 ++++++++++++++ src/renderer/themes/kontena-dark.json | 1 + src/renderer/themes/kontena-light.json | 1 + src/renderer/themes/theme-vars.scss | 3 + 14 files changed, 453 insertions(+), 440 deletions(-) delete mode 100644 src/renderer/components/+workloads-pods/pod-logs-dialog.scss delete mode 100644 src/renderer/components/+workloads-pods/pod-logs-dialog.tsx create mode 100644 src/renderer/components/dock/pod-logs.scss create mode 100644 src/renderer/components/dock/pod-logs.store.ts create mode 100644 src/renderer/components/dock/pod-logs.tsx diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index c1394ab6db..e1545f09c7 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -47,6 +47,7 @@ export interface IPodLogsQuery { tailLines?: number; timestamps?: boolean; sinceTime?: string; // Date.toISOString()-format + follow?: boolean; } export enum PodStatus { diff --git a/src/renderer/components/+workloads-pods/pod-logs-dialog.scss b/src/renderer/components/+workloads-pods/pod-logs-dialog.scss deleted file mode 100644 index 0c8845c78f..0000000000 --- a/src/renderer/components/+workloads-pods/pod-logs-dialog.scss +++ /dev/null @@ -1,110 +0,0 @@ -.PodLogsDialog { - --log-line-height: 16px; - - .Wizard { - width: 90vw; - max-height: none; - - .WizardStep { - & > .step-content.scrollable { - max-height: none; - } - - & > :last-child { - padding: $padding * 2; - } - } - } - - .log-controls { - padding-bottom: $padding * 2; - - .time-range { - flex-grow: 2; - text-align: center; - } - - .controls { - width: 100%; - } - - .control-buttons { - margin-right: 0; - white-space: nowrap; - - .Icon { - border-radius: $radius; - padding: 3px; - - &:hover { - color: $textColorPrimary; - background: #f4f4f4; - } - - &.active { - color: $primary; - background: #f4f4f4; - } - } - } - - @include media("<=desktop") { - flex-direction: column; - align-items: start; - - .container { - width: 100%; - } - - .controls { - margin-top: $margin * 2; - - .time-range { - text-align: left; - } - } - } - } - - .logs-area { - position: relative; - @include custom-scrollbar; - - // fix for `this.logsArea.scrollTop = this.logsArea.scrollHeight` - // `overflow: overlay` don't allow scroll to the last line - overflow: auto; - - color: #C5C8C6; - background: #1D1F21; - line-height: var(--log-line-height); - border-radius: 2px; - height: 45vh; - padding: $padding / 4 $padding; - font-family: $font-monospace; - font-size: smaller; - white-space: pre; - - .no-logs { - text-align: center; - } - } - - .new-logs-sep { - position: relative; - display: block; - height: 0; - border-top: 1px solid $primary; - margin: $margin * 2; - - &:after { - position: absolute; - left: 50%; - transform: translate(-50%, -50%); - content: 'new'; - background: $primary; - color: white; - padding: $padding / 3 $padding /2; - border-radius: $radius; - } - } -} \ No newline at end of file diff --git a/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx b/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx deleted file mode 100644 index d7ff3863cc..0000000000 --- a/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import "./pod-logs-dialog.scss"; - -import React from "react"; -import { observable } from "mobx"; -import { observer } from "mobx-react"; -import { t, Trans } from "@lingui/macro"; -import { _i18n } from "../../i18n"; -import { Dialog, DialogProps } from "../dialog"; -import { Wizard, WizardStep } from "../wizard"; -import { IPodContainer, Pod, podsApi } from "../../api/endpoints"; -import { Icon } from "../icon"; -import { Select, SelectOption } from "../select"; -import { Spinner } from "../spinner"; -import { cssNames, downloadFile, interval } from "../../utils"; -import AnsiUp from "ansi_up"; -import DOMPurify from "dompurify" - -interface IPodLogsDialogData { - pod: Pod; - container?: IPodContainer; -} - -interface Props extends Partial { -} - -@observer -export class PodLogsDialog extends React.Component { - @observable static isOpen = false; - @observable static data: IPodLogsDialogData = null; - - static open(pod: Pod, container?: IPodContainer) { - PodLogsDialog.isOpen = true; - PodLogsDialog.data = { pod, container }; - } - - static close() { - PodLogsDialog.isOpen = false; - } - - get data() { - return PodLogsDialog.data; - } - - private logsArea: HTMLDivElement; - private refresher = interval(5, () => this.load()); - private containers: IPodContainer[] = [] - private initContainers: IPodContainer[] = [] - private lastLineIsShown = true; // used for proper auto-scroll content after refresh - private colorConverter = new AnsiUp(); - - @observable logs = ""; // latest downloaded logs for pod - @observable newLogs = ""; // new logs since dialog is open - @observable logsReady = false; - @observable selectedContainer: IPodContainer; - @observable showTimestamps = true; - @observable tailLines = 1000; - - lineOptions = [ - { label: _i18n._(t`All logs`), value: Number.MAX_SAFE_INTEGER }, - { label: 1000, value: 1000 }, - { label: 10000, value: 10000 }, - { label: 100000, value: 100000 }, - ] - - onOpen = async () => { - const { pod, container } = this.data; - this.containers = pod.getContainers(); - this.initContainers = pod.getInitContainers(); - this.selectedContainer = container || this.containers[0]; - await this.load(); - this.refresher.start(); - } - - onClose = () => { - this.resetLogs(); - this.refresher.stop(); - } - - close = () => { - PodLogsDialog.close(); - } - - load = async () => { - if (!this.data) return; - const { pod } = this.data; - try { - // if logs already loaded, check the latest timestamp for getting updates only from this point - const logsTimestamps = this.getTimestamps(this.newLogs || this.logs); - let lastLogDate = new Date(0) - if (logsTimestamps) { - lastLogDate = new Date(logsTimestamps.slice(-1)[0]); - lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second - } - const namespace = pod.getNs(); - const name = pod.getName(); - const logs = await podsApi.getLogs({ namespace, name }, { - container: this.selectedContainer.name, - timestamps: true, - tailLines: this.tailLines ? this.tailLines : undefined, - sinceTime: lastLogDate.toISOString(), - }); - if (!this.logs) { - this.logs = logs; - } - else if (logs) { - this.newLogs = `${this.newLogs}\n${logs}`.trim(); - } - } catch (error) { - this.logs = [ - _i18n._(t`Failed to load logs: ${error.message}`), - _i18n._(t`Reason: ${error.reason} (${error.code})`), - ].join("\n") - } - this.logsReady = true; - } - - reload = async () => { - this.resetLogs(); - this.refresher.stop(); - await this.load(); - this.refresher.start(); - } - - componentDidUpdate() { - // scroll logs only when it's already in the end, - // otherwise it can interrupt reading by jumping after loading new logs update - if (this.logsArea && this.lastLineIsShown) { - this.logsArea.scrollTop = this.logsArea.scrollHeight; - } - } - - onScroll = (evt: React.UIEvent) => { - const logsArea = evt.currentTarget; - const { scrollHeight, clientHeight, scrollTop } = logsArea; - this.lastLineIsShown = clientHeight + scrollTop === scrollHeight; - }; - - getLogs() { - const { logs, newLogs, showTimestamps } = this; - return { - logs: showTimestamps ? logs : this.removeTimestamps(logs), - newLogs: showTimestamps ? newLogs : this.removeTimestamps(newLogs), - } - } - - getTimestamps(logs: string) { - return logs.match(/^\d+\S+/gm); - } - - removeTimestamps(logs: string) { - return logs.replace(/^\d+.*?\s/gm, ""); - } - - resetLogs() { - this.logs = ""; - this.newLogs = ""; - this.lastLineIsShown = true; - this.logsReady = false; - } - - onContainerChange = (option: SelectOption) => { - this.selectedContainer = this.containers - .concat(this.initContainers) - .find(container => container.name === option.value); - this.reload(); - } - - onTailLineChange = (option: SelectOption) => { - this.tailLines = option.value; - this.reload(); - } - - formatOptionLabel = (option: SelectOption) => { - const { value, label } = option; - return label || <> {value}; - } - - toggleTimestamps = () => { - this.showTimestamps = !this.showTimestamps; - } - - downloadLogs = () => { - const { logs, newLogs } = this.getLogs(); - const fileName = this.selectedContainer.name + ".log"; - const fileContents = logs + newLogs; - downloadFile(fileName, fileContents, "text/plain"); - } - - get containerSelectOptions() { - return [ - { - label: _i18n._(t`Containers`), - options: this.containers.map(container => { - return { value: container.name } - }), - }, - { - label: _i18n._(t`Init Containers`), - options: this.initContainers.map(container => { - return { value: container.name } - }), - } - ]; - } - - renderControlsPanel() { - const { logsReady, showTimestamps } = this; - if (!logsReady) return; - const timestamps = this.getTimestamps(this.logs + this.newLogs); - let from = ""; - let to = ""; - if (timestamps) { - from = new Date(timestamps[0]).toLocaleString(); - to = new Date(timestamps[timestamps.length - 1]).toLocaleString(); - } - return ( -
-
- {timestamps && From {from} to {to}} -
-
- - -
-
- ) - } - - renderLogs() { - if (!this.logsReady) { - return - } - const { logs, newLogs } = this.getLogs(); - if (!logs && !newLogs) { - return

There are no logs available for container.

- } - return ( - <> -
- {newLogs && ( - <> -

-

- - )} - - ); - } - - render() { - const { ...dialogProps } = this.props; - const { selectedContainer, tailLines } = this; - const podName = this.data ? this.data.pod.getName() : ""; - const header =
{podName} Logs
; - return ( - - - Close}> -
-
- Container - {selectedContainer && ( - -
- {this.renderControlsPanel()} -
-
this.logsArea = e}> - {this.renderLogs()} -
-
-
-
- ) - } -} diff --git a/src/renderer/components/+workloads-pods/pod-menu.tsx b/src/renderer/components/+workloads-pods/pod-menu.tsx index cf6fcd360f..64b5d1f5eb 100644 --- a/src/renderer/components/+workloads-pods/pod-menu.tsx +++ b/src/renderer/components/+workloads-pods/pod-menu.tsx @@ -3,15 +3,15 @@ import "./pod-menu.scss"; import React from "react"; import { t, Trans } from "@lingui/macro"; import { MenuItem, SubMenu } from "../menu"; -import { IPodContainer, Pod, nodesApi } from "../../api/endpoints"; +import { IPodContainer, Pod } from "../../api/endpoints"; import { Icon } from "../icon"; import { StatusBrick } from "../status-brick"; -import { PodLogsDialog } from "./pod-logs-dialog"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { cssNames, prevDefault } from "../../utils"; import { terminalStore, createTerminalTab } from "../dock/terminal.store"; import { _i18n } from "../../i18n"; import { hideDetails } from "../../navigation"; +import { createPodLogsTab } from "../dock/pod-logs.store"; interface Props extends KubeObjectMenuProps { } @@ -42,7 +42,16 @@ export class PodMenu extends React.Component { } showLogs(container: IPodContainer) { - PodLogsDialog.open(this.props.object, container); + hideDetails(); + const pod = this.props.object; + createPodLogsTab({ + pod, + containers: pod.getContainers(), + initContainers: pod.getInitContainers(), + selectedContainer: container, + showTimestamps: false, + tailLines: 1000 + }); } renderShellMenu() { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index aee156ff31..cf8ba01003 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -23,7 +23,6 @@ import { eventRoute } from "./+events"; import { Apps, appsRoute } from "./+apps"; import { KubeObjectDetails } from "./kube-object/kube-object-details"; import { AddRoleBindingDialog } from "./+user-management-roles-bindings"; -import { PodLogsDialog } from "./+workloads-pods/pod-logs-dialog"; import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog"; import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog"; import { CustomResources } from "./+custom-resources/custom-resources"; @@ -82,7 +81,6 @@ export class App extends React.Component { - diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts index 2fed511e75..2ec01df8de 100644 --- a/src/renderer/components/dock/dock.store.ts +++ b/src/renderer/components/dock/dock.store.ts @@ -11,6 +11,7 @@ export enum TabKind { EDIT_RESOURCE = "edit-resource", INSTALL_CHART = "install-chart", UPGRADE_CHART = "upgrade-chart", + POD_LOGS = "pod-logs", } export interface IDockTab { diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index c020d20672..f4936417e6 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -22,6 +22,8 @@ import { createResourceTab, isCreateResourceTab } from "./create-resource.store" import { isEditResourceTab } from "./edit-resource.store"; import { isInstallChartTab } from "./install-chart.store"; import { isUpgradeChartTab } from "./upgrade-chart.store"; +import { PodLogs } from "./pod-logs"; +import { isPodLogsTab } from "./pod-logs.store"; interface Props { className?: string; @@ -59,6 +61,9 @@ export class Dock extends React.Component { if (isInstallChartTab(tab) || isUpgradeChartTab(tab)) { return } /> } + if (isPodLogsTab(tab)) { + return + } } renderTabContent() { @@ -71,6 +76,7 @@ export class Dock extends React.Component { {isInstallChartTab(tab) && } {isUpgradeChartTab(tab) && } {isTerminalTab(tab) && } + {isPodLogsTab(tab) && }
) } diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 122fea6d9f..777aa01027 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -13,7 +13,7 @@ import { Notifications } from "../notifications"; interface Props extends OptionalProps { tabId: TabId; - submit: () => Promise; + submit?: () => Promise; } interface OptionalProps { @@ -23,6 +23,7 @@ interface OptionalProps { submitLabel?: ReactNode; submittingMessage?: ReactNode; disableSubmit?: boolean; + showButtons?: boolean showSubmitClose?: boolean; showInlineInfo?: boolean; showNotifications?: boolean; @@ -33,6 +34,7 @@ export class InfoPanel extends Component { static defaultProps: OptionalProps = { submitLabel: Submit, submittingMessage: Submitting.., + showButtons: true, showSubmitClose: true, showInlineInfo: true, showNotifications: true, @@ -87,7 +89,7 @@ export class InfoPanel extends Component { } render() { - const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showSubmitClose } = this.props; + const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props; const { submit, close, submitAndClose, waiting } = this; const isDisabled = !!(disableSubmit || waiting || error); return ( @@ -98,22 +100,26 @@ export class InfoPanel extends Component {
{waiting ? <> {submittingMessage} : this.renderErrorIcon()}
-
); diff --git a/src/renderer/components/dock/pod-logs.scss b/src/renderer/components/dock/pod-logs.scss new file mode 100644 index 0000000000..73c6f523fb --- /dev/null +++ b/src/renderer/components/dock/pod-logs.scss @@ -0,0 +1,43 @@ +.PodLogs { + .logs { + @include custom-scrollbar; + + // fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight` + // `overflow: overlay` don't allow scroll to the last line + overflow: auto; + + color: $textColorAccent; + background: $logsBackground; + line-height: var(--log-line-height); + border-radius: 2px; + padding: $padding * 2; + font-family: $font-monospace; + font-size: smaller; + white-space: pre; + flex-grow: 1; + + > div { + // Provides font better readability on large screens + -webkit-font-smoothing: subpixel-antialiased; + } + } + + .new-logs-sep { + position: relative; + display: block; + height: 0; + border-top: 1px solid $primary; + margin: $margin * 2; + + &:after { + position: absolute; + left: 50%; + transform: translate(-50%, -50%); + content: 'new'; + background: $primary; + color: white; + padding: $padding / 3; + border-radius: $radius; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/pod-logs.store.ts new file mode 100644 index 0000000000..bf2bf9eb5c --- /dev/null +++ b/src/renderer/components/dock/pod-logs.store.ts @@ -0,0 +1,126 @@ +import { autorun, observable } from "mobx"; +import { Pod, IPodContainer, podsApi } from "../../api/endpoints"; +import { autobind, interval } from "../../utils"; +import { DockTabStore } from "./dock-tab.store"; +import { dockStore, IDockTab, TabKind } from "./dock.store"; +import { t } from "@lingui/macro"; +import { _i18n } from "../../i18n"; + +export interface IPodLogsData { + pod: Pod; + selectedContainer: IPodContainer + containers: IPodContainer[] + initContainers: IPodContainer[] + showTimestamps: boolean + tailLines: number +} + +type TabId = string; + +interface PodLogs { + oldLogs?: string + newLogs?: string +} + +@autobind() +export class PodLogsStore extends DockTabStore { + private refresher = interval(10, () => this.load(dockStore.selectedTabId)); + + @observable logs = observable.map(); + + constructor() { + super({ + storageName: "pod_logs" + }); + autorun(() => { + const { selectedTab, isOpen } = dockStore; + if (isPodLogsTab(selectedTab) && isOpen) { + this.refresher.start(); + } else { + this.refresher.stop(); + } + }, { delay: 500 }); + } + + load = async (tabId: TabId) => { + if (!this.logs.has(tabId)) { + this.logs.set(tabId, { oldLogs: "", newLogs: "" }) + } + const data = this.getData(tabId); + const { oldLogs, newLogs } = this.logs.get(tabId); + const { selectedContainer, tailLines } = data; + const pod = new Pod(data.pod); + try { + // if logs already loaded, check the latest timestamp for getting updates only from this point + const logsTimestamps = this.getTimestamps(newLogs || oldLogs); + let lastLogDate = new Date(0); + if (logsTimestamps) { + lastLogDate = new Date(logsTimestamps.slice(-1)[0]); + lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second + } + const namespace = pod.getNs(); + const name = pod.getName(); + const loadedLogs = await podsApi.getLogs({ namespace, name }, { + sinceTime: lastLogDate.toISOString(), + timestamps: true, // Always setting timestampt to separate old logs from new ones + container: selectedContainer.name, + tailLines: tailLines, + }); + if (!oldLogs) { + this.logs.set(tabId, { oldLogs: loadedLogs, newLogs }); + } else { + this.logs.set(tabId, { oldLogs, newLogs: loadedLogs }); + } + } catch (error) { + this.logs.set(tabId, { + oldLogs: [ + _i18n._(t`Failed to load logs: ${error.message}`), + _i18n._(t`Reason: ${error.reason} (${error.code})`) + ].join("\n"), + newLogs + }); + } + } + + getTimestamps(logs: string) { + return logs.match(/^\d+\S+/gm); + } + + removeTimestamps(logs: string) { + return logs.replace(/^\d+.*?\s/gm, ""); + } + + clearLogs(tabId: TabId) { + this.logs.delete(tabId); + } + + clearData(tabId: TabId) { + this.data.delete(tabId); + this.clearLogs(tabId); + } +} + +export const podLogsStore = new PodLogsStore(); + +export function createPodLogsTab(data: IPodLogsData, tabParams: Partial = {}) { + const podId = data.pod.getId(); + let tab = dockStore.getTabById(podId); + if (tab) { + dockStore.open(); + dockStore.selectTab(tab.id); + return; + } + // If no existent tab found + tab = dockStore.createTab({ + id: podId, + kind: TabKind.POD_LOGS, + title: `Logs: ${data.pod.getName()}`, + ...tabParams + }, false); + podLogsStore.setData(tab.id, data); + return tab; +} + +export function isPodLogsTab(tab: IDockTab) { + return tab && tab.kind === TabKind.POD_LOGS; +} diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx new file mode 100644 index 0000000000..6f97163bde --- /dev/null +++ b/src/renderer/components/dock/pod-logs.tsx @@ -0,0 +1,235 @@ +import "./pod-logs.scss"; +import React from "react"; +import AnsiUp from "ansi_up"; +import DOMPurify from "dompurify"; +import { t, Trans } from "@lingui/macro"; +import { computed, observable, reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; +import { _i18n } from "../../i18n"; +import { autobind, cssNames, downloadFile } from "../../utils"; +import { Icon } from "../icon"; +import { Select, SelectOption } from "../select"; +import { Spinner } from "../spinner"; +import { IDockTab } from "./dock.store"; +import { InfoPanel } from "./info-panel"; +import { IPodLogsData, podLogsStore } from "./pod-logs.store"; + +interface Props { + className?: string + tab: IDockTab +} + +@observer +export class PodLogs extends React.Component { + @observable ready = false; + + private logsElement: HTMLDivElement; + private lastLineIsShown = true; // used for proper auto-scroll content after refresh + private colorConverter = new AnsiUp(); + private lineOptions = [ + { label: _i18n._(t`All logs`), value: Number.MAX_SAFE_INTEGER }, + { label: 1000, value: 1000 }, + { label: 10000, value: 10000 }, + { label: 100000, value: 100000 }, + ]; + + componentDidMount() { + disposeOnUnmount(this, + reaction(() => this.props.tab.id, async () => { + if (podLogsStore.logs.has(this.tabId)) { + this.ready = true; + return; + } + await this.load(); + }, { fireImmediately: true }) + ); + } + + componentDidUpdate() { + // scroll logs only when it's already in the end, + // otherwise it can interrupt reading by jumping after loading new logs update + if (this.logsElement && this.lastLineIsShown) { + this.logsElement.scrollTop = this.logsElement.scrollHeight; + } + } + + get tabData() { + return podLogsStore.getData(this.tabId); + } + + get tabId() { + return this.props.tab.id; + } + + @autobind() + save(data: Partial) { + podLogsStore.setData(this.tabId, { ...this.tabData, ...data }); + } + + load = async () => { + this.ready = false; + await podLogsStore.load(this.tabId); + this.ready = true; + } + + reload = async () => { + podLogsStore.clearLogs(this.tabId); + this.lastLineIsShown = true; + await this.load(); + } + + @computed + get logs() { + if (!podLogsStore.logs.has(this.tabId)) return; + const { oldLogs, newLogs } = podLogsStore.logs.get(this.tabId); + const { getData, removeTimestamps } = podLogsStore; + const { showTimestamps } = getData(this.tabId); + return { + oldLogs: showTimestamps ? oldLogs : removeTimestamps(oldLogs), + newLogs: showTimestamps ? newLogs : removeTimestamps(newLogs) + } + } + + toggleTimestamps = () => { + this.save({ showTimestamps: !this.tabData.showTimestamps }); + } + + onScroll = (evt: React.UIEvent) => { + const logsArea = evt.currentTarget; + const { scrollHeight, clientHeight, scrollTop } = logsArea; + this.lastLineIsShown = clientHeight + scrollTop === scrollHeight; + }; + + downloadLogs = () => { + const { oldLogs, newLogs } = this.logs; + const { pod, selectedContainer } = this.tabData; + const fileName = selectedContainer ? selectedContainer.name : pod.getName(); + const fileContents = oldLogs + newLogs; + downloadFile(fileName + ".log", fileContents, "text/plain"); + } + + onContainerChange = (option: SelectOption) => { + const { containers, initContainers } = this.tabData; + this.save({ + selectedContainer: containers + .concat(initContainers) + .find(container => container.name === option.value) + }) + this.reload(); + } + + onTailLineChange = (option: SelectOption) => { + this.save({ tailLines: option.value }) + this.reload(); + } + + get containerSelectOptions() { + const { containers, initContainers } = this.tabData; + return [ + { + label: _i18n._(t`Containers`), + options: containers.map(container => { + return { value: container.name } + }), + }, + { + label: _i18n._(t`Init Containers`), + options: initContainers.map(container => { + return { value: container.name } + }), + } + ]; + } + + formatOptionLabel = (option: SelectOption) => { + const { value, label } = option; + return label || <> {value}; + } + + renderControls() { + if (!this.ready) return null; + const { selectedContainer, showTimestamps, tailLines } = this.tabData; + const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs); + return ( +
+ Container + +
+ {timestamps && ( + <> + Since{" "} + {new Date(timestamps[0]).toLocaleString()} + + )} +
+
+ + +
+
+ ); + } + + renderLogs() { + if (!this.ready) { + return ; + } + const { oldLogs, newLogs } = this.logs; + if (!oldLogs && !newLogs) { + return ( +
+ There are no logs available for container. +
+ ); + } + return ( + <> +
+ {newLogs && ( + <> +

+

+ + )} + + ); + } + + render() { + const { className } = this.props; + return ( +
+ +
this.logsElement = e}> + {this.renderLogs()} +
+
+ ); + } +} diff --git a/src/renderer/themes/kontena-dark.json b/src/renderer/themes/kontena-dark.json index 1071024841..25defd41bf 100644 --- a/src/renderer/themes/kontena-dark.json +++ b/src/renderer/themes/kontena-dark.json @@ -60,6 +60,7 @@ "dockHeadBackground": "#2e3136", "dockInfoBackground": "#1e2125", "dockInfoBorderColor": "#303136", + "logsBackground": "#000000", "terminalBackground": "#000000", "terminalForeground": "#ffffff", "terminalCursor": "#ffffff", diff --git a/src/renderer/themes/kontena-light.json b/src/renderer/themes/kontena-light.json index f39fa53d00..a1eca8d51c 100644 --- a/src/renderer/themes/kontena-light.json +++ b/src/renderer/themes/kontena-light.json @@ -61,6 +61,7 @@ "dockHeadBackground": "#e8e8e8", "dockInfoBackground": "#e8e8e8", "dockInfoBorderColor": "#c9cfd3", + "logsBackground": "#ffffff", "terminalBackground": "#ffffff", "terminalForeground": "#2d2d2d", "terminalCursor": "#2d2d2d", diff --git a/src/renderer/themes/theme-vars.scss b/src/renderer/themes/theme-vars.scss index c6c3072b8f..6a48721fcc 100644 --- a/src/renderer/themes/theme-vars.scss +++ b/src/renderer/themes/theme-vars.scss @@ -91,6 +91,9 @@ $terminalBrightMagenta: var(--terminalBrightMagenta); $terminalBrightCyan: var(--terminalBrightCyan); $terminalBrightWhite: var(--terminalBrightWhite); +// Logs +$logsBackground: var(--logsBackground); + // Dialogs $dialogTextColor: var(--dialogTextColor); $dialogBackground: var(--dialogBackground); From dc07d646ec5ddbd07323b77cb2176d7361bb8024 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 12 Oct 2020 10:08:57 +0300 Subject: [PATCH 06/15] Align NamespaceSelect and SearchField (#1073) Signed-off-by: Alex Andreev --- src/renderer/components/input/search-input.scss | 6 +++++- .../components/item-object-list/item-list-layout.scss | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/input/search-input.scss b/src/renderer/components/input/search-input.scss index 2f0570da86..a0c706bf3f 100644 --- a/src/renderer/components/input/search-input.scss +++ b/src/renderer/components/input/search-input.scss @@ -7,7 +7,11 @@ > label { color: inherit; border-radius: $radius; - padding: $padding / 1.33 $padding * 1.25; + padding: 6px 6px 6px 10px; + + .Icon { + height: $margin * 2; + } } &.compact { diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss index 6b1623c395..f52afa56e0 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -15,6 +15,14 @@ white-space: nowrap; } + .NamespaceSelect { + .Select { + &__value-container { + margin-bottom: 0; + } + } + } + .SearchInput { label { background: none; From daa4992f2bf0e25052273bec758c190995e639f0 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Mon, 12 Oct 2020 10:10:30 +0300 Subject: [PATCH 07/15] Start authentication proxy when opening cluster settings (#1063) Signed-off-by: Lauri Nevala --- src/common/cluster-ipc.ts | 5 ++--- src/main/cluster.ts | 12 ++++++++---- .../+cluster-settings/cluster-settings.tsx | 3 ++- .../components/cluster-manager/cluster-status.tsx | 7 ++++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index 6ab46c1489..0ff5f41734 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -5,11 +5,10 @@ import { tracker } from "./tracker"; export const clusterIpc = { activate: createIpcChannel({ channel: "cluster:activate", - handle: (clusterId: ClusterId, frameId?: number) => { + handle: (clusterId: ClusterId, force = false) => { const cluster = clusterStore.getById(clusterId); if (cluster) { - if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates - return cluster.activate(); + return cluster.activate(force); } }, }), diff --git a/src/main/cluster.ts b/src/main/cluster.ts index f9b3c53ddc..3b9e89ef86 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -46,6 +46,7 @@ export class Cluster implements ClusterModel { public contextHandler: ContextHandler; protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; + protected activated = false; whenInitialized = when(() => this.initialized); whenReady = when(() => this.ready); @@ -126,7 +127,10 @@ export class Cluster implements ClusterModel { } @action - async activate() { + async activate(force = false ) { + if (this.activated && !force) { + return this.pushState(); + } logger.info(`[CLUSTER]: activate`, this.getMeta()); await this.whenInitialized; if (!this.eventDisposers.length) { @@ -142,6 +146,7 @@ export class Cluster implements ClusterModel { this.kubeCtl = new Kubectl(this.version) this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard } + this.activated = true return this.pushState(); } @@ -162,6 +167,7 @@ export class Cluster implements ClusterModel { this.online = false; this.accessible = false; this.ready = false; + this.activated = false; this.pushState(); } @@ -184,9 +190,7 @@ export class Cluster implements ClusterModel { this.refreshEvents(), this.refreshAllowedResources(), ]); - if (!this.ready) { - this.ready = true - } + this.ready = true } this.pushState(); } diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index b284ed9c88..9558b26967 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -46,8 +46,9 @@ export class ClusterSettings extends React.Component { } } - refreshCluster = () => { + refreshCluster = async () => { if(this.cluster) { + await clusterIpc.activate.invokeFromRenderer(this.cluster.id); clusterIpc.refresh.invokeFromRenderer(this.cluster.id); } } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 846b26a504..5a488c4405 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -47,13 +47,14 @@ export class ClusterStatus extends React.Component { ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`); } - activateCluster = async () => { - await clusterIpc.activate.invokeFromRenderer(this.props.clusterId); + activateCluster = async (force = false) => { + await clusterIpc.activate.invokeFromRenderer(this.props.clusterId, force); } reconnect = async () => { + this.authOutput = [] this.isReconnecting = true; - await this.activateCluster(); + await this.activateCluster(true); this.isReconnecting = false; } From 4fcac6b0d004d3805863b03526771ce5b13aee0b Mon Sep 17 00:00:00 2001 From: steve richards Date: Mon, 12 Oct 2020 08:20:08 +0100 Subject: [PATCH 08/15] Added additional checks on the command used in the Exec plugin in a kubeconfig (#1013) - Added check to see if the program being referenced in the command field of the exec object in the User construct exists. If it doesn't an error will be raised. If more than 1 context is selected when adding a kubeconfig then valid contexts will be added and any with an error will not be. Signed-off-by: Steve Richards Co-authored-by: Steve Richards --- package.json | 1 + src/common/custom-errors.ts | 12 +++++++ src/common/kube-helpers.ts | 27 ++++++++++++++ .../components/+add-cluster/add-cluster.tsx | 36 +++++++++++++++---- types/command-exists.d.ts | 16 +++++++++ yarn.lock | 5 +++ 6 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/common/custom-errors.ts create mode 100644 types/command-exists.d.ts diff --git a/package.json b/package.json index ea65b9d2f0..43c5c31359 100644 --- a/package.json +++ b/package.json @@ -175,6 +175,7 @@ "@types/tar": "^4.0.3", "array-move": "^3.0.0", "chalk": "^4.1.0", + "command-exists": "1.2.9", "conf": "^7.0.1", "crypto-js": "^4.0.0", "electron-updater": "^4.3.1", diff --git a/src/common/custom-errors.ts b/src/common/custom-errors.ts new file mode 100644 index 0000000000..3c7750487a --- /dev/null +++ b/src/common/custom-errors.ts @@ -0,0 +1,12 @@ +export class ExecValidationNotFoundError extends Error { + constructor(execPath: string, isAbsolute: boolean) { + super(`User Exec command "${execPath}" not found on host.`); + let message = `User Exec command "${execPath}" not found on host.`; + if (!isAbsolute) { + message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`; + } + this.message = message; + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} \ No newline at end of file diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index 18f472243b..8f517ba555 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -4,6 +4,8 @@ import path from "path" import os from "os" import yaml from "js-yaml" import logger from "../main/logger"; +import commandExists from "command-exists"; +import { ExecValidationNotFoundError } from "./custom-errors"; export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config'); @@ -140,3 +142,28 @@ export function getNodeWarningConditions(node: V1Node) { c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" ) } + +/** + * Validates kubeconfig supplied in the add clusters screen. At present this will just validate + * the User struct, specifically the command passed to the exec substructure. + */ +export function validateKubeConfig (config: KubeConfig) { + // we only receive a single context, cluster & user object here so lets validate them as this + // will be called when we add a new cluster to Lens + logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`); + + // Validate the User Object + const user = config.getCurrentUser(); + if (user.exec) { + const execCommand = user.exec["command"]; + // check if the command is absolute or not + const isAbsolute = path.isAbsolute(execCommand); + // validate the exec struct in the user object, start with the command field + logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`); + + if (!commandExists.sync(execCommand)) { + logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`); + throw new ExecValidationNotFoundError(execCommand, isAbsolute); + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index a0414541d8..60d609d4f0 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -13,7 +13,7 @@ import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { Icon } from "../icon"; import { WizardLayout } from "../layout/wizard-layout"; -import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig } from "../../../common/kube-helpers"; +import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { v4 as uuid } from "uuid" @@ -23,6 +23,7 @@ import { clusterViewURL } from "../cluster-manager/cluster-view.route"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Tab, Tabs } from "../tabs"; +import { ExecValidationNotFoundError } from "../../../common/custom-errors"; enum KubeConfigSourceTab { FILE = "file", @@ -118,14 +119,32 @@ export class AddCluster extends React.Component { } addClusters = () => { + const configValidationErrors:string[] = []; + let newClusters: ClusterModel[] = []; + try { if (!this.selectedContexts.length) { this.error = Please select at least one cluster context return; } this.error = "" - this.isWaiting = true - const newClusters: ClusterModel[] = this.selectedContexts.map(context => { + this.isWaiting = true + + newClusters = this.selectedContexts.filter(context => { + try { + const kubeConfig = this.kubeContexts.get(context); + validateKubeConfig(kubeConfig); + return true; + } catch (err) { + this.error = String(err.message) + if (err instanceof ExecValidationNotFoundError ) { + Notifications.error(Error while adding cluster(s): {this.error}); + return false; + } else { + throw new Error(err); + } + } + }).map(context => { const clusterId = uuid(); const kubeConfig = this.kubeContexts.get(context); const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE @@ -141,7 +160,8 @@ export class AddCluster extends React.Component { httpsProxy: this.proxyServer || undefined, }, } - }); + }) + runInAction(() => { clusterStore.addCluster(...newClusters); if (newClusters.length === 1) { @@ -149,9 +169,11 @@ export class AddCluster extends React.Component { clusterStore.setActive(clusterId); navigate(clusterViewURL({ params: { clusterId } })); } else { - Notifications.ok( - Successfully imported {newClusters.length} cluster(s) - ); + if (newClusters.length > 1) { + Notifications.ok( + Successfully imported {newClusters.length} cluster(s) + ); + } } }) this.refreshContexts(); diff --git a/types/command-exists.d.ts b/types/command-exists.d.ts new file mode 100644 index 0000000000..b5375ae390 --- /dev/null +++ b/types/command-exists.d.ts @@ -0,0 +1,16 @@ +// Type definitions for command-exists 1.2 +// Project: https://github.com/mathisonian/command-exists +// Definitions by: BendingBender +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +export = commandExists; + +declare function commandExists(commandName: string): Promise; +declare function commandExists( + commandName: string, + cb: (error: null, exists: boolean) => void +): void; + +declare namespace commandExists { + function sync(commandName: string): boolean; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1c92dd0978..a8dcc87642 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3888,6 +3888,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +command-exists@1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + commander@*, commander@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" From 83b8fa9d98246f175e778e4b95dca949269f8394 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 12 Oct 2020 15:58:18 +0300 Subject: [PATCH 09/15] Ability to show previous terminated container logs (#1074) * Show previous terminated container logs Signed-off-by: Alex Andreev * Fix for parsing error object Signed-off-by: Alex Andreev --- src/renderer/api/endpoints/pods.api.ts | 2 ++ .../components/+workloads-pods/pod-menu.tsx | 1 + src/renderer/components/dock/pod-logs.store.ts | 8 +++++--- src/renderer/components/dock/pod-logs.tsx | 13 ++++++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index e1545f09c7..e1c91f6e36 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -42,12 +42,14 @@ export interface IPodMetrics { networkTransmit: T; } +// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core export interface IPodLogsQuery { container?: string; tailLines?: number; timestamps?: boolean; sinceTime?: string; // Date.toISOString()-format follow?: boolean; + previous?: boolean; } export enum PodStatus { diff --git a/src/renderer/components/+workloads-pods/pod-menu.tsx b/src/renderer/components/+workloads-pods/pod-menu.tsx index 64b5d1f5eb..4ec3bb69d0 100644 --- a/src/renderer/components/+workloads-pods/pod-menu.tsx +++ b/src/renderer/components/+workloads-pods/pod-menu.tsx @@ -50,6 +50,7 @@ export class PodMenu extends React.Component { initContainers: pod.getInitContainers(), selectedContainer: container, showTimestamps: false, + previous: false, tailLines: 1000 }); } diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/pod-logs.store.ts index bf2bf9eb5c..fa1da38c68 100644 --- a/src/renderer/components/dock/pod-logs.store.ts +++ b/src/renderer/components/dock/pod-logs.store.ts @@ -13,6 +13,7 @@ export interface IPodLogsData { initContainers: IPodContainer[] showTimestamps: boolean tailLines: number + previous: boolean } type TabId = string; @@ -48,7 +49,7 @@ export class PodLogsStore extends DockTabStore { } const data = this.getData(tabId); const { oldLogs, newLogs } = this.logs.get(tabId); - const { selectedContainer, tailLines } = data; + const { selectedContainer, tailLines, previous } = data; const pod = new Pod(data.pod); try { // if logs already loaded, check the latest timestamp for getting updates only from this point @@ -64,14 +65,15 @@ export class PodLogsStore extends DockTabStore { sinceTime: lastLogDate.toISOString(), timestamps: true, // Always setting timestampt to separate old logs from new ones container: selectedContainer.name, - tailLines: tailLines, + tailLines, + previous }); if (!oldLogs) { this.logs.set(tabId, { oldLogs: loadedLogs, newLogs }); } else { this.logs.set(tabId, { oldLogs, newLogs: loadedLogs }); } - } catch (error) { + } catch ({error}) { this.logs.set(tabId, { oldLogs: [ _i18n._(t`Failed to load logs: ${error.message}`), diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx index 6f97163bde..06643bc711 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/pod-logs.tsx @@ -94,6 +94,11 @@ export class PodLogs extends React.Component { this.save({ showTimestamps: !this.tabData.showTimestamps }); } + togglePrevious = () => { + this.save({ previous: !this.tabData.previous }); + this.reload(); + } + onScroll = (evt: React.UIEvent) => { const logsArea = evt.currentTarget; const { scrollHeight, clientHeight, scrollTop } = logsArea; @@ -148,7 +153,7 @@ export class PodLogs extends React.Component { renderControls() { if (!this.ready) return null; - const { selectedContainer, showTimestamps, tailLines } = this.tabData; + const { selectedContainer, showTimestamps, tailLines, previous } = this.tabData; const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs); return (
@@ -181,6 +186,12 @@ export class PodLogs extends React.Component { className={cssNames("timestamps-icon", { active: showTimestamps })} tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)} /> + Date: Wed, 14 Oct 2020 14:26:09 +0300 Subject: [PATCH 10/15] Fix integration test teardown (#1082) Signed-off-by: Lauri Nevala --- integration/helpers/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 55395f84e3..92e9ade7af 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -21,10 +21,11 @@ export function setup(): Application { } export async function tearDown(app: Application) { - const pid = app.mainProcess.pid + let mpid: any = app.mainProcess.pid + let pid = await mpid() await app.stop() try { - process.kill(pid, 0); + process.kill(pid, "SIGKILL"); } catch (e) { return } From db9af88d91ee2a9d0299bcfb97d51ad4677a5621 Mon Sep 17 00:00:00 2001 From: steve richards Date: Wed, 14 Oct 2020 12:34:04 +0100 Subject: [PATCH 11/15] Updated regex for isUrl to allow for an empty string (#1049) Updated regex for isUrl to allow for an empty string aswell as a correct URL. Signed-off-by: Steve Richards Co-authored-by: Steve Richards --- src/renderer/components/input/input_validators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/input/input_validators.ts b/src/renderer/components/input/input_validators.ts index 611394134f..b11dbfa1da 100644 --- a/src/renderer/components/input/input_validators.ts +++ b/src/renderer/components/input/input_validators.ts @@ -39,7 +39,7 @@ export const isNumber: Validator = { export const isUrl: Validator = { condition: ({ type }) => type === "url", message: () => _i18n._(t`Wrong url format`), - validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), + validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), }; export const isPath: Validator = { From 78f6f8ef54718c23dc409ba287c5e27891ddb458 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 14 Oct 2020 14:35:33 +0300 Subject: [PATCH 12/15] Fix bundled kubectl path on dev env (#981) * fix bundled kubectl path on dev env Signed-off-by: Jari Kolehmainen * fix specs Signed-off-by: Jari Kolehmainen --- src/main/kube-auth-proxy.ts | 4 +-- src/main/kubectl.ts | 36 ++++++++++--------- src/main/kubectl_spec.ts | 8 ++--- src/main/routes/port-forward-route.ts | 4 +-- src/main/shell-session.ts | 2 +- .../+preferences/kubectl-binaries.tsx | 4 +-- 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 33521fdcf5..7192425466 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -2,7 +2,7 @@ import { ChildProcess, spawn } from "child_process" import { waitUntilUsed } from "tcp-port-used"; import { broadcastIpc } from "../common/ipc"; import type { Cluster } from "./cluster" -import { bundledKubectl, Kubectl } from "./kubectl" +import { Kubectl } from "./kubectl" import logger from "./logger" export interface KubeAuthProxyLog { @@ -23,7 +23,7 @@ export class KubeAuthProxy { this.env = env this.port = port this.cluster = cluster - this.kubectl = bundledKubectl + this.kubectl = Kubectl.bundled() } public async run(): Promise { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 672d93cf65..6b2f51476d 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -36,15 +36,21 @@ const packageMirrors: Map = new Map([ let bundledPath: string const initScriptVersionString = "# lens-initscript v3\n" -if (isDevelopment || isTestEnv) { - const platformName = isWindows ? "windows" : process.platform - bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl") -} else { - bundledPath = path.join(process.resourcesPath, process.arch, "kubectl") -} +export function bundledKubectlPath(): string { + if (bundledPath) { return bundledPath } -if (isWindows) { - bundledPath = `${bundledPath}.exe` + if (isDevelopment || isTestEnv) { + const platformName = isWindows ? "windows" : process.platform + bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl") + } else { + bundledPath = path.join(process.resourcesPath, process.arch, "kubectl") + } + + if (isWindows) { + bundledPath = `${bundledPath}.exe` + } + + return bundledPath } export class Kubectl { @@ -58,7 +64,6 @@ export class Kubectl { return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl") } - public static readonly bundledKubectlPath = bundledPath public static readonly bundledKubectlVersion: string = bundledVersion public static invalidBundle = false private static bundledInstance: Kubectl; @@ -102,7 +107,7 @@ export class Kubectl { } public getBundledPath() { - return Kubectl.bundledKubectlPath + return bundledKubectlPath() } public getPathFromPreferences() { @@ -125,19 +130,19 @@ export class Kubectl { // return binary name if bundled path is not functional if (!await this.checkBinary(this.getBundledPath(), false)) { Kubectl.invalidBundle = true - return path.basename(bundledPath) + return path.basename(this.getBundledPath()) } try { if (!await this.ensureKubectl()) { logger.error("Failed to ensure kubectl, fallback to the bundled version") - return Kubectl.bundledKubectlPath + return this.getBundledPath() } return this.path } catch (err) { logger.error("Failed to ensure kubectl, fallback to the bundled version") logger.error(err) - return Kubectl.bundledKubectlPath + return this.getBundledPath() } } @@ -183,7 +188,7 @@ export class Kubectl { try { const exist = await pathExists(this.path) if (!exist) { - await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path) + await fs.promises.copyFile(this.getBundledPath(), this.path) await fs.promises.chmod(this.path, 0o755) } return true @@ -332,6 +337,3 @@ export class Kubectl { return packageMirrors.get("default") // MacOS packages are only available from default } } - -const bundledKubectl = Kubectl.bundled() -export { bundledKubectl } diff --git a/src/main/kubectl_spec.ts b/src/main/kubectl_spec.ts index 4e5cdbf986..ade999c082 100644 --- a/src/main/kubectl_spec.ts +++ b/src/main/kubectl_spec.ts @@ -1,20 +1,20 @@ import packageInfo from "../../package.json" import path from "path" -import { bundledKubectl, Kubectl } from "../../src/main/kubectl"; +import { Kubectl } from "../../src/main/kubectl"; import { isWindows } from "../common/vars"; jest.mock("../common/user-store"); describe("kubectlVersion", () => { it("returns bundled version if exactly same version used", async () => { - const kubectl = new Kubectl(bundledKubectl.kubectlVersion) - expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) + const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion) + expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion) }) it("returns bundled version if same major.minor version is used", async () => { const { bundledKubectlVersion } = packageInfo.config; const kubectl = new Kubectl(bundledKubectlVersion); - expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) + expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion) }) }) diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 3f1a855902..7ed79aa936 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -1,7 +1,7 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" import { spawn, ChildProcessWithoutNullStreams } from "child_process" -import { bundledKubectl } from "../kubectl" +import { Kubectl } from "../kubectl" import { getFreePort } from "../port" import { shell } from "electron" import * as tcpPortUsed from "tcp-port-used" @@ -37,7 +37,7 @@ class PortForward { public async start() { this.localPort = await getFreePort() - const kubectlBin = await bundledKubectl.getPath() + const kubectlBin = await Kubectl.bundled().getPath() const args = [ "--kubeconfig", this.kubeConfig, "port-forward", diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 962074c803..2367bb63c0 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -38,7 +38,7 @@ export class ShellSession extends EventEmitter { public async open() { this.kubectlBinDir = await this.kubectl.binDir() - const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath + const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath() this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences) this.helmBinDir = helmCli.getBinaryDir() const env = await this.getCachedShellEnv() diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index 06c4f3ec83..002ddfdcc3 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -6,7 +6,7 @@ import { Input } from '../input'; import { SubTitle } from '../layout/sub-title'; import { UserPreferences, userStore } from '../../../common/user-store'; import { observer } from 'mobx-react'; -import { Kubectl } from '../../../main/kubectl'; +import { bundledKubectlPath } from '../../../main/kubectl'; import { SelectOption, Select } from '../select'; export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => { @@ -58,7 +58,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre Date: Wed, 14 Oct 2020 07:36:19 -0400 Subject: [PATCH 13/15] move autobind and debouncePromise to common/utils (#1081) Signed-off-by: Sebastian Malton --- src/{renderer => common}/utils/autobind.ts | 0 src/common/utils/debouncePromise.ts | 9 +++++++ src/common/utils/index.ts | 13 ++++++--- src/renderer/api/api-manager.ts | 2 +- src/renderer/components/dock/terminal.ts | 2 +- src/renderer/theme.store.ts | 2 +- src/renderer/utils/debouncePromise.ts | 9 ------- src/renderer/utils/index.ts | 31 ++++++++++------------ 8 files changed, 36 insertions(+), 32 deletions(-) rename src/{renderer => common}/utils/autobind.ts (100%) create mode 100755 src/common/utils/debouncePromise.ts delete mode 100755 src/renderer/utils/debouncePromise.ts diff --git a/src/renderer/utils/autobind.ts b/src/common/utils/autobind.ts similarity index 100% rename from src/renderer/utils/autobind.ts rename to src/common/utils/autobind.ts diff --git a/src/common/utils/debouncePromise.ts b/src/common/utils/debouncePromise.ts new file mode 100755 index 0000000000..22ffd5217f --- /dev/null +++ b/src/common/utils/debouncePromise.ts @@ -0,0 +1,9 @@ +// Debouncing promise evaluation + +export function debouncePromise(func: (...args: F) => T | Promise, timeout = 0): (...args: F) => Promise { + let timer: NodeJS.Timeout; + return (...params: any[]) => new Promise((resolve, reject) => { + clearTimeout(timer); + timer = setTimeout(() => resolve(func.apply(this, params)), timeout); + }); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 580a8f15c2..db46a37d97 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,7 +1,14 @@ -// Common utils (main/renderer) +// Common utils (main OR renderer) +export * from "./app-version" +export * from "./autobind" export * from "./base64" export * from "./camelCase" -export * from "./splitArray" -export * from "./getRandId" +export * from "./cloneJson" +export * from "./debouncePromise" +export * from "./defineGlobal" +export * from "./getRandId" +export * from "./splitArray" +export * from "./saveToAppFiles" +export * from "./singleton" export * from "./cloneJson" diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 22735c3c09..e4a5432965 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -3,7 +3,7 @@ import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuP import type React from "react"; import { observable } from "mobx"; -import { autobind } from "../utils/autobind"; +import { autobind } from "../utils"; import { KubeApi } from "./kube-api"; export interface ApiComponents { diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal.ts index cea1e91292..e508cc22b2 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal.ts @@ -5,7 +5,7 @@ import { FitAddon } from "xterm-addon-fit"; import { dockStore, TabId } from "./dock.store"; import { TerminalApi } from "../../api/terminal-api"; import { themeStore } from "../../theme.store"; -import { autobind } from "../../utils/autobind"; +import { autobind } from "../../utils"; export class Terminal { static spawningPool: HTMLElement; diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 5245d2a728..539be6651d 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -1,5 +1,5 @@ import { computed, observable, reaction } from "mobx"; -import { autobind } from "./utils/autobind"; +import { autobind } from "./utils"; import { userStore } from "../common/user-store"; import logger from "../main/logger"; diff --git a/src/renderer/utils/debouncePromise.ts b/src/renderer/utils/debouncePromise.ts deleted file mode 100755 index 4ce949a944..0000000000 --- a/src/renderer/utils/debouncePromise.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Debouncing promise evaluation - -export const debouncePromise = function (promisedFunc: Function, timeout = 0) { - let timer: number; - return (...params: any[]) => new Promise((resolve, reject) => { - clearTimeout(timer); - timer = window.setTimeout(() => resolve(promisedFunc.apply(this, params)), timeout); - }); -}; diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 578ec5c355..4ae9b068a7 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -3,21 +3,18 @@ export const noop: any = Function(); export const isElectron = !!navigator.userAgent.match(/Electron/); -export * from '../../common/utils/camelCase' -export * from '../../common/utils/base64' +export * from "../../common/utils" -export * from './autobind' -export * from './cssVar' -export * from './cssNames' -export * from './eventEmitter' -export * from './downloadFile' -export * from './prevDefault' -export * from './createStorage' -export * from './interval' -export * from './debouncePromise' -export * from './copyToClipboard' -export * from './formatDuration' -export * from './isReactNode' -export * from './convertMemory' -export * from './convertCpu' -export * from './metricUnitsToNumber' +export * from "./cssVar" +export * from "./cssNames" +export * from "./eventEmitter" +export * from "./downloadFile" +export * from "./prevDefault" +export * from "./createStorage" +export * from "./interval" +export * from "./copyToClipboard" +export * from "./formatDuration" +export * from "./isReactNode" +export * from "./convertMemory" +export * from "./convertCpu" +export * from "./metricUnitsToNumber" From 81575b333a49c58dc1b92a2cabec83961c5ce580 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 14 Oct 2020 17:10:58 +0300 Subject: [PATCH 14/15] Fix KubeAuthProxy unit tests (#1084) Signed-off-by: Lauri Nevala --- src/main/__test__/kube-auth-proxy.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 48f430b7c5..e7bee67c07 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -32,7 +32,7 @@ import { KubeAuthProxy } from "../kube-auth-proxy" import { getFreePort } from "../port" import { broadcastIpc } from "../../common/ipc" import { ChildProcess, spawn, SpawnOptions } from "child_process" -import { Kubectl } from "../kubectl" +import { bundledKubectlPath, Kubectl } from "../kubectl" import { mock, MockProxy } from 'jest-mock-extended'; import { waitUntilUsed } from 'tcp-port-used'; import { Readable } from "stream" @@ -81,7 +81,7 @@ describe("kube auth proxy tests", () => { return mockedCP.stdout }) mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => { - expect(command).toBe(Kubectl.bundledKubectlPath) + expect(command).toBe(bundledKubectlPath()) return mockedCP }) mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()) From 087ebf6aecf76974136e95faf3b934c181761fcd Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 14 Oct 2020 12:44:16 -0400 Subject: [PATCH 15/15] extract internationalizations (#1086) Signed-off-by: Sebastian Malton --- locales/en/messages.po | 116 +++++++++++++++++++++++------------------ locales/fi/messages.po | 116 +++++++++++++++++++++++------------------ locales/ru/messages.po | 116 +++++++++++++++++++++++------------------ 3 files changed, 192 insertions(+), 156 deletions(-) diff --git a/locales/en/messages.po b/locales/en/messages.po index 86f99a841f..2b3980ed5c 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -87,7 +87,7 @@ msgstr "Account Name" msgid "Active" msgstr "Active" -#: src/renderer/components/+add-cluster/add-cluster.tsx:289 +#: src/renderer/components/+add-cluster/add-cluster.tsx:310 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130 msgid "Add Cluster" msgstr "Add Cluster" @@ -112,7 +112,7 @@ msgstr "Add bindings to {name}" #~ msgid "Add cluster" #~ msgstr "Add cluster" -#: src/renderer/components/+add-cluster/add-cluster.tsx:306 +#: src/renderer/components/+add-cluster/add-cluster.tsx:327 msgid "Add cluster(s)" msgstr "Add cluster(s)" @@ -203,7 +203,7 @@ msgstr "All clusters within workspace will be cleared as well" msgid "All groups" msgstr "All groups" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57 +#: src/renderer/components/dock/pod-logs.tsx:37 msgid "All logs" msgstr "All logs" @@ -323,7 +323,7 @@ msgstr "Binding targets" msgid "Bindings" msgstr "Bindings" -#: src/renderer/components/+add-cluster/add-cluster.tsx:236 +#: src/renderer/components/+add-cluster/add-cluster.tsx:257 msgid "Browse" msgstr "Browse" @@ -404,7 +404,7 @@ msgstr "CPU:" #: src/renderer/components/+workspaces/workspaces.tsx:133 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 -#: src/renderer/components/dock/info-panel.tsx:85 +#: src/renderer/components/dock/info-panel.tsx:86 #: src/renderer/components/wizard/wizard.tsx:130 msgid "Cancel" msgstr "Cancel" @@ -473,7 +473,6 @@ msgstr "Claim" msgid "Claim Name" msgstr "Claim Name" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243 #: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93 msgid "Close" @@ -566,7 +565,7 @@ msgstr "Configuration" msgid "Connection" msgstr "Connection" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246 +#: src/renderer/components/dock/pod-logs.tsx:148 msgid "Container" msgstr "Container" @@ -595,8 +594,8 @@ msgid "Container runtime" msgstr "Container runtime" #: src/renderer/components/+workloads-pods/pod-details.tsx:122 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186 #: src/renderer/components/+workloads-pods/pods.tsx:77 +#: src/renderer/components/dock/pod-logs.tsx:129 msgid "Containers" msgstr "Containers" @@ -691,7 +690,7 @@ msgstr "Create new Secret" msgid "Create new Service Account" msgstr "Create new Service Account" -#: src/renderer/components/dock/dock.tsx:93 +#: src/renderer/components/dock/dock.tsx:99 msgid "Create resource" msgstr "Create resource" @@ -928,7 +927,8 @@ msgstr "Environment" msgid "Error stack" msgstr "Error stack" -#: src/renderer/components/+add-cluster/add-cluster.tsx:109 +#: src/renderer/components/+add-cluster/add-cluster.tsx:89 +#: src/renderer/components/+add-cluster/add-cluster.tsx:130 msgid "Error while adding cluster(s): {0}" msgstr "Error while adding cluster(s): {0}" @@ -948,7 +948,7 @@ msgstr "Everything is fine in the Cluster" #~ msgid "Excluded items with \"system:\" prefix" #~ msgstr "Excluded items with \"system:\" prefix" -#: src/renderer/components/dock/dock.tsx:98 +#: src/renderer/components/dock/dock.tsx:104 msgid "Exit full size mode" msgstr "Exit full size mode" @@ -964,7 +964,7 @@ msgstr "External IP" msgid "External IPs" msgstr "External IPs" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106 +#: src/renderer/components/dock/pod-logs.store.ts:65 msgid "Failed to load logs: {0}" msgstr "Failed to load logs: {0}" @@ -989,7 +989,7 @@ msgstr "Finalizers" msgid "First seen" msgstr "First seen" -#: src/renderer/components/dock/dock.tsx:98 +#: src/renderer/components/dock/dock.tsx:104 msgid "Fit to window" msgstr "Fit to window" @@ -1006,8 +1006,8 @@ msgid "From" msgstr "From" #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212 -msgid "From <0>{from} to <1>{to}" -msgstr "From <0>{from} to <1>{to}" +#~ msgid "From <0>{from} to <1>{to}" +#~ msgstr "From <0>{from} to <1>{to}" #: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125 msgid "Fs Group" @@ -1068,7 +1068,7 @@ msgid "Helm branch <0>{0} already in use" msgstr "Helm branch <0>{0} already in use" #: src/renderer/components/+config-secrets/secret-details.tsx:93 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19 msgid "Hide" msgstr "Hide" @@ -1153,7 +1153,7 @@ msgid "Ingresses" msgstr "Ingresses" #: src/renderer/components/+workloads-pods/pod-details.tsx:118 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192 +#: src/renderer/components/dock/pod-logs.tsx:135 msgid "Init Containers" msgstr "Init Containers" @@ -1314,7 +1314,7 @@ msgstr "Limited to {0}" msgid "Limits" msgstr "Limits" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248 +#: src/renderer/components/dock/pod-logs.tsx:150 msgid "Lines" msgstr "Lines" @@ -1338,8 +1338,8 @@ msgstr "Load-Balancer Ingress Points" msgid "Loading" msgstr "Loading" -#: src/renderer/components/+workloads-pods/pod-menu.tsx:90 -#: src/renderer/components/+workloads-pods/pod-menu.tsx:91 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:100 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:101 msgid "Logs" msgstr "Logs" @@ -1445,7 +1445,7 @@ msgstr "Min Available" msgid "Min Pods" msgstr "Min Pods" -#: src/renderer/components/dock/dock.tsx:99 +#: src/renderer/components/dock/dock.tsx:105 msgid "Minimize" msgstr "Minimize" @@ -1600,11 +1600,11 @@ msgstr "Network File System" msgid "Network Policies" msgstr "Network Policies" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231 +#: src/renderer/components/dock/pod-logs.tsx:178 msgid "New logs since opening the dialog" msgstr "New logs since opening the dialog" -#: src/renderer/components/dock/dock.tsx:86 +#: src/renderer/components/dock/dock.tsx:92 msgid "New tab" msgstr "New tab" @@ -1642,7 +1642,7 @@ msgstr "No Nodes Available." #~ msgid "No contexts available or they already added" #~ msgstr "No contexts available or they already added" -#: src/renderer/components/+add-cluster/add-cluster.tsx:260 +#: src/renderer/components/+add-cluster/add-cluster.tsx:281 msgid "No contexts available or they have been added already" msgstr "No contexts available or they have been added already" @@ -1738,7 +1738,7 @@ msgstr "Ok" msgid "Ok, got it!" msgstr "Ok, got it!" -#: src/renderer/components/dock/dock.tsx:99 +#: src/renderer/components/dock/dock.tsx:105 msgid "Open" msgstr "Open" @@ -1774,7 +1774,7 @@ msgstr "Parallelism" msgid "Parameters" msgstr "Parameters" -#: src/renderer/components/+add-cluster/add-cluster.tsx:230 +#: src/renderer/components/+add-cluster/add-cluster.tsx:251 msgid "Paste as text" msgstr "Paste as text" @@ -1798,7 +1798,7 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" -#: src/renderer/components/+add-cluster/add-cluster.tsx:72 +#: src/renderer/components/+add-cluster/add-cluster.tsx:75 msgid "Please select at least one cluster context" msgstr "Please select at least one cluster context" @@ -1851,7 +1851,7 @@ msgstr "Pod Selector" msgid "Pod Status" msgstr "Pod Status" -#: src/renderer/components/+workloads-pods/pod-menu.tsx:67 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:77 msgid "Pod shell" msgstr "Pod shell" @@ -1914,7 +1914,7 @@ msgstr "Privileged" #~ msgid "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgstr "Pro-Tip: paste kubeconfig to collect available contexts" -#: src/renderer/components/+add-cluster/add-cluster.tsx:248 +#: src/renderer/components/+add-cluster/add-cluster.tsx:269 msgid "Pro-Tip: paste kubeconfig to get available contexts" msgstr "Pro-Tip: paste kubeconfig to get available contexts" @@ -1922,7 +1922,7 @@ msgstr "Pro-Tip: paste kubeconfig to get available contexts" #~ msgid "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgstr "Pro-Tip: paste kubeconfig to parse available contexts" -#: src/renderer/components/+add-cluster/add-cluster.tsx:239 +#: src/renderer/components/+add-cluster/add-cluster.tsx:260 msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" msgstr "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" @@ -1943,7 +1943,7 @@ msgstr "Provisioner" msgid "Proxy is used only for non-cluster communication." msgstr "Proxy is used only for non-cluster communication." -#: src/renderer/components/+add-cluster/add-cluster.tsx:294 +#: src/renderer/components/+add-cluster/add-cluster.tsx:315 msgid "Proxy settings" msgstr "Proxy settings" @@ -1979,7 +1979,7 @@ msgstr "Readiness" msgid "Reason" msgstr "Reason" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107 +#: src/renderer/components/dock/pod-logs.store.ts:66 msgid "Reason: {0} ({1})" msgstr "Reason: {0} ({1})" @@ -2124,7 +2124,7 @@ msgstr "Required Drop Capabilities" msgid "Required field" msgstr "Required field" -#: src/renderer/components/+add-cluster/add-cluster.tsx:235 +#: src/renderer/components/+add-cluster/add-cluster.tsx:256 #: src/renderer/components/item-object-list/page-filters-list.tsx:31 msgid "Reset" msgstr "Reset" @@ -2266,9 +2266,9 @@ msgstr "Runtime Class" #: src/renderer/components/+apps-releases/release-details.tsx:114 #: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-secrets/secret-details.tsx:97 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216 #: src/renderer/components/+workspaces/workspaces.tsx:132 #: src/renderer/components/dock/edit-resource.tsx:87 +#: src/renderer/components/dock/pod-logs.tsx:161 msgid "Save" msgstr "Save" @@ -2360,7 +2360,7 @@ msgstr "Select a quota.." #~ msgid "Select context(s)" #~ msgstr "Select context(s)" -#: src/renderer/components/+add-cluster/add-cluster.tsx:257 +#: src/renderer/components/+add-cluster/add-cluster.tsx:278 msgid "Select contexts" msgstr "Select contexts" @@ -2373,8 +2373,8 @@ msgstr "Select contexts" #~ msgid "Select custom kube-config file" #~ msgstr "Select custom kube-config file" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:63 +#: src/renderer/components/+add-cluster/add-cluster.tsx:63 msgid "Select custom kubeconfig file" msgstr "Select custom kubeconfig file" @@ -2390,7 +2390,7 @@ msgstr "Select custom kubeconfig file" #~ msgid "Select kubeconfig" #~ msgstr "Select kubeconfig" -#: src/renderer/components/+add-cluster/add-cluster.tsx:229 +#: src/renderer/components/+add-cluster/add-cluster.tsx:250 msgid "Select kubeconfig file" msgstr "Select kubeconfig file" @@ -2418,7 +2418,7 @@ msgstr "Select service accounts" #~ msgid "Selected contexts ({0}): <0>{1}" #~ msgstr "Selected contexts ({0}): <0>{1}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:256 +#: src/renderer/components/+add-cluster/add-cluster.tsx:277 msgid "Selected contexts: <0>{0}" msgstr "Selected contexts: <0>{0}" @@ -2475,13 +2475,13 @@ msgid "Settings" msgstr "Settings" #: src/renderer/components/+nodes/node-menu.tsx:48 -#: src/renderer/components/+workloads-pods/pod-menu.tsx:68 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:78 msgid "Shell" msgstr "Shell" #: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+workloads-pods/pod-container-env.tsx:101 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19 msgid "Show" msgstr "Show" @@ -2490,10 +2490,22 @@ msgstr "Show" msgid "Show Notes" msgstr "Show Notes" +#: src/renderer/components/dock/pod-logs.tsx:160 +msgid "Show current logs" +msgstr "Show current logs" + +#: src/renderer/components/dock/pod-logs.tsx:160 +msgid "Show previous terminated container logs" +msgstr "Show previous terminated container logs" + #: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20 msgid "Show value" msgstr "Show value" +#: src/renderer/components/dock/pod-logs.tsx:154 +msgid "Since" +msgstr "Since" + #: src/renderer/components/+nodes/node-charts.tsx:80 #: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49 msgid "Size" @@ -2588,12 +2600,12 @@ msgstr "Strategy Type" msgid "Sub-object" msgstr "Sub-object" -#: src/renderer/components/dock/info-panel.tsx:93 +#: src/renderer/components/dock/info-panel.tsx:95 #: src/renderer/components/wizard/wizard.tsx:131 msgid "Submit" msgstr "Submit" -#: src/renderer/components/dock/info-panel.tsx:94 +#: src/renderer/components/dock/info-panel.tsx:96 msgid "Submitting.." msgstr "Submitting.." @@ -2601,7 +2613,7 @@ msgstr "Submitting.." msgid "Subsets" msgstr "Subsets" -#: src/renderer/components/+add-cluster/add-cluster.tsx:102 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Successfully imported <0>{0} cluster(s)" msgstr "Successfully imported <0>{0} cluster(s)" @@ -2635,7 +2647,7 @@ msgstr "Telemetry & usage data is collected to continuously improve the Lens exp msgid "Terminal" msgstr "Terminal" -#: src/renderer/components/dock/dock.tsx:89 +#: src/renderer/components/dock/dock.tsx:95 msgid "Terminal session" msgstr "Terminal session" @@ -2643,7 +2655,7 @@ msgstr "Terminal session" msgid "The path to the kubectl binary on the system." msgstr "The path to the kubectl binary on the system." -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226 +#: src/renderer/components/dock/pod-logs.tsx:172 msgid "There are no logs available for container." msgstr "There are no logs available for container." @@ -2773,8 +2785,8 @@ msgstr "Upgrade version" msgid "Usage" msgstr "Usage" -#: src/renderer/components/+add-cluster/add-cluster.tsx:63 -#: src/renderer/components/+add-cluster/add-cluster.tsx:63 +#: src/renderer/components/+add-cluster/add-cluster.tsx:64 +#: src/renderer/components/+add-cluster/add-cluster.tsx:64 msgid "Use configuration" msgstr "Use configuration" @@ -2961,7 +2973,7 @@ msgstr "sec" msgid "singular" msgstr "singular" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 msgid "timestamps" msgstr "timestamps" @@ -3006,8 +3018,8 @@ msgid "{metricsRemainCount} more..." msgstr "{metricsRemainCount} more..." #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240 -msgid "{podName} Logs" -msgstr "{podName} Logs" +#~ msgid "{podName} Logs" +#~ msgstr "{podName} Logs" #: src/renderer/components/dock/edit-resource.tsx:56 msgid "{resourceType} <0>{resourceName} updated." @@ -3017,6 +3029,6 @@ msgstr "{resourceType} <0>{resourceName} updated." msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" msgstr "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" -#: src/renderer/components/dock/info-panel.tsx:88 +#: src/renderer/components/dock/info-panel.tsx:89 msgid "{submitLabel} & Close" msgstr "{submitLabel} & Close" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 1b53f1bffa..613e68b6fe 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -87,7 +87,7 @@ msgstr "" msgid "Active" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:289 +#: src/renderer/components/+add-cluster/add-cluster.tsx:310 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130 msgid "Add Cluster" msgstr "" @@ -112,7 +112,7 @@ msgstr "" #~ msgid "Add cluster" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:306 +#: src/renderer/components/+add-cluster/add-cluster.tsx:327 msgid "Add cluster(s)" msgstr "" @@ -203,7 +203,7 @@ msgstr "" msgid "All groups" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57 +#: src/renderer/components/dock/pod-logs.tsx:37 msgid "All logs" msgstr "" @@ -323,7 +323,7 @@ msgstr "" msgid "Bindings" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:236 +#: src/renderer/components/+add-cluster/add-cluster.tsx:257 msgid "Browse" msgstr "" @@ -404,7 +404,7 @@ msgstr "" #: src/renderer/components/+workspaces/workspaces.tsx:133 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 -#: src/renderer/components/dock/info-panel.tsx:85 +#: src/renderer/components/dock/info-panel.tsx:86 #: src/renderer/components/wizard/wizard.tsx:130 msgid "Cancel" msgstr "" @@ -469,7 +469,6 @@ msgstr "" msgid "Claim Name" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243 #: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93 msgid "Close" @@ -562,7 +561,7 @@ msgstr "" msgid "Connection" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246 +#: src/renderer/components/dock/pod-logs.tsx:148 msgid "Container" msgstr "" @@ -591,8 +590,8 @@ msgid "Container runtime" msgstr "" #: src/renderer/components/+workloads-pods/pod-details.tsx:122 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186 #: src/renderer/components/+workloads-pods/pods.tsx:77 +#: src/renderer/components/dock/pod-logs.tsx:129 msgid "Containers" msgstr "" @@ -687,7 +686,7 @@ msgstr "" msgid "Create new Service Account" msgstr "" -#: src/renderer/components/dock/dock.tsx:93 +#: src/renderer/components/dock/dock.tsx:99 msgid "Create resource" msgstr "" @@ -924,7 +923,8 @@ msgstr "" msgid "Error stack" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:109 +#: src/renderer/components/+add-cluster/add-cluster.tsx:89 +#: src/renderer/components/+add-cluster/add-cluster.tsx:130 msgid "Error while adding cluster(s): {0}" msgstr "" @@ -939,7 +939,7 @@ msgstr "" msgid "Everything is fine in the Cluster" msgstr "" -#: src/renderer/components/dock/dock.tsx:98 +#: src/renderer/components/dock/dock.tsx:104 msgid "Exit full size mode" msgstr "" @@ -955,7 +955,7 @@ msgstr "" msgid "External IPs" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106 +#: src/renderer/components/dock/pod-logs.store.ts:65 msgid "Failed to load logs: {0}" msgstr "" @@ -980,7 +980,7 @@ msgstr "" msgid "First seen" msgstr "" -#: src/renderer/components/dock/dock.tsx:98 +#: src/renderer/components/dock/dock.tsx:104 msgid "Fit to window" msgstr "" @@ -997,8 +997,8 @@ msgid "From" msgstr "" #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212 -msgid "From <0>{from} to <1>{to}" -msgstr "" +#~ msgid "From <0>{from} to <1>{to}" +#~ msgstr "" #: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125 msgid "Fs Group" @@ -1059,7 +1059,7 @@ msgid "Helm branch <0>{0} already in use" msgstr "" #: src/renderer/components/+config-secrets/secret-details.tsx:93 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19 msgid "Hide" msgstr "" @@ -1144,7 +1144,7 @@ msgid "Ingresses" msgstr "" #: src/renderer/components/+workloads-pods/pod-details.tsx:118 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192 +#: src/renderer/components/dock/pod-logs.tsx:135 msgid "Init Containers" msgstr "" @@ -1305,7 +1305,7 @@ msgstr "" msgid "Limits" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248 +#: src/renderer/components/dock/pod-logs.tsx:150 msgid "Lines" msgstr "" @@ -1329,8 +1329,8 @@ msgstr "" msgid "Loading" msgstr "" -#: src/renderer/components/+workloads-pods/pod-menu.tsx:90 -#: src/renderer/components/+workloads-pods/pod-menu.tsx:91 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:100 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:101 msgid "Logs" msgstr "" @@ -1436,7 +1436,7 @@ msgstr "" msgid "Min Pods" msgstr "" -#: src/renderer/components/dock/dock.tsx:99 +#: src/renderer/components/dock/dock.tsx:105 msgid "Minimize" msgstr "" @@ -1591,11 +1591,11 @@ msgstr "" msgid "Network Policies" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231 +#: src/renderer/components/dock/pod-logs.tsx:178 msgid "New logs since opening the dialog" msgstr "" -#: src/renderer/components/dock/dock.tsx:86 +#: src/renderer/components/dock/dock.tsx:92 msgid "New tab" msgstr "" @@ -1625,7 +1625,7 @@ msgstr "" #~ msgid "No contexts available or they already added" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:260 +#: src/renderer/components/+add-cluster/add-cluster.tsx:281 msgid "No contexts available or they have been added already" msgstr "" @@ -1721,7 +1721,7 @@ msgstr "" msgid "Ok, got it!" msgstr "" -#: src/renderer/components/dock/dock.tsx:99 +#: src/renderer/components/dock/dock.tsx:105 msgid "Open" msgstr "" @@ -1757,7 +1757,7 @@ msgstr "" msgid "Parameters" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:230 +#: src/renderer/components/+add-cluster/add-cluster.tsx:251 msgid "Paste as text" msgstr "" @@ -1781,7 +1781,7 @@ msgstr "" msgid "Persistent Volumes" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:72 +#: src/renderer/components/+add-cluster/add-cluster.tsx:75 msgid "Please select at least one cluster context" msgstr "" @@ -1834,7 +1834,7 @@ msgstr "" msgid "Pod Status" msgstr "" -#: src/renderer/components/+workloads-pods/pod-menu.tsx:67 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:77 msgid "Pod shell" msgstr "" @@ -1897,7 +1897,7 @@ msgstr "" #~ msgid "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:248 +#: src/renderer/components/+add-cluster/add-cluster.tsx:269 msgid "Pro-Tip: paste kubeconfig to get available contexts" msgstr "" @@ -1905,7 +1905,7 @@ msgstr "" #~ msgid "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:239 +#: src/renderer/components/+add-cluster/add-cluster.tsx:260 msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" msgstr "" @@ -1926,7 +1926,7 @@ msgstr "" msgid "Proxy is used only for non-cluster communication." msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:294 +#: src/renderer/components/+add-cluster/add-cluster.tsx:315 msgid "Proxy settings" msgstr "" @@ -1962,7 +1962,7 @@ msgstr "" msgid "Reason" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107 +#: src/renderer/components/dock/pod-logs.store.ts:66 msgid "Reason: {0} ({1})" msgstr "" @@ -2107,7 +2107,7 @@ msgstr "" msgid "Required field" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:235 +#: src/renderer/components/+add-cluster/add-cluster.tsx:256 #: src/renderer/components/item-object-list/page-filters-list.tsx:31 msgid "Reset" msgstr "" @@ -2249,9 +2249,9 @@ msgstr "" #: src/renderer/components/+apps-releases/release-details.tsx:114 #: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-secrets/secret-details.tsx:97 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216 #: src/renderer/components/+workspaces/workspaces.tsx:132 #: src/renderer/components/dock/edit-resource.tsx:87 +#: src/renderer/components/dock/pod-logs.tsx:161 msgid "Save" msgstr "" @@ -2343,7 +2343,7 @@ msgstr "" #~ msgid "Select context(s)" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:257 +#: src/renderer/components/+add-cluster/add-cluster.tsx:278 msgid "Select contexts" msgstr "" @@ -2356,8 +2356,8 @@ msgstr "" #~ msgid "Select custom kube-config file" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:63 +#: src/renderer/components/+add-cluster/add-cluster.tsx:63 msgid "Select custom kubeconfig file" msgstr "" @@ -2373,7 +2373,7 @@ msgstr "" #~ msgid "Select kubeconfig" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:229 +#: src/renderer/components/+add-cluster/add-cluster.tsx:250 msgid "Select kubeconfig file" msgstr "" @@ -2401,7 +2401,7 @@ msgstr "" #~ msgid "Selected contexts ({0}): <0>{1}" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:256 +#: src/renderer/components/+add-cluster/add-cluster.tsx:277 msgid "Selected contexts: <0>{0}" msgstr "" @@ -2458,13 +2458,13 @@ msgid "Settings" msgstr "" #: src/renderer/components/+nodes/node-menu.tsx:48 -#: src/renderer/components/+workloads-pods/pod-menu.tsx:68 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:78 msgid "Shell" msgstr "" #: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+workloads-pods/pod-container-env.tsx:101 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19 msgid "Show" msgstr "" @@ -2473,10 +2473,22 @@ msgstr "" msgid "Show Notes" msgstr "" +#: src/renderer/components/dock/pod-logs.tsx:160 +msgid "Show current logs" +msgstr "" + +#: src/renderer/components/dock/pod-logs.tsx:160 +msgid "Show previous terminated container logs" +msgstr "" + #: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20 msgid "Show value" msgstr "" +#: src/renderer/components/dock/pod-logs.tsx:154 +msgid "Since" +msgstr "" + #: src/renderer/components/+nodes/node-charts.tsx:80 #: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49 msgid "Size" @@ -2571,12 +2583,12 @@ msgstr "" msgid "Sub-object" msgstr "" -#: src/renderer/components/dock/info-panel.tsx:93 +#: src/renderer/components/dock/info-panel.tsx:95 #: src/renderer/components/wizard/wizard.tsx:131 msgid "Submit" msgstr "" -#: src/renderer/components/dock/info-panel.tsx:94 +#: src/renderer/components/dock/info-panel.tsx:96 msgid "Submitting.." msgstr "" @@ -2584,7 +2596,7 @@ msgstr "" msgid "Subsets" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:102 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Successfully imported <0>{0} cluster(s)" msgstr "" @@ -2618,7 +2630,7 @@ msgstr "" msgid "Terminal" msgstr "" -#: src/renderer/components/dock/dock.tsx:89 +#: src/renderer/components/dock/dock.tsx:95 msgid "Terminal session" msgstr "" @@ -2626,7 +2638,7 @@ msgstr "" msgid "The path to the kubectl binary on the system." msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226 +#: src/renderer/components/dock/pod-logs.tsx:172 msgid "There are no logs available for container." msgstr "" @@ -2756,8 +2768,8 @@ msgstr "" msgid "Usage" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:63 -#: src/renderer/components/+add-cluster/add-cluster.tsx:63 +#: src/renderer/components/+add-cluster/add-cluster.tsx:64 +#: src/renderer/components/+add-cluster/add-cluster.tsx:64 msgid "Use configuration" msgstr "" @@ -2944,7 +2956,7 @@ msgstr "" msgid "singular" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 msgid "timestamps" msgstr "" @@ -2989,8 +3001,8 @@ msgid "{metricsRemainCount} more..." msgstr "" #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240 -msgid "{podName} Logs" -msgstr "" +#~ msgid "{podName} Logs" +#~ msgstr "" #: src/renderer/components/dock/edit-resource.tsx:56 msgid "{resourceType} <0>{resourceName} updated." @@ -3000,6 +3012,6 @@ msgstr "" msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" msgstr "" -#: src/renderer/components/dock/info-panel.tsx:88 +#: src/renderer/components/dock/info-panel.tsx:89 msgid "{submitLabel} & Close" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 5cbf7f603e..58d4a545f1 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -88,7 +88,7 @@ msgstr "Название аккаунта" msgid "Active" msgstr "Активный" -#: src/renderer/components/+add-cluster/add-cluster.tsx:289 +#: src/renderer/components/+add-cluster/add-cluster.tsx:310 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130 msgid "Add Cluster" msgstr "" @@ -113,7 +113,7 @@ msgstr "Добавить привязки к {name}" #~ msgid "Add cluster" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:306 +#: src/renderer/components/+add-cluster/add-cluster.tsx:327 msgid "Add cluster(s)" msgstr "" @@ -204,7 +204,7 @@ msgstr "" msgid "All groups" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57 +#: src/renderer/components/dock/pod-logs.tsx:37 msgid "All logs" msgstr "Все логи" @@ -324,7 +324,7 @@ msgstr "Цели привязки" msgid "Bindings" msgstr "Привязки" -#: src/renderer/components/+add-cluster/add-cluster.tsx:236 +#: src/renderer/components/+add-cluster/add-cluster.tsx:257 msgid "Browse" msgstr "" @@ -405,7 +405,7 @@ msgstr "CPU:" #: src/renderer/components/+workspaces/workspaces.tsx:133 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 -#: src/renderer/components/dock/info-panel.tsx:85 +#: src/renderer/components/dock/info-panel.tsx:86 #: src/renderer/components/wizard/wizard.tsx:130 msgid "Cancel" msgstr "Отмена" @@ -474,7 +474,6 @@ msgstr "Запрос" msgid "Claim Name" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243 #: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93 msgid "Close" @@ -567,7 +566,7 @@ msgstr "Конфигурация" msgid "Connection" msgstr "Соединение" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246 +#: src/renderer/components/dock/pod-logs.tsx:148 msgid "Container" msgstr "Контейнер" @@ -596,8 +595,8 @@ msgid "Container runtime" msgstr "Среда контейнеров" #: src/renderer/components/+workloads-pods/pod-details.tsx:122 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186 #: src/renderer/components/+workloads-pods/pods.tsx:77 +#: src/renderer/components/dock/pod-logs.tsx:129 msgid "Containers" msgstr "Контейнеры" @@ -692,7 +691,7 @@ msgstr "Создать новый секрет" msgid "Create new Service Account" msgstr "Создать новый Service Account" -#: src/renderer/components/dock/dock.tsx:93 +#: src/renderer/components/dock/dock.tsx:99 msgid "Create resource" msgstr "Создать ресурс" @@ -929,7 +928,8 @@ msgstr "Среда" msgid "Error stack" msgstr "Стэк ошибки" -#: src/renderer/components/+add-cluster/add-cluster.tsx:109 +#: src/renderer/components/+add-cluster/add-cluster.tsx:89 +#: src/renderer/components/+add-cluster/add-cluster.tsx:130 msgid "Error while adding cluster(s): {0}" msgstr "" @@ -949,7 +949,7 @@ msgstr "В кластере все в порядке" #~ msgid "Excluded items with \"system:\" prefix" #~ msgstr "За исключением объектов с префиксом “system:”" -#: src/renderer/components/dock/dock.tsx:98 +#: src/renderer/components/dock/dock.tsx:104 msgid "Exit full size mode" msgstr "Выйти из полного размера" @@ -965,7 +965,7 @@ msgstr "Внешний IP" msgid "External IPs" msgstr "Внешние IP" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106 +#: src/renderer/components/dock/pod-logs.store.ts:65 msgid "Failed to load logs: {0}" msgstr "Ошибка загрузки логов: {0}" @@ -990,7 +990,7 @@ msgstr "Финализаторы" msgid "First seen" msgstr "Увиденно впервые" -#: src/renderer/components/dock/dock.tsx:98 +#: src/renderer/components/dock/dock.tsx:104 msgid "Fit to window" msgstr "По размеру окна" @@ -1007,8 +1007,8 @@ msgid "From" msgstr "От" #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212 -msgid "From <0>{from} to <1>{to}" -msgstr "От <0>{from} до <1>{to}" +#~ msgid "From <0>{from} to <1>{to}" +#~ msgstr "От <0>{from} до <1>{to}" #: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125 msgid "Fs Group" @@ -1069,7 +1069,7 @@ msgid "Helm branch <0>{0} already in use" msgstr "" #: src/renderer/components/+config-secrets/secret-details.tsx:93 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19 msgid "Hide" msgstr "Скрыть" @@ -1154,7 +1154,7 @@ msgid "Ingresses" msgstr "Ingresses" #: src/renderer/components/+workloads-pods/pod-details.tsx:118 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192 +#: src/renderer/components/dock/pod-logs.tsx:135 msgid "Init Containers" msgstr "Контейнеры инициализации" @@ -1315,7 +1315,7 @@ msgstr "" msgid "Limits" msgstr "Лимиты" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248 +#: src/renderer/components/dock/pod-logs.tsx:150 msgid "Lines" msgstr "Строки" @@ -1339,8 +1339,8 @@ msgstr "" msgid "Loading" msgstr "Загрузка" -#: src/renderer/components/+workloads-pods/pod-menu.tsx:90 -#: src/renderer/components/+workloads-pods/pod-menu.tsx:91 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:100 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:101 msgid "Logs" msgstr "Логи" @@ -1446,7 +1446,7 @@ msgstr "" msgid "Min Pods" msgstr "Мин. подов" -#: src/renderer/components/dock/dock.tsx:99 +#: src/renderer/components/dock/dock.tsx:105 msgid "Minimize" msgstr "Минимизировать" @@ -1601,11 +1601,11 @@ msgstr "Сетевая файловая система" msgid "Network Policies" msgstr "Network Policies" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231 +#: src/renderer/components/dock/pod-logs.tsx:178 msgid "New logs since opening the dialog" msgstr "Новые логи с момента открытия диалога" -#: src/renderer/components/dock/dock.tsx:86 +#: src/renderer/components/dock/dock.tsx:92 msgid "New tab" msgstr "Новая вкладка" @@ -1643,7 +1643,7 @@ msgstr "Нет доступных нод." #~ msgid "No contexts available or they already added" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:260 +#: src/renderer/components/+add-cluster/add-cluster.tsx:281 msgid "No contexts available or they have been added already" msgstr "" @@ -1739,7 +1739,7 @@ msgstr "Ок" msgid "Ok, got it!" msgstr "" -#: src/renderer/components/dock/dock.tsx:99 +#: src/renderer/components/dock/dock.tsx:105 msgid "Open" msgstr "Открыть" @@ -1775,7 +1775,7 @@ msgstr "Параллелизм" msgid "Parameters" msgstr "Параметры" -#: src/renderer/components/+add-cluster/add-cluster.tsx:230 +#: src/renderer/components/+add-cluster/add-cluster.tsx:251 msgid "Paste as text" msgstr "" @@ -1799,7 +1799,7 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" -#: src/renderer/components/+add-cluster/add-cluster.tsx:72 +#: src/renderer/components/+add-cluster/add-cluster.tsx:75 msgid "Please select at least one cluster context" msgstr "" @@ -1852,7 +1852,7 @@ msgstr "Селектор подов" msgid "Pod Status" msgstr "Статус подов" -#: src/renderer/components/+workloads-pods/pod-menu.tsx:67 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:77 msgid "Pod shell" msgstr "Командная строка пода" @@ -1915,7 +1915,7 @@ msgstr "" #~ msgid "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:248 +#: src/renderer/components/+add-cluster/add-cluster.tsx:269 msgid "Pro-Tip: paste kubeconfig to get available contexts" msgstr "" @@ -1923,7 +1923,7 @@ msgstr "" #~ msgid "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:239 +#: src/renderer/components/+add-cluster/add-cluster.tsx:260 msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" msgstr "" @@ -1944,7 +1944,7 @@ msgstr "Комиссия" msgid "Proxy is used only for non-cluster communication." msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:294 +#: src/renderer/components/+add-cluster/add-cluster.tsx:315 msgid "Proxy settings" msgstr "" @@ -1980,7 +1980,7 @@ msgstr "Готовность" msgid "Reason" msgstr "Причина" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107 +#: src/renderer/components/dock/pod-logs.store.ts:66 msgid "Reason: {0} ({1})" msgstr "Причина: {0} ({1})" @@ -2125,7 +2125,7 @@ msgstr "" msgid "Required field" msgstr "Обязательное поле" -#: src/renderer/components/+add-cluster/add-cluster.tsx:235 +#: src/renderer/components/+add-cluster/add-cluster.tsx:256 #: src/renderer/components/item-object-list/page-filters-list.tsx:31 msgid "Reset" msgstr "Сбросить" @@ -2267,9 +2267,9 @@ msgstr "" #: src/renderer/components/+apps-releases/release-details.tsx:114 #: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-secrets/secret-details.tsx:97 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216 #: src/renderer/components/+workspaces/workspaces.tsx:132 #: src/renderer/components/dock/edit-resource.tsx:87 +#: src/renderer/components/dock/pod-logs.tsx:161 msgid "Save" msgstr "Сохранить" @@ -2361,7 +2361,7 @@ msgstr "Выберите квоту..." #~ msgid "Select context(s)" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:257 +#: src/renderer/components/+add-cluster/add-cluster.tsx:278 msgid "Select contexts" msgstr "" @@ -2374,8 +2374,8 @@ msgstr "" #~ msgid "Select custom kube-config file" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:63 +#: src/renderer/components/+add-cluster/add-cluster.tsx:63 msgid "Select custom kubeconfig file" msgstr "" @@ -2391,7 +2391,7 @@ msgstr "" #~ msgid "Select kubeconfig" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:229 +#: src/renderer/components/+add-cluster/add-cluster.tsx:250 msgid "Select kubeconfig file" msgstr "" @@ -2419,7 +2419,7 @@ msgstr "Выбрать сервисные аккаунты" #~ msgid "Selected contexts ({0}): <0>{1}" #~ msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:256 +#: src/renderer/components/+add-cluster/add-cluster.tsx:277 msgid "Selected contexts: <0>{0}" msgstr "" @@ -2476,13 +2476,13 @@ msgid "Settings" msgstr "" #: src/renderer/components/+nodes/node-menu.tsx:48 -#: src/renderer/components/+workloads-pods/pod-menu.tsx:68 +#: src/renderer/components/+workloads-pods/pod-menu.tsx:78 msgid "Shell" msgstr "Командная строка" #: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+workloads-pods/pod-container-env.tsx:101 -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19 msgid "Show" msgstr "Показать" @@ -2491,10 +2491,22 @@ msgstr "Показать" msgid "Show Notes" msgstr "Показать логи" +#: src/renderer/components/dock/pod-logs.tsx:160 +msgid "Show current logs" +msgstr "" + +#: src/renderer/components/dock/pod-logs.tsx:160 +msgid "Show previous terminated container logs" +msgstr "" + #: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20 msgid "Show value" msgstr "Показать значение" +#: src/renderer/components/dock/pod-logs.tsx:154 +msgid "Since" +msgstr "" + #: src/renderer/components/+nodes/node-charts.tsx:80 #: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49 msgid "Size" @@ -2589,12 +2601,12 @@ msgstr "Тип стратегии" msgid "Sub-object" msgstr "Суб-объект" -#: src/renderer/components/dock/info-panel.tsx:93 +#: src/renderer/components/dock/info-panel.tsx:95 #: src/renderer/components/wizard/wizard.tsx:131 msgid "Submit" msgstr "Отправить" -#: src/renderer/components/dock/info-panel.tsx:94 +#: src/renderer/components/dock/info-panel.tsx:96 msgid "Submitting.." msgstr "Применение.." @@ -2602,7 +2614,7 @@ msgstr "Применение.." msgid "Subsets" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:102 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Successfully imported <0>{0} cluster(s)" msgstr "" @@ -2636,7 +2648,7 @@ msgstr "" msgid "Terminal" msgstr "Терминал" -#: src/renderer/components/dock/dock.tsx:89 +#: src/renderer/components/dock/dock.tsx:95 msgid "Terminal session" msgstr "Сессия терминала" @@ -2644,7 +2656,7 @@ msgstr "Сессия терминала" msgid "The path to the kubectl binary on the system." msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226 +#: src/renderer/components/dock/pod-logs.tsx:172 msgid "There are no logs available for container." msgstr "Для контейнера нет логов." @@ -2774,8 +2786,8 @@ msgstr "Обновить версию" msgid "Usage" msgstr "Использование" -#: src/renderer/components/+add-cluster/add-cluster.tsx:63 -#: src/renderer/components/+add-cluster/add-cluster.tsx:63 +#: src/renderer/components/+add-cluster/add-cluster.tsx:64 +#: src/renderer/components/+add-cluster/add-cluster.tsx:64 msgid "Use configuration" msgstr "" @@ -2962,7 +2974,7 @@ msgstr "сек" msgid "singular" msgstr "" -#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 +#: src/renderer/components/dock/pod-logs.tsx:159 msgid "timestamps" msgstr "временные метки" @@ -3007,8 +3019,8 @@ msgid "{metricsRemainCount} more..." msgstr "{metricsRemainCount} еще…" #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240 -msgid "{podName} Logs" -msgstr "{podName} логи" +#~ msgid "{podName} Logs" +#~ msgstr "{podName} логи" #: src/renderer/components/dock/edit-resource.tsx:56 msgid "{resourceType} <0>{resourceName} updated." @@ -3025,6 +3037,6 @@ msgstr "" "other {<2>Удалить <3>{selectedCount} элементов <4>{selectedNames} {tail}?}\n" "}" -#: src/renderer/components/dock/info-panel.tsx:88 +#: src/renderer/components/dock/info-panel.tsx:89 msgid "{submitLabel} & Close" msgstr "{submitLabel} и закрыть"