1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into extensions-api

This commit is contained in:
Jari Kolehmainen 2020-09-30 19:47:13 +03:00
commit de3849d22c
53 changed files with 1273 additions and 584 deletions

View File

@ -1,69 +1,446 @@
/*
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
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa.
*/
import { Application } from "spectron"
import * as util from "../helpers/utils"
import { spawnSync } from "child_process"
jest.setTimeout(30000)
const describeif = (condition : boolean) => condition ? describe : describe.skip
const itif = (condition : boolean) => condition ? it : it.skip
const BACKSPACE = "\uE003"
jest.setTimeout(60000)
describe("app start", () => {
describe("Lens integration tests", () => {
const TEST_NAMESPACE = "integration-tests"
const BACKSPACE = "\uE003"
let app: Application
const appStart = async () => {
app = util.setup()
await app.start()
// Wait for splash screen to be closed
while (await app.client.getWindowCount() > 1);
await app.client.windowByIndex(0)
await app.client.waitUntilWindowLoaded()
}
const clickWhatsNew = async (app: Application) => {
await app.client.waitUntilTextExists("h1", "What's new")
await app.client.click("button.primary")
await app.client.waitUntilTextExists("h1", "Welcome")
}
const addMinikubeCluster = async (app: Application) => {
await app.client.click("div.add-cluster")
await app.client.waitUntilTextExists("div", "Select kubeconfig file")
await app.client.click("button.primary")
}
describe("app start", () => {
beforeAll(appStart, 20000)
const waitForMinikubeDashboard = async (app: Application) => {
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started")
await app.client.getWindowCount()
await app.client.waitForExist(`iframe[name="minikube"]`)
await app.client.frame("minikube")
await app.client.waitUntilTextExists("span.link-text", "Cluster")
afterAll(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
beforeEach(async () => {
app = util.setup()
await app.start()
await app.client.waitUntilWindowLoaded()
// Wait for splash screen to be closed
while (await app.client.getWindowCount() > 1);
await app.client.windowByIndex(0)
await app.client.waitUntilWindowLoaded()
}, 20000)
})
it('shows "whats new"', async () => {
await clickWhatsNew(app)
})
it('allows to add a cluster', async () => {
const status = spawnSync("minikube status", { shell: true })
// Todo figure out how to access main menu to get these to work
it.skip('shows "add cluster"', async () => {
await app.client.keys(['Shift', 'Meta', 'A'])
await app.client.waitUntilTextExists("h2", "Add Cluster")
await app.client.keys(['Shift', 'Meta'])
})
it.skip('shows "preferences"', async () => {
await app.client.keys(['Meta', ','])
await app.client.waitUntilTextExists("h2", "Preferences")
await app.client.keys('Meta')
})
it.skip('quits Lens"', async () => {
await app.client.keys(['Meta', 'Q'])
await app.client.keys('Meta')
})
})
const minikubeReady = (): boolean => {
// determine if minikube is running
let status = spawnSync("minikube status", { shell: true })
if (status.status !== 0) {
console.warn("minikube not running, skipping test")
return
console.warn("minikube not running")
return false
}
// Remove TEST_NAMESPACE if it already exists
status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true })
if (status.status === 0) {
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`)
status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true })
if (status.status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`)
return false
}
console.log(status.stdout.toString())
}
return true
}
const ready = minikubeReady()
const addMinikubeCluster = async (app: Application) => {
await app.client.click("div.add-cluster")
await app.client.waitUntilTextExists("div", "Select kubeconfig file")
await app.client.click("div.Select__control") // show the context drop-down list
await app.client.waitUntilTextExists("div", "minikube")
if (!await app.client.$("button.primary").isEnabled()) {
await app.client.click("div.minikube") // select minikube context
} // else the only context, which must be 'minikube', is automatically selected
await app.client.click("div.Select__control") // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
await app.client.click("button.primary") // add minikube cluster
}
const waitForMinikubeDashboard = async (app: Application) => {
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started")
await app.client.waitForExist(`iframe[name="minikube"]`)
await app.client.frame("minikube")
await app.client.waitUntilTextExists("span.link-text", "Cluster")
}
describeif(ready)("cluster tests", () => {
let clusterAdded = false
const addCluster = async () => {
await clickWhatsNew(app)
await addMinikubeCluster(app)
await waitForMinikubeDashboard(app)
await app.client.click('a[href="/nodes"]')
await app.client.waitUntilTextExists("div.TableCell", "Ready")
}
describe("cluster add", () => {
beforeAll(appStart, 20000)
afterAll(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
})
it('allows to create a pod', async () => {
const status = spawnSync("minikube status", { shell: true })
if (status.status !== 0) {
console.warn("minikube not running, skipping test")
return
it('allows to add a cluster', async () => {
await addCluster()
clusterAdded = true
})
})
const appStartAddCluster = async () => {
if (clusterAdded) {
await appStart()
await addCluster()
}
await clickWhatsNew(app)
await addMinikubeCluster(app)
await waitForMinikubeDashboard(app)
}
describe("cluster pages", () => {
beforeAll(appStartAddCluster, 40000)
afterAll(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
})
const tests : {
drawer?: string
drawerId?: string
pages: {
name: string,
href: string,
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: "Nodes",
href: "nodes",
expectedSelector: "h5.title",
expectedText: "Nodes"
}]
},
{
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"
} ]
},
{
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"
} ]
},
{
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 () => {
expect(clusterAdded).toBe(true)
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}) => {
it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`a[href="/${href}"]`)
await app.client.waitUntilTextExists(expectedSelector, expectedText)
})
})
if (drawer !== "") {
// hide the drawer
it(`hides ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow()
})
}
})
})
describe("cluster operations", () => {
beforeEach(appStartAddCluster, 40000)
afterEach(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
})
it('shows default namespace', async () => {
expect(clusterAdded).toBe(true)
await app.client.click('a[href="/namespaces"]')
await app.client.waitUntilTextExists("div.TableCell", "default")
await app.client.waitUntilTextExists("div.TableCell", "kube-system")
})
it(`creates ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click('a[href="/namespaces"]')
await app.client.waitUntilTextExists("div.TableCell", "default")
await app.client.waitUntilTextExists("div.TableCell", "kube-system")
await app.client.click("button.add-button")
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace")
await app.client.keys(`${TEST_NAMESPACE}\n`)
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`)
})
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods")
await app.client.click('a[href="/pods"]')
@ -76,24 +453,21 @@ describe("app start", () => {
await app.client.keys("apiVersion: v1\n")
await app.client.keys("kind: Pod\n")
await app.client.keys("metadata:\n")
await app.client.keys(" name: nginx\n")
await app.client.keys(" name: nginx-create-pod-test\n")
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`)
await app.client.keys(BACKSPACE + "spec:\n")
await app.client.keys(" containers:\n")
await app.client.keys("- name: nginx\n")
await app.client.keys("- name: nginx-create-pod-test\n")
await app.client.keys(" image: nginx:alpine\n")
// Create deployent
// Create deployment
await app.client.waitForEnabled("button.Button=Create & Close")
await app.client.click("button.Button=Create & Close")
// Wait until first bits of pod appears on dashboard
await app.client.waitForExist(".name=nginx")
await app.client.waitForExist(".name=nginx-create-pod-test")
// Open pod details
await app.client.click(".name=nginx")
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx")
await app.client.click(".name=nginx-create-pod-test")
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test")
})
})
afterEach(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
})
})

View File

@ -25,7 +25,7 @@ msgstr ""
msgid "(as a percentage of request)"
msgstr "(as a percentage of request)"
#: src/renderer/components/+workspaces/workspaces.tsx:108
#: src/renderer/components/+workspaces/workspaces.tsx:121
msgid "(current)"
msgstr "(current)"
@ -57,11 +57,11 @@ msgstr "<0>{0}</0> successfully created"
#~ msgid "A HTTP proxy server URL (format: http://<address>:<port>)"
#~ msgstr "A HTTP proxy server URL (format: http://<address>:<port>)"
#: src/renderer/components/input/input.validators.ts:40
#: src/renderer/components/input/input.validators.ts:46
msgid "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics."
msgstr "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics."
#: src/renderer/components/+workspaces/workspaces.tsx:84
#: src/renderer/components/+workspaces/workspaces.tsx:93
msgid "A single workspaces contains a list of clusters and their full configuration."
msgstr "A single workspaces contains a list of clusters and their full configuration."
@ -87,8 +87,8 @@ msgstr "Account Name"
msgid "Active"
msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:303
#: src/renderer/components/cluster-manager/clusters-menu.tsx:118
#: src/renderer/components/+add-cluster/add-cluster.tsx:288
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "Add Cluster"
msgstr "Add Cluster"
@ -100,7 +100,7 @@ msgstr "Add Namespace"
msgid "Add RoleBinding"
msgstr "Add RoleBinding"
#: src/renderer/components/+workspaces/workspaces.tsx:125
#: src/renderer/components/+workspaces/workspaces.tsx:138
msgid "Add Workspace"
msgstr "Add Workspace"
@ -112,7 +112,7 @@ msgstr "Add bindings to {name}"
#~ msgid "Add cluster"
#~ msgstr "Add cluster"
#: src/renderer/components/+add-cluster/add-cluster.tsx:320
#: src/renderer/components/+add-cluster/add-cluster.tsx:305
msgid "Add cluster(s)"
msgstr "Add cluster(s)"
@ -136,7 +136,7 @@ msgstr "Add field"
#~ msgid "Adding clusters: <0>{0}</0>"
#~ msgstr "Adding clusters: <0>{0}</0>"
#: src/renderer/components/+preferences/preferences.tsx:103
#: src/renderer/components/+preferences/preferences.tsx:111
msgid "Adding helm branch <0>{0}</0> has failed: {1}"
msgstr "Adding helm branch <0>{0}</0> has failed: {1}"
@ -191,7 +191,7 @@ msgstr "Affinities"
msgid "Age"
msgstr "Age"
#: src/renderer/components/+workspaces/workspaces.tsx:64
#: src/renderer/components/+workspaces/workspaces.tsx:65
msgid "All clusters within workspace will be cleared as well"
msgstr "All clusters within workspace will be cleared as well"
@ -219,11 +219,11 @@ msgstr "Allocatable"
msgid "Allow Privilege Escalation"
msgstr "Allow Privilege Escalation"
#: src/renderer/components/+preferences/preferences.tsx:162
#: src/renderer/components/+preferences/preferences.tsx:169
msgid "Allow telemetry & usage tracking"
msgstr "Allow telemetry & usage tracking"
#: src/renderer/components/+preferences/preferences.tsx:154
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Allow untrusted Certificate Authorities"
msgstr "Allow untrusted Certificate Authorities"
@ -281,7 +281,7 @@ msgstr "Applying.."
msgid "Apps"
msgstr "Apps"
#: src/renderer/components/+workspaces/workspaces.tsx:61
#: src/renderer/components/+workspaces/workspaces.tsx:62
msgid "Are you sure you want remove workspace <0>{0}</0>?"
msgstr "Are you sure you want remove workspace <0>{0}</0>?"
@ -293,7 +293,7 @@ msgstr "Are you sure you want to drain <0>{nodeName}</0>?"
msgid "Arguments"
msgstr "Arguments"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:108
#: src/renderer/components/+landing-page/landing-page.tsx:27
msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button."
msgstr "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button."
@ -323,7 +323,7 @@ msgstr "Binding targets"
msgid "Bindings"
msgstr "Bindings"
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
#: src/renderer/components/+add-cluster/add-cluster.tsx:236
msgid "Browse"
msgstr "Browse"
@ -402,7 +402,7 @@ msgstr "CPU requests"
msgid "CPU:"
msgstr "CPU:"
#: src/renderer/components/+workspaces/workspaces.tsx:119
#: src/renderer/components/+workspaces/workspaces.tsx:133
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
#: src/renderer/components/dock/info-panel.tsx:97
#: src/renderer/components/wizard/wizard.tsx:130
@ -422,7 +422,7 @@ msgstr "Cancel"
msgid "Capacity"
msgstr "Capacity"
#: src/renderer/components/+preferences/preferences.tsx:153
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Certificate Trust"
msgstr "Certificate Trust"
@ -501,7 +501,7 @@ msgstr "Cluster IP"
msgid "Cluster Issuers"
msgstr "Cluster Issuers"
#: src/renderer/components/+preferences/preferences.tsx:126
#: src/renderer/components/+preferences/preferences.tsx:134
msgid "Color Theme"
msgstr "Color Theme"
@ -712,7 +712,6 @@ msgid "Cron Jobs"
msgstr "Cron Jobs"
#: src/renderer/components/+workloads/workloads.tsx:77
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:67
msgid "CronJobs"
msgstr "CronJobs"
@ -759,7 +758,6 @@ msgid "Daemon Sets"
msgstr "Daemon Sets"
#: src/renderer/components/+workloads/workloads.tsx:53
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:57
msgid "DaemonSets"
msgstr "DaemonSets"
@ -784,11 +782,15 @@ msgstr "Default Add Capabilities"
msgid "Default Runtime Class Name"
msgstr "Default Runtime Class Name"
#: src/renderer/components/+preferences/kubectl-binaries.tsx:30
msgid "Default:"
msgstr "Default:"
#: src/renderer/components/+custom-resources/custom-resources.tsx:22
msgid "Definitions"
msgstr "Definitions"
#: src/renderer/components/+workspaces/workspaces.tsx:113
#: src/renderer/components/+workspaces/workspaces.tsx:126
#: src/renderer/components/menu/menu-actions.tsx:84
msgid "Delete"
msgstr "Delete"
@ -799,12 +801,11 @@ msgstr "Deploy Revisions"
#: src/renderer/components/+workloads/workloads.tsx:45
#: src/renderer/components/+workloads-deployments/deployments.tsx:57
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:47
msgid "Deployments"
msgstr "Deployments"
#: src/renderer/components/+apps-helm-charts/helm-charts.tsx:65
#: src/renderer/components/+workspaces/workspaces.tsx:118
#: src/renderer/components/+workspaces/workspaces.tsx:131
msgid "Description"
msgstr "Description"
@ -817,7 +818,7 @@ msgstr "Desired Healthy"
msgid "Desired number of replicas"
msgstr "Desired number of replicas"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:64
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
msgid "Disconnect"
msgstr "Disconnect"
@ -831,7 +832,7 @@ msgstr "Disk"
msgid "Disk:"
msgstr "Disk:"
#: src/renderer/components/+preferences/preferences.tsx:158
#: src/renderer/components/+preferences/preferences.tsx:165
msgid "Does not affect cluster communications!"
msgstr "Does not affect cluster communications!"
@ -840,14 +841,22 @@ msgid "Domains"
msgstr "Domains"
#: src/renderer/components/+preferences/preferences.tsx:129
msgid "Download Mirror"
msgstr "Download Mirror"
#~ msgid "Download Mirror"
#~ msgstr "Download Mirror"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file"
msgstr "Download file"
#: src/renderer/components/+preferences/preferences.tsx:130
#: src/renderer/components/+preferences/kubectl-binaries.tsx:39
msgid "Download kubectl binaries"
msgstr "Download kubectl binaries"
#: src/renderer/components/+preferences/kubectl-binaries.tsx:37
msgid "Download kubectl binaries matching to Kubernetes cluster verison."
msgstr "Download kubectl binaries matching to Kubernetes cluster verison."
#: src/renderer/components/+preferences/kubectl-binaries.tsx:41
msgid "Download mirror for kubectl"
msgstr "Download mirror for kubectl"
@ -873,7 +882,7 @@ msgstr "Duration"
msgid "E-mail"
msgstr "E-mail"
#: src/renderer/components/+workspaces/workspaces.tsx:112
#: src/renderer/components/+workspaces/workspaces.tsx:125
#: src/renderer/components/menu/menu-actions.tsx:80
#: src/renderer/components/menu/menu-actions.tsx:81
msgid "Edit"
@ -1000,7 +1009,7 @@ msgstr "From <0>{from}</0> to <1>{to}</1>"
msgid "Fs Group"
msgstr "Fs Group"
#: src/renderer/components/+landing-page/landing-page.tsx:23
#: src/renderer/components/+landing-page/landing-page.tsx:37
msgid "Get started by associating one or more clusters to Lens."
msgstr "Get started by associating one or more clusters to Lens."
@ -1022,7 +1031,7 @@ msgstr "Groups"
msgid "HPA"
msgstr "HPA"
#: src/renderer/components/+preferences/preferences.tsx:147
#: src/renderer/components/+preferences/preferences.tsx:137
msgid "HTTP Proxy"
msgstr "HTTP Proxy"
@ -1030,7 +1039,7 @@ msgstr "HTTP Proxy"
#~ msgid "HTTP Proxy server. Used for communicating with Kubernetes API."
#~ msgstr "HTTP Proxy server. Used for communicating with Kubernetes API."
#: src/renderer/components/+preferences/preferences.tsx:132
#: src/renderer/components/+preferences/preferences.tsx:145
msgid "Helm"
msgstr "Helm"
@ -1050,7 +1059,7 @@ msgstr "Helm Install: {repo}/{name}"
msgid "Helm Upgrade: {0}"
msgstr "Helm Upgrade: {0}"
#: src/renderer/components/+preferences/preferences.tsx:47
#: src/renderer/components/+preferences/preferences.tsx:51
msgid "Helm branch <0>{0}</0> already in use"
msgstr "Helm branch <0>{0}</0> already in use"
@ -1157,11 +1166,11 @@ msgstr "Installation complete!"
msgid "Installing..."
msgstr "Installing..."
#: src/renderer/components/input/input.validators.ts:44
#: src/renderer/components/input/input.validators.ts:50
msgid "Invalid account ID"
msgstr "Invalid account ID"
#: src/renderer/components/input/input.validators.ts:15
#: src/renderer/components/input/input.validators.ts:16
msgid "Invalid number"
msgstr "Invalid number"
@ -1197,7 +1206,6 @@ msgstr "Job name"
#: src/renderer/components/+workloads/workloads.tsx:69
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:62
#: src/renderer/components/+workloads-jobs/jobs.tsx:36
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:62
msgid "Jobs"
msgstr "Jobs"
@ -1241,6 +1249,10 @@ msgstr "Kubeconfig"
msgid "Kubeconfig File"
msgstr "Kubeconfig File"
#: src/renderer/components/+preferences/kubectl-binaries.tsx:35
msgid "Kubectl Binary"
msgstr "Kubectl Binary"
#: src/renderer/components/+nodes/node-details.tsx:98
msgid "Kubelet version"
msgstr "Kubelet version"
@ -1357,7 +1369,7 @@ msgstr "Max Pods"
msgid "Max Unavailable"
msgstr "Max Unavailable"
#: src/renderer/components/input/input.validators.ts:35
#: src/renderer/components/input/input.validators.ts:41
msgid "Maximum length is {maxLength}"
msgstr "Maximum length is {maxLength}"
@ -1433,7 +1445,7 @@ msgstr "Min Pods"
msgid "Minimize"
msgstr "Minimize"
#: src/renderer/components/input/input.validators.ts:30
#: src/renderer/components/input/input.validators.ts:36
msgid "Minimum length is {minLength}"
msgstr "Minimum length is {minLength}"
@ -1497,7 +1509,7 @@ msgstr "Mounts"
#: src/renderer/components/+workloads-pods/pods.tsx:74
#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50
#: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:40
#: src/renderer/components/+workspaces/workspaces.tsx:117
#: src/renderer/components/+workspaces/workspaces.tsx:130
#: src/renderer/components/dock/edit-resource.tsx:90
#: src/renderer/components/kube-object/kube-object-meta.tsx:20
msgid "Name"
@ -1565,7 +1577,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}"
msgstr "Namespaces: {0}"
#: src/renderer/components/+preferences/preferences.tsx:157
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr "Needed with some corporate proxies that do certificate re-writing."
@ -1626,7 +1638,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:275
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
msgid "No contexts available or they have been added already"
msgstr "No contexts available or they have been added already"
@ -1742,7 +1754,7 @@ msgid "Organization"
msgstr "Organization"
#: src/renderer/components/+workloads/workloads.tsx:29
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:35
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:45
msgid "Overview"
msgstr "Overview"
@ -1758,7 +1770,7 @@ msgstr "Parallelism"
msgid "Parameters"
msgstr "Parameters"
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
#: src/renderer/components/+add-cluster/add-cluster.tsx:230
msgid "Paste as text"
msgstr "Paste as text"
@ -1848,7 +1860,6 @@ msgstr "Pod shell"
#: src/renderer/components/+workloads/workloads.tsx:37
#: src/renderer/components/+workloads-daemonsets/daemonsets.tsx:47
#: src/renderer/components/+workloads-deployments/deployments.tsx:60
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:42
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:89
#: src/renderer/components/+workloads-pods/pods.tsx:73
#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52
@ -1899,7 +1910,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:263
#: src/renderer/components/+add-cluster/add-cluster.tsx:248
msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr "Pro-Tip: paste kubeconfig to get available contexts"
@ -1907,7 +1918,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:254
#: src/renderer/components/+add-cluster/add-cluster.tsx:239
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"
@ -1924,11 +1935,11 @@ msgstr "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgid "Provisioner"
msgstr "Provisioner"
#: src/renderer/components/+preferences/preferences.tsx:150
#: src/renderer/components/+preferences/preferences.tsx:140
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:308
#: src/renderer/components/+add-cluster/add-cluster.tsx:293
msgid "Proxy settings"
msgstr "Proxy settings"
@ -2008,10 +2019,10 @@ msgstr "Release: {0}"
msgid "Releases"
msgstr "Releases"
#: src/renderer/components/+preferences/preferences.tsx:139
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:74
#: src/renderer/components/cluster-manager/clusters-menu.tsx:80
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2022,7 +2033,7 @@ msgstr "Remove"
msgid "Remove <0>{releaseNames}</0>?"
msgstr "Remove <0>{releaseNames}</0>?"
#: src/renderer/components/+workspaces/workspaces.tsx:51
#: src/renderer/components/+workspaces/workspaces.tsx:52
msgid "Remove Workspace"
msgstr "Remove Workspace"
@ -2050,7 +2061,7 @@ msgstr "Remove selected items ({0})"
msgid "Remove {resourceKind} <0>{resourceName}</0>?"
msgstr "Remove {resourceKind} <0>{resourceName}</0>?"
#: src/renderer/components/+preferences/preferences.tsx:114
#: src/renderer/components/+preferences/preferences.tsx:122
msgid "Removing helm branch <0>{0}</0> has failed: {1}"
msgstr "Removing helm branch <0>{0}</0> has failed: {1}"
@ -2074,7 +2085,7 @@ msgstr "Replicas"
msgid "Repo/Name"
msgstr "Repo/Name"
#: src/renderer/components/+preferences/preferences.tsx:133
#: src/renderer/components/+preferences/preferences.tsx:146
msgid "Repositories"
msgstr "Repositories"
@ -2109,7 +2120,7 @@ msgstr "Required Drop Capabilities"
msgid "Required field"
msgstr "Required field"
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
#: src/renderer/components/+add-cluster/add-cluster.tsx:235
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset"
msgstr "Reset"
@ -2252,7 +2263,7 @@ msgstr "Runtime Class"
#: 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:120
#: src/renderer/components/+workspaces/workspaces.tsx:132
#: src/renderer/components/dock/edit-resource.tsx:88
msgid "Save"
msgstr "Save"
@ -2341,13 +2352,13 @@ msgstr "Select a quota.."
#~ msgid "Select context(s)"
#~ msgstr "Select context(s)"
#: src/renderer/components/+add-cluster/add-cluster.tsx:272
#~ msgid "Select contexts"
#~ msgstr "Select contexts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
msgid "Select contexts"
msgstr "Select contexts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:272
msgid "Select contexts (available: {0})"
msgstr "Select contexts (available: {0})"
#~ msgid "Select contexts (available: {0})"
#~ msgstr "Select contexts (available: {0})"
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
@ -2371,7 +2382,7 @@ msgstr "Select custom kubeconfig file"
#~ msgid "Select kubeconfig"
#~ msgstr "Select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
msgid "Select kubeconfig file"
msgstr "Select kubeconfig file"
@ -2399,7 +2410,7 @@ msgstr "Select service accounts"
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr "Selected contexts ({0}): <0>{1}</0>"
#: src/renderer/components/+add-cluster/add-cluster.tsx:271
#: src/renderer/components/+add-cluster/add-cluster.tsx:256
msgid "Selected contexts: <0>{0}</0>"
msgstr "Selected contexts: <0>{0}</0>"
@ -2503,7 +2514,6 @@ msgid "Stateful Sets"
msgstr "Stateful Sets"
#: src/renderer/components/+workloads/workloads.tsx:61
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:52
msgid "StatefulSets"
msgstr "StatefulSets"
@ -2605,11 +2615,11 @@ msgstr "TLS"
msgid "Taints"
msgstr "Taints"
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Telemetry & Usage Tracking"
msgstr "Telemetry & Usage Tracking"
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr "Telemetry & usage data is collected to continuously improve the Lens experience."
@ -2629,15 +2639,19 @@ msgstr "There are no logs available for container."
msgid "There are no logs available."
msgstr "There are no logs available."
#: src/renderer/components/input/input.validators.ts:5
#: src/renderer/components/input/input.validators.ts:6
msgid "This field is required"
msgstr "This field is required"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
#: src/renderer/components/input/input.validators.ts:31
msgid "This field must be a valid path"
msgstr "This field must be a valid path"
#: src/renderer/components/+landing-page/landing-page.tsx:25
msgid "This is the quick launch menu."
msgstr "This is the quick launch menu."
#: src/renderer/components/+preferences/preferences.tsx:156
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr "This will make Lens to trust ANY certificate authority without any validations."
@ -2661,13 +2675,13 @@ msgstr "Tolerations"
msgid "Transmit"
msgstr "Transmit"
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:107
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:106
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:79
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:80
msgid "Trigger"
msgstr "Trigger"
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:103
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:102
msgid "Trigger CronJob <0>{cronjobName}</0>"
msgstr "Trigger CronJob <0>{cronjobName}</0>"
@ -2690,7 +2704,7 @@ msgstr "Trigger CronJob <0>{cronjobName}</0>"
msgid "Type"
msgstr "Type"
#: src/renderer/components/+preferences/preferences.tsx:148
#: src/renderer/components/+preferences/preferences.tsx:138
msgid "Type HTTP proxy url (example: http://proxy.acme.org:8080)"
msgstr "Type HTTP proxy url (example: http://proxy.acme.org:8080)"
@ -2827,11 +2841,11 @@ msgstr "Waiting services to be running"
msgid "Warnings: {0}"
msgstr "Warnings: {0}"
#: src/renderer/components/+landing-page/landing-page.tsx:20
#: src/renderer/components/+landing-page/landing-page.tsx:34
msgid "Welcome!"
msgstr "Welcome!"
#: src/renderer/components/+workspaces/workspaces.tsx:79
#: src/renderer/components/+workspaces/workspaces.tsx:88
msgid "What is a Workspace?"
msgstr "What is a Workspace?"
@ -2844,19 +2858,19 @@ msgid "Workloads"
msgstr "Workloads"
#: src/renderer/components/+workspaces/workspace-menu.tsx:39
#: src/renderer/components/+workspaces/workspaces.tsx:91
#: src/renderer/components/+workspaces/workspaces.tsx:100
msgid "Workspaces"
msgstr "Workspaces"
#: src/renderer/components/+workspaces/workspaces.tsx:81
#: src/renderer/components/+workspaces/workspaces.tsx:90
msgid "Workspaces are used to organize number of clusters into logical groups."
msgstr "Workspaces are used to organize number of clusters into logical groups."
#: src/renderer/components/input/input.validators.ts:10
#: src/renderer/components/input/input.validators.ts:11
msgid "Wrong email format"
msgstr "Wrong email format"
#: src/renderer/components/input/input.validators.ts:25
#: src/renderer/components/input/input.validators.ts:26
msgid "Wrong url format"
msgstr "Wrong url format"
@ -2915,7 +2929,7 @@ msgstr "listKind"
msgid "never"
msgstr "never"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:121
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
msgid "new"
msgstr "new"

View File

@ -25,7 +25,7 @@ msgstr ""
msgid "(as a percentage of request)"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:108
#: src/renderer/components/+workspaces/workspaces.tsx:121
msgid "(current)"
msgstr ""
@ -57,11 +57,11 @@ msgstr ""
#~ msgid "A HTTP proxy server URL (format: http://<address>:<port>)"
#~ msgstr ""
#: src/renderer/components/input/input.validators.ts:40
#: src/renderer/components/input/input.validators.ts:46
msgid "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics."
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:84
#: src/renderer/components/+workspaces/workspaces.tsx:93
msgid "A single workspaces contains a list of clusters and their full configuration."
msgstr ""
@ -87,8 +87,8 @@ msgstr ""
msgid "Active"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:303
#: src/renderer/components/cluster-manager/clusters-menu.tsx:118
#: src/renderer/components/+add-cluster/add-cluster.tsx:288
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "Add Cluster"
msgstr ""
@ -100,7 +100,7 @@ msgstr ""
msgid "Add RoleBinding"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:125
#: src/renderer/components/+workspaces/workspaces.tsx:138
msgid "Add Workspace"
msgstr ""
@ -112,7 +112,7 @@ msgstr ""
#~ msgid "Add cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:320
#: src/renderer/components/+add-cluster/add-cluster.tsx:305
msgid "Add cluster(s)"
msgstr ""
@ -136,7 +136,7 @@ msgstr ""
#~ msgid "Adding clusters: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:103
#: src/renderer/components/+preferences/preferences.tsx:111
msgid "Adding helm branch <0>{0}</0> has failed: {1}"
msgstr ""
@ -191,7 +191,7 @@ msgstr ""
msgid "Age"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:64
#: src/renderer/components/+workspaces/workspaces.tsx:65
msgid "All clusters within workspace will be cleared as well"
msgstr ""
@ -219,11 +219,11 @@ msgstr ""
msgid "Allow Privilege Escalation"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:162
#: src/renderer/components/+preferences/preferences.tsx:169
msgid "Allow telemetry & usage tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:154
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Allow untrusted Certificate Authorities"
msgstr ""
@ -281,7 +281,7 @@ msgstr ""
msgid "Apps"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:61
#: src/renderer/components/+workspaces/workspaces.tsx:62
msgid "Are you sure you want remove workspace <0>{0}</0>?"
msgstr ""
@ -293,7 +293,7 @@ msgstr ""
msgid "Arguments"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:108
#: src/renderer/components/+landing-page/landing-page.tsx:27
msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button."
msgstr ""
@ -323,7 +323,7 @@ msgstr ""
msgid "Bindings"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
#: src/renderer/components/+add-cluster/add-cluster.tsx:236
msgid "Browse"
msgstr ""
@ -402,7 +402,7 @@ msgstr ""
msgid "CPU:"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:119
#: src/renderer/components/+workspaces/workspaces.tsx:133
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
#: src/renderer/components/dock/info-panel.tsx:97
#: src/renderer/components/wizard/wizard.tsx:130
@ -422,7 +422,7 @@ msgstr ""
msgid "Capacity"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:153
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Certificate Trust"
msgstr ""
@ -497,7 +497,7 @@ msgstr ""
msgid "Cluster Issuers"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:126
#: src/renderer/components/+preferences/preferences.tsx:134
msgid "Color Theme"
msgstr ""
@ -708,7 +708,6 @@ msgid "Cron Jobs"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:77
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:67
msgid "CronJobs"
msgstr ""
@ -755,7 +754,6 @@ msgid "Daemon Sets"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:53
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:57
msgid "DaemonSets"
msgstr ""
@ -780,11 +778,15 @@ msgstr ""
msgid "Default Runtime Class Name"
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:30
msgid "Default:"
msgstr ""
#: src/renderer/components/+custom-resources/custom-resources.tsx:22
msgid "Definitions"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:113
#: src/renderer/components/+workspaces/workspaces.tsx:126
#: src/renderer/components/menu/menu-actions.tsx:84
msgid "Delete"
msgstr ""
@ -795,12 +797,11 @@ msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:45
#: src/renderer/components/+workloads-deployments/deployments.tsx:57
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:47
msgid "Deployments"
msgstr ""
#: src/renderer/components/+apps-helm-charts/helm-charts.tsx:65
#: src/renderer/components/+workspaces/workspaces.tsx:118
#: src/renderer/components/+workspaces/workspaces.tsx:131
msgid "Description"
msgstr ""
@ -813,7 +814,7 @@ msgstr ""
msgid "Desired number of replicas"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:64
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
msgid "Disconnect"
msgstr ""
@ -827,7 +828,7 @@ msgstr ""
msgid "Disk:"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:158
#: src/renderer/components/+preferences/preferences.tsx:165
msgid "Does not affect cluster communications!"
msgstr ""
@ -836,14 +837,22 @@ msgid "Domains"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:129
msgid "Download Mirror"
msgstr ""
#~ msgid "Download Mirror"
#~ msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:130
#: src/renderer/components/+preferences/kubectl-binaries.tsx:39
msgid "Download kubectl binaries"
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:37
msgid "Download kubectl binaries matching to Kubernetes cluster verison."
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:41
msgid "Download mirror for kubectl"
msgstr ""
@ -869,7 +878,7 @@ msgstr ""
msgid "E-mail"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:112
#: src/renderer/components/+workspaces/workspaces.tsx:125
#: src/renderer/components/menu/menu-actions.tsx:80
#: src/renderer/components/menu/menu-actions.tsx:81
msgid "Edit"
@ -991,7 +1000,7 @@ msgstr ""
msgid "Fs Group"
msgstr ""
#: src/renderer/components/+landing-page/landing-page.tsx:23
#: src/renderer/components/+landing-page/landing-page.tsx:37
msgid "Get started by associating one or more clusters to Lens."
msgstr ""
@ -1013,7 +1022,7 @@ msgstr ""
msgid "HPA"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:147
#: src/renderer/components/+preferences/preferences.tsx:137
msgid "HTTP Proxy"
msgstr ""
@ -1021,7 +1030,7 @@ msgstr ""
#~ msgid "HTTP Proxy server. Used for communicating with Kubernetes API."
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:132
#: src/renderer/components/+preferences/preferences.tsx:145
msgid "Helm"
msgstr ""
@ -1041,7 +1050,7 @@ msgstr ""
msgid "Helm Upgrade: {0}"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:47
#: src/renderer/components/+preferences/preferences.tsx:51
msgid "Helm branch <0>{0}</0> already in use"
msgstr ""
@ -1148,11 +1157,11 @@ msgstr ""
msgid "Installing..."
msgstr ""
#: src/renderer/components/input/input.validators.ts:44
#: src/renderer/components/input/input.validators.ts:50
msgid "Invalid account ID"
msgstr ""
#: src/renderer/components/input/input.validators.ts:15
#: src/renderer/components/input/input.validators.ts:16
msgid "Invalid number"
msgstr ""
@ -1188,7 +1197,6 @@ msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:69
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:62
#: src/renderer/components/+workloads-jobs/jobs.tsx:36
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:62
msgid "Jobs"
msgstr ""
@ -1232,6 +1240,10 @@ msgstr ""
msgid "Kubeconfig File"
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:35
msgid "Kubectl Binary"
msgstr ""
#: src/renderer/components/+nodes/node-details.tsx:98
msgid "Kubelet version"
msgstr ""
@ -1348,7 +1360,7 @@ msgstr ""
msgid "Max Unavailable"
msgstr ""
#: src/renderer/components/input/input.validators.ts:35
#: src/renderer/components/input/input.validators.ts:41
msgid "Maximum length is {maxLength}"
msgstr ""
@ -1424,7 +1436,7 @@ msgstr ""
msgid "Minimize"
msgstr ""
#: src/renderer/components/input/input.validators.ts:30
#: src/renderer/components/input/input.validators.ts:36
msgid "Minimum length is {minLength}"
msgstr ""
@ -1488,7 +1500,7 @@ msgstr ""
#: src/renderer/components/+workloads-pods/pods.tsx:74
#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50
#: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:40
#: src/renderer/components/+workspaces/workspaces.tsx:117
#: src/renderer/components/+workspaces/workspaces.tsx:130
#: src/renderer/components/dock/edit-resource.tsx:90
#: src/renderer/components/kube-object/kube-object-meta.tsx:20
msgid "Name"
@ -1556,7 +1568,7 @@ msgstr ""
msgid "Namespaces: {0}"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:157
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr ""
@ -1609,7 +1621,7 @@ msgstr ""
#~ msgid "No contexts available or they already added"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
msgid "No contexts available or they have been added already"
msgstr ""
@ -1725,7 +1737,7 @@ msgid "Organization"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:29
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:35
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:45
msgid "Overview"
msgstr ""
@ -1741,7 +1753,7 @@ msgstr ""
msgid "Parameters"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
#: src/renderer/components/+add-cluster/add-cluster.tsx:230
msgid "Paste as text"
msgstr ""
@ -1831,7 +1843,6 @@ msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:37
#: src/renderer/components/+workloads-daemonsets/daemonsets.tsx:47
#: src/renderer/components/+workloads-deployments/deployments.tsx:60
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:42
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:89
#: src/renderer/components/+workloads-pods/pods.tsx:73
#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52
@ -1882,7 +1893,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:263
#: src/renderer/components/+add-cluster/add-cluster.tsx:248
msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr ""
@ -1890,7 +1901,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:254
#: src/renderer/components/+add-cluster/add-cluster.tsx:239
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgstr ""
@ -1907,11 +1918,11 @@ msgstr ""
msgid "Provisioner"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:150
#: src/renderer/components/+preferences/preferences.tsx:140
msgid "Proxy is used only for non-cluster communication."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:308
#: src/renderer/components/+add-cluster/add-cluster.tsx:293
msgid "Proxy settings"
msgstr ""
@ -1991,10 +2002,10 @@ msgstr ""
msgid "Releases"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:139
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:74
#: src/renderer/components/cluster-manager/clusters-menu.tsx:80
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2005,7 +2016,7 @@ msgstr ""
msgid "Remove <0>{releaseNames}</0>?"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:51
#: src/renderer/components/+workspaces/workspaces.tsx:52
msgid "Remove Workspace"
msgstr ""
@ -2033,7 +2044,7 @@ msgstr ""
msgid "Remove {resourceKind} <0>{resourceName}</0>?"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:114
#: src/renderer/components/+preferences/preferences.tsx:122
msgid "Removing helm branch <0>{0}</0> has failed: {1}"
msgstr ""
@ -2057,7 +2068,7 @@ msgstr ""
msgid "Repo/Name"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:133
#: src/renderer/components/+preferences/preferences.tsx:146
msgid "Repositories"
msgstr ""
@ -2092,7 +2103,7 @@ msgstr ""
msgid "Required field"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
#: src/renderer/components/+add-cluster/add-cluster.tsx:235
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset"
msgstr ""
@ -2235,7 +2246,7 @@ msgstr ""
#: 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:120
#: src/renderer/components/+workspaces/workspaces.tsx:132
#: src/renderer/components/dock/edit-resource.tsx:88
msgid "Save"
msgstr ""
@ -2324,13 +2335,13 @@ msgstr ""
#~ msgid "Select context(s)"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:272
#~ msgid "Select contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
msgid "Select contexts"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:272
msgid "Select contexts (available: {0})"
msgstr ""
#~ msgid "Select contexts (available: {0})"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
@ -2354,7 +2365,7 @@ msgstr ""
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
msgid "Select kubeconfig file"
msgstr ""
@ -2382,7 +2393,7 @@ msgstr ""
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:271
#: src/renderer/components/+add-cluster/add-cluster.tsx:256
msgid "Selected contexts: <0>{0}</0>"
msgstr ""
@ -2486,7 +2497,6 @@ msgid "Stateful Sets"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:61
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:52
msgid "StatefulSets"
msgstr ""
@ -2588,11 +2598,11 @@ msgstr ""
msgid "Taints"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Telemetry & Usage Tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr ""
@ -2612,15 +2622,19 @@ msgstr ""
msgid "There are no logs available."
msgstr ""
#: src/renderer/components/input/input.validators.ts:5
#: src/renderer/components/input/input.validators.ts:6
msgid "This field is required"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
#: src/renderer/components/input/input.validators.ts:31
msgid "This field must be a valid path"
msgstr ""
#: src/renderer/components/+landing-page/landing-page.tsx:25
msgid "This is the quick launch menu."
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:156
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr ""
@ -2644,13 +2658,13 @@ msgstr ""
msgid "Transmit"
msgstr ""
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:107
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:106
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:79
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:80
msgid "Trigger"
msgstr ""
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:103
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:102
msgid "Trigger CronJob <0>{cronjobName}</0>"
msgstr ""
@ -2673,7 +2687,7 @@ msgstr ""
msgid "Type"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:148
#: src/renderer/components/+preferences/preferences.tsx:138
msgid "Type HTTP proxy url (example: http://proxy.acme.org:8080)"
msgstr ""
@ -2810,11 +2824,11 @@ msgstr ""
msgid "Warnings: {0}"
msgstr ""
#: src/renderer/components/+landing-page/landing-page.tsx:20
#: src/renderer/components/+landing-page/landing-page.tsx:34
msgid "Welcome!"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:79
#: src/renderer/components/+workspaces/workspaces.tsx:88
msgid "What is a Workspace?"
msgstr ""
@ -2827,19 +2841,19 @@ msgid "Workloads"
msgstr ""
#: src/renderer/components/+workspaces/workspace-menu.tsx:39
#: src/renderer/components/+workspaces/workspaces.tsx:91
#: src/renderer/components/+workspaces/workspaces.tsx:100
msgid "Workspaces"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:81
#: src/renderer/components/+workspaces/workspaces.tsx:90
msgid "Workspaces are used to organize number of clusters into logical groups."
msgstr ""
#: src/renderer/components/input/input.validators.ts:10
#: src/renderer/components/input/input.validators.ts:11
msgid "Wrong email format"
msgstr ""
#: src/renderer/components/input/input.validators.ts:25
#: src/renderer/components/input/input.validators.ts:26
msgid "Wrong url format"
msgstr ""
@ -2898,7 +2912,7 @@ msgstr ""
msgid "never"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:121
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
msgid "new"
msgstr ""

View File

@ -26,7 +26,7 @@ msgstr ""
msgid "(as a percentage of request)"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:108
#: src/renderer/components/+workspaces/workspaces.tsx:121
msgid "(current)"
msgstr ""
@ -58,11 +58,11 @@ msgstr ""
#~ msgid "A HTTP proxy server URL (format: http://<address>:<port>)"
#~ msgstr ""
#: src/renderer/components/input/input.validators.ts:40
#: src/renderer/components/input/input.validators.ts:46
msgid "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics."
msgstr "Это поле может содержать только латинские буквы в нижнем регистре, номера и дефис."
#: src/renderer/components/+workspaces/workspaces.tsx:84
#: src/renderer/components/+workspaces/workspaces.tsx:93
msgid "A single workspaces contains a list of clusters and their full configuration."
msgstr ""
@ -88,8 +88,8 @@ msgstr "Название аккаунта"
msgid "Active"
msgstr "Активный"
#: src/renderer/components/+add-cluster/add-cluster.tsx:303
#: src/renderer/components/cluster-manager/clusters-menu.tsx:118
#: src/renderer/components/+add-cluster/add-cluster.tsx:288
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "Add Cluster"
msgstr ""
@ -101,7 +101,7 @@ msgstr "Добавить Namespace"
msgid "Add RoleBinding"
msgstr "Добавить привязку ролей"
#: src/renderer/components/+workspaces/workspaces.tsx:125
#: src/renderer/components/+workspaces/workspaces.tsx:138
msgid "Add Workspace"
msgstr ""
@ -113,7 +113,7 @@ msgstr "Добавить привязки к {name}"
#~ msgid "Add cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:320
#: src/renderer/components/+add-cluster/add-cluster.tsx:305
msgid "Add cluster(s)"
msgstr ""
@ -137,7 +137,7 @@ msgstr "Добавить поле"
#~ msgid "Adding clusters: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:103
#: src/renderer/components/+preferences/preferences.tsx:111
msgid "Adding helm branch <0>{0}</0> has failed: {1}"
msgstr ""
@ -192,7 +192,7 @@ msgstr "Аффинитеты"
msgid "Age"
msgstr "Возраст"
#: src/renderer/components/+workspaces/workspaces.tsx:64
#: src/renderer/components/+workspaces/workspaces.tsx:65
msgid "All clusters within workspace will be cleared as well"
msgstr ""
@ -220,11 +220,11 @@ msgstr ""
msgid "Allow Privilege Escalation"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:162
#: src/renderer/components/+preferences/preferences.tsx:169
msgid "Allow telemetry & usage tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:154
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Allow untrusted Certificate Authorities"
msgstr ""
@ -282,7 +282,7 @@ msgstr "Применение.."
msgid "Apps"
msgstr "Приложения"
#: src/renderer/components/+workspaces/workspaces.tsx:61
#: src/renderer/components/+workspaces/workspaces.tsx:62
msgid "Are you sure you want remove workspace <0>{0}</0>?"
msgstr ""
@ -294,7 +294,7 @@ msgstr "Выполнить команду drain для ноды <0>{nodeName}</0
msgid "Arguments"
msgstr "Аргументы"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:108
#: src/renderer/components/+landing-page/landing-page.tsx:27
msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button."
msgstr ""
@ -324,7 +324,7 @@ msgstr "Цели привязки"
msgid "Bindings"
msgstr "Привязки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
#: src/renderer/components/+add-cluster/add-cluster.tsx:236
msgid "Browse"
msgstr ""
@ -403,7 +403,7 @@ msgstr "Запросы к процессору"
msgid "CPU:"
msgstr "CPU:"
#: src/renderer/components/+workspaces/workspaces.tsx:119
#: src/renderer/components/+workspaces/workspaces.tsx:133
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
#: src/renderer/components/dock/info-panel.tsx:97
#: src/renderer/components/wizard/wizard.tsx:130
@ -423,7 +423,7 @@ msgstr "Отмена"
msgid "Capacity"
msgstr "Емкость"
#: src/renderer/components/+preferences/preferences.tsx:153
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Certificate Trust"
msgstr ""
@ -502,7 +502,7 @@ msgstr "IP-адрес кластера"
msgid "Cluster Issuers"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:126
#: src/renderer/components/+preferences/preferences.tsx:134
msgid "Color Theme"
msgstr ""
@ -713,7 +713,6 @@ msgid "Cron Jobs"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:77
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:67
msgid "CronJobs"
msgstr "CronJobs"
@ -760,7 +759,6 @@ msgid "Daemon Sets"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:53
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:57
msgid "DaemonSets"
msgstr "DaemonSets"
@ -785,11 +783,15 @@ msgstr ""
msgid "Default Runtime Class Name"
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:30
msgid "Default:"
msgstr ""
#: src/renderer/components/+custom-resources/custom-resources.tsx:22
msgid "Definitions"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:113
#: src/renderer/components/+workspaces/workspaces.tsx:126
#: src/renderer/components/menu/menu-actions.tsx:84
msgid "Delete"
msgstr "Удалить"
@ -800,12 +802,11 @@ msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:45
#: src/renderer/components/+workloads-deployments/deployments.tsx:57
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:47
msgid "Deployments"
msgstr "Deployments"
#: src/renderer/components/+apps-helm-charts/helm-charts.tsx:65
#: src/renderer/components/+workspaces/workspaces.tsx:118
#: src/renderer/components/+workspaces/workspaces.tsx:131
msgid "Description"
msgstr "Описание"
@ -818,7 +819,7 @@ msgstr ""
msgid "Desired number of replicas"
msgstr "Нужный уровень реплик"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:64
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
msgid "Disconnect"
msgstr ""
@ -832,7 +833,7 @@ msgstr "Диск"
msgid "Disk:"
msgstr "Диск:"
#: src/renderer/components/+preferences/preferences.tsx:158
#: src/renderer/components/+preferences/preferences.tsx:165
msgid "Does not affect cluster communications!"
msgstr ""
@ -841,14 +842,22 @@ msgid "Domains"
msgstr "Домены"
#: src/renderer/components/+preferences/preferences.tsx:129
msgid "Download Mirror"
msgstr ""
#~ msgid "Download Mirror"
#~ msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file"
msgstr "Скачать файл"
#: src/renderer/components/+preferences/preferences.tsx:130
#: src/renderer/components/+preferences/kubectl-binaries.tsx:39
msgid "Download kubectl binaries"
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:37
msgid "Download kubectl binaries matching to Kubernetes cluster verison."
msgstr ""
#: src/renderer/components/+preferences/kubectl-binaries.tsx:41
msgid "Download mirror for kubectl"
msgstr ""
@ -874,7 +883,7 @@ msgstr "Продолжительность"
msgid "E-mail"
msgstr "Эл. почта"
#: src/renderer/components/+workspaces/workspaces.tsx:112
#: src/renderer/components/+workspaces/workspaces.tsx:125
#: src/renderer/components/menu/menu-actions.tsx:80
#: src/renderer/components/menu/menu-actions.tsx:81
msgid "Edit"
@ -1001,7 +1010,7 @@ msgstr "От <0>{from}</0> до <1>{to}</1>"
msgid "Fs Group"
msgstr ""
#: src/renderer/components/+landing-page/landing-page.tsx:23
#: src/renderer/components/+landing-page/landing-page.tsx:37
msgid "Get started by associating one or more clusters to Lens."
msgstr ""
@ -1023,7 +1032,7 @@ msgstr "Группы"
msgid "HPA"
msgstr "HPA"
#: src/renderer/components/+preferences/preferences.tsx:147
#: src/renderer/components/+preferences/preferences.tsx:137
msgid "HTTP Proxy"
msgstr ""
@ -1031,7 +1040,7 @@ msgstr ""
#~ msgid "HTTP Proxy server. Used for communicating with Kubernetes API."
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:132
#: src/renderer/components/+preferences/preferences.tsx:145
msgid "Helm"
msgstr ""
@ -1051,7 +1060,7 @@ msgstr "Helm установка: {repo}/{name}"
msgid "Helm Upgrade: {0}"
msgstr "Helm обновление: {0}"
#: src/renderer/components/+preferences/preferences.tsx:47
#: src/renderer/components/+preferences/preferences.tsx:51
msgid "Helm branch <0>{0}</0> already in use"
msgstr ""
@ -1158,11 +1167,11 @@ msgstr "Установка завершена!"
msgid "Installing..."
msgstr "Установка.."
#: src/renderer/components/input/input.validators.ts:44
#: src/renderer/components/input/input.validators.ts:50
msgid "Invalid account ID"
msgstr "Неверный ID аккаунта"
#: src/renderer/components/input/input.validators.ts:15
#: src/renderer/components/input/input.validators.ts:16
msgid "Invalid number"
msgstr "Неверный номер"
@ -1198,7 +1207,6 @@ msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:69
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:62
#: src/renderer/components/+workloads-jobs/jobs.tsx:36
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:62
msgid "Jobs"
msgstr "Jobs"
@ -1242,6 +1250,10 @@ msgstr "Файл конфигурации"
msgid "Kubeconfig File"
msgstr "Файл конфигурации"
#: src/renderer/components/+preferences/kubectl-binaries.tsx:35
msgid "Kubectl Binary"
msgstr ""
#: src/renderer/components/+nodes/node-details.tsx:98
msgid "Kubelet version"
msgstr "Версия Kubelet"
@ -1358,7 +1370,7 @@ msgstr "Макс. подов"
msgid "Max Unavailable"
msgstr ""
#: src/renderer/components/input/input.validators.ts:35
#: src/renderer/components/input/input.validators.ts:41
msgid "Maximum length is {maxLength}"
msgstr "Максимальная длина {maxLength}"
@ -1434,7 +1446,7 @@ msgstr "Мин. подов"
msgid "Minimize"
msgstr "Минимизировать"
#: src/renderer/components/input/input.validators.ts:30
#: src/renderer/components/input/input.validators.ts:36
msgid "Minimum length is {minLength}"
msgstr "Минимальная длина {minLength}"
@ -1498,7 +1510,7 @@ msgstr "Установки"
#: src/renderer/components/+workloads-pods/pods.tsx:74
#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50
#: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:40
#: src/renderer/components/+workspaces/workspaces.tsx:117
#: src/renderer/components/+workspaces/workspaces.tsx:130
#: src/renderer/components/dock/edit-resource.tsx:90
#: src/renderer/components/kube-object/kube-object-meta.tsx:20
msgid "Name"
@ -1566,7 +1578,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}"
msgstr "Namespaces: {0}"
#: src/renderer/components/+preferences/preferences.tsx:157
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr ""
@ -1627,7 +1639,7 @@ msgstr "Нет доступных нод."
#~ msgid "No contexts available or they already added"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
msgid "No contexts available or they have been added already"
msgstr ""
@ -1743,7 +1755,7 @@ msgid "Organization"
msgstr "Организация"
#: src/renderer/components/+workloads/workloads.tsx:29
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:35
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:45
msgid "Overview"
msgstr "Обзор"
@ -1759,7 +1771,7 @@ msgstr "Параллелизм"
msgid "Parameters"
msgstr "Параметры"
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
#: src/renderer/components/+add-cluster/add-cluster.tsx:230
msgid "Paste as text"
msgstr ""
@ -1849,7 +1861,6 @@ msgstr "Командная строка пода"
#: src/renderer/components/+workloads/workloads.tsx:37
#: src/renderer/components/+workloads-daemonsets/daemonsets.tsx:47
#: src/renderer/components/+workloads-deployments/deployments.tsx:60
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:42
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:89
#: src/renderer/components/+workloads-pods/pods.tsx:73
#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52
@ -1900,7 +1911,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:263
#: src/renderer/components/+add-cluster/add-cluster.tsx:248
msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr ""
@ -1908,7 +1919,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:254
#: src/renderer/components/+add-cluster/add-cluster.tsx:239
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgstr ""
@ -1925,11 +1936,11 @@ msgstr ""
msgid "Provisioner"
msgstr "Комиссия"
#: src/renderer/components/+preferences/preferences.tsx:150
#: src/renderer/components/+preferences/preferences.tsx:140
msgid "Proxy is used only for non-cluster communication."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:308
#: src/renderer/components/+add-cluster/add-cluster.tsx:293
msgid "Proxy settings"
msgstr ""
@ -2009,10 +2020,10 @@ msgstr "Установка: {0}"
msgid "Releases"
msgstr "Релизы"
#: src/renderer/components/+preferences/preferences.tsx:139
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:74
#: src/renderer/components/cluster-manager/clusters-menu.tsx:80
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2023,7 +2034,7 @@ msgstr "Удалить"
msgid "Remove <0>{releaseNames}</0>?"
msgstr "Удалить <0>{releaseNames}</0>?"
#: src/renderer/components/+workspaces/workspaces.tsx:51
#: src/renderer/components/+workspaces/workspaces.tsx:52
msgid "Remove Workspace"
msgstr ""
@ -2051,7 +2062,7 @@ msgstr "Удалить выбранные элементы ({0})"
msgid "Remove {resourceKind} <0>{resourceName}</0>?"
msgstr "Удалить {resourceKind} <0>{resourceName}</0>?"
#: src/renderer/components/+preferences/preferences.tsx:114
#: src/renderer/components/+preferences/preferences.tsx:122
msgid "Removing helm branch <0>{0}</0> has failed: {1}"
msgstr ""
@ -2075,7 +2086,7 @@ msgstr "Реплики"
msgid "Repo/Name"
msgstr "Репозиторий/Имя"
#: src/renderer/components/+preferences/preferences.tsx:133
#: src/renderer/components/+preferences/preferences.tsx:146
msgid "Repositories"
msgstr ""
@ -2110,7 +2121,7 @@ msgstr ""
msgid "Required field"
msgstr "Обязательное поле"
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
#: src/renderer/components/+add-cluster/add-cluster.tsx:235
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset"
msgstr "Сбросить"
@ -2253,7 +2264,7 @@ msgstr ""
#: 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:120
#: src/renderer/components/+workspaces/workspaces.tsx:132
#: src/renderer/components/dock/edit-resource.tsx:88
msgid "Save"
msgstr "Сохранить"
@ -2342,13 +2353,13 @@ msgstr "Выберите квоту..."
#~ msgid "Select context(s)"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:272
#~ msgid "Select contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
msgid "Select contexts"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:272
msgid "Select contexts (available: {0})"
msgstr ""
#~ msgid "Select contexts (available: {0})"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
@ -2372,7 +2383,7 @@ msgstr ""
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
msgid "Select kubeconfig file"
msgstr ""
@ -2400,7 +2411,7 @@ msgstr "Выбрать сервисные аккаунты"
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:271
#: src/renderer/components/+add-cluster/add-cluster.tsx:256
msgid "Selected contexts: <0>{0}</0>"
msgstr ""
@ -2504,7 +2515,6 @@ msgid "Stateful Sets"
msgstr ""
#: src/renderer/components/+workloads/workloads.tsx:61
#: src/renderer/components/+workloads-overview/overview-statuses.tsx:52
msgid "StatefulSets"
msgstr "StatefulSets"
@ -2606,11 +2616,11 @@ msgstr "TLS"
msgid "Taints"
msgstr "Метки блокировки"
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Telemetry & Usage Tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr ""
@ -2630,15 +2640,19 @@ msgstr "Для контейнера нет логов."
msgid "There are no logs available."
msgstr "Логи отсутствуют."
#: src/renderer/components/input/input.validators.ts:5
#: src/renderer/components/input/input.validators.ts:6
msgid "This field is required"
msgstr "Это обязательное поле"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
#: src/renderer/components/input/input.validators.ts:31
msgid "This field must be a valid path"
msgstr ""
#: src/renderer/components/+landing-page/landing-page.tsx:25
msgid "This is the quick launch menu."
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:156
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr ""
@ -2662,13 +2676,13 @@ msgstr "Толерантности"
msgid "Transmit"
msgstr "Транзит"
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:107
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:106
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:79
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:80
msgid "Trigger"
msgstr ""
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:103
#: src/renderer/components/+workloads-cronjobs/cronjob-trigger-dialog.tsx:102
msgid "Trigger CronJob <0>{cronjobName}</0>"
msgstr ""
@ -2691,7 +2705,7 @@ msgstr ""
msgid "Type"
msgstr "Тип"
#: src/renderer/components/+preferences/preferences.tsx:148
#: src/renderer/components/+preferences/preferences.tsx:138
msgid "Type HTTP proxy url (example: http://proxy.acme.org:8080)"
msgstr ""
@ -2828,11 +2842,11 @@ msgstr "Ожидание запуска сервисов"
msgid "Warnings: {0}"
msgstr "Предупреждения: {0}"
#: src/renderer/components/+landing-page/landing-page.tsx:20
#: src/renderer/components/+landing-page/landing-page.tsx:34
msgid "Welcome!"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:79
#: src/renderer/components/+workspaces/workspaces.tsx:88
msgid "What is a Workspace?"
msgstr ""
@ -2845,19 +2859,19 @@ msgid "Workloads"
msgstr "Ресурсы"
#: src/renderer/components/+workspaces/workspace-menu.tsx:39
#: src/renderer/components/+workspaces/workspaces.tsx:91
#: src/renderer/components/+workspaces/workspaces.tsx:100
msgid "Workspaces"
msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:81
#: src/renderer/components/+workspaces/workspaces.tsx:90
msgid "Workspaces are used to organize number of clusters into logical groups."
msgstr ""
#: src/renderer/components/input/input.validators.ts:10
#: src/renderer/components/input/input.validators.ts:11
msgid "Wrong email format"
msgstr "Неверный формат электронной почты"
#: src/renderer/components/input/input.validators.ts:25
#: src/renderer/components/input/input.validators.ts:26
msgid "Wrong url format"
msgstr "Неверный url формат"
@ -2916,7 +2930,7 @@ msgstr ""
msgid "never"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:121
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
msgid "new"
msgstr ""

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.4",
"version": "3.6.5-rc.1",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",
@ -39,7 +39,7 @@
},
"config": {
"bundledKubectlVersion": "1.17.11",
"bundledHelmVersion": "3.3.1"
"bundledHelmVersion": "3.3.4"
},
"engines": {
"node": ">=12.0 <13.0"

View File

@ -93,6 +93,10 @@ export class BaseStore<T = any> extends Singleton {
}
}
unregisterIpcListener() {
ipcRenderer.removeAllListeners(this.syncChannel)
}
disableSync() {
this.syncDisposers.forEach(dispose => dispose());
this.syncDisposers.length = 0;

View File

@ -9,7 +9,7 @@ export const clusterIpc = {
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(true);
return cluster.activate();
}
},
}),

View File

@ -1,6 +1,6 @@
import type { WorkspaceId } from "./workspace-store";
import path from "path";
import { app, ipcRenderer, remote } from "electron";
import { app, ipcRenderer, remote, webFrame, webContents } from "electron";
import { unlink } from "fs-extra";
import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
@ -73,20 +73,27 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: migrations,
});
if (ipcRenderer) {
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
this.applyWithoutSync(() => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host}`, model);
this.getById(model.id)?.updateModel(model);
})
})
}
}
@observable activeClusterId: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
this.applyWithoutSync(() => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model);
this.getById(model.id)?.updateModel(model);
})
})
}
unregisterIpcListener() {
super.unregisterIpcListener()
ipcRenderer.removeAllListeners("cluster:state")
}
@computed get activeCluster(): Cluster | null {
return this.getById(this.activeClusterId);
}
@ -122,10 +129,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.size > 0;
}
hasContext(name: string) {
return this.clustersList.some(cluster => cluster.contextName === name);
}
getById(id: ClusterId): Cluster {
return this.clusters.get(id);
}

View File

@ -146,14 +146,22 @@ describe("config with existing clusters", () => {
id: 'cluster1',
kubeConfig: 'foo',
contextName: 'foo',
preferences: { terminalCWD: '/foo' }
preferences: { terminalCWD: '/foo' },
workspace: 'default'
},
{
id: 'cluster2',
kubeConfig: 'foo2',
contextName: 'foo2',
preferences: { terminalCWD: '/foo2' }
}
},
{
id: 'cluster3',
kubeConfig: 'foo',
contextName: 'foo',
preferences: { terminalCWD: '/foo' },
workspace: 'foo'
},
]
})
}
@ -183,10 +191,12 @@ describe("config with existing clusters", () => {
it("allows getting all of the clusters", async () => {
const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(3)
expect(storedClusters[0].id).toBe('cluster1')
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
expect(storedClusters[1].id).toBe('cluster2')
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
expect(storedClusters[2].id).toBe('cluster3')
})
})

View File

@ -1,9 +1,10 @@
import { PrometheusLens } from "../main/prometheus/lens";
import { PrometheusHelm } from "../main/prometheus/helm";
import { PrometheusOperator } from "../main/prometheus/operator";
import { PrometheusStacklight } from "../main/prometheus/stacklight";
import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry";
[PrometheusLens, PrometheusHelm, PrometheusOperator].forEach(providerClass => {
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
const provider = new providerClass()
PrometheusProviderRegistry.registerProvider(provider.id, provider)
});

View File

@ -59,8 +59,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
colorTheme: UserStore.defaultTheme,
downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
downloadBinariesPath: this.getDefaultKubectlPath(),
kubectlBinariesPath: ""
};
get isNewVersion() {

View File

@ -56,10 +56,10 @@ export class Cluster implements ClusterModel {
@observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server url
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable online: boolean;
@observable accessible: boolean;
@observable ready: boolean;
@observable disconnected: boolean;
@observable online = false;
@observable accessible = false;
@observable ready = false;
@observable disconnected = true;
@observable failureReason: string;
@observable nodes = 0;
@observable version: string;
@ -124,13 +124,14 @@ export class Cluster implements ClusterModel {
this.eventDisposers.length = 0;
}
async activate(init = false) {
@action
async activate() {
logger.info(`[CLUSTER]: activate`, this.getMeta());
await this.whenInitialized;
if (!this.eventDisposers.length) {
this.bindEvents();
}
if (this.disconnected || (!init && !this.accessible)) {
if (this.disconnected || !this.accessible) {
await this.reconnect();
}
await this.refreshConnectionStatus()
@ -143,6 +144,7 @@ export class Cluster implements ClusterModel {
return this.pushState();
}
@action
async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.contextHandler.stopServer();

View File

@ -36,7 +36,7 @@ const packageMirrors: Map<string, string> = new Map([
let bundledPath: string
const initScriptVersionString = "# lens-initscript v3\n"
if (isDevelopment || isTestEnv) {
if (isDevelopment || isTestEnv) {
const platformName = isWindows ? "windows" : process.platform
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
} else {
@ -110,7 +110,11 @@ export class Kubectl {
}
protected getDownloadDir() {
return userStore.preferences?.downloadBinariesPath || Kubectl.kubectlDir
if (userStore.preferences?.downloadBinariesPath) {
return path.join(userStore.preferences.downloadBinariesPath, "kubectl")
}
return Kubectl.kubectlDir
}
public async getPath(bundled = false): Promise<string> {
@ -214,7 +218,7 @@ export class Kubectl {
});
isValid = !await this.checkBinary(this.path, false)
}
if(!isValid) {
if (!isValid) {
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
return false
@ -279,6 +283,12 @@ export class Kubectl {
bashScript += "fi\n"
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
bashScript += "export NO_PROXY\n"
bashScript += "unset tempkubeconfig\n"
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 })
@ -304,6 +314,11 @@ export class Kubectl {
zshScript += "d=${d/#:/}\n"
zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n"
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
zshScript += "export NO_PROXY\n"
zshScript += "unset tempkubeconfig\n"
zshScript += "unset OLD_ZDOTDIR\n"
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 })

View File

@ -44,9 +44,7 @@ export class LensProxy {
const spdyProxy = spdy.createServer({
spdy: {
plain: true,
connection: {
autoSpdy31: true
}
protocols: ["http/1.1", "spdy/3.1"]
}
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res)
@ -55,11 +53,7 @@ export class LensProxy {
if (req.url.startsWith(`${apiPrefix}?`)) {
this.handleWsUpgrade(req, socket, head)
} else {
if (req.headers.upgrade?.startsWith("SPDY")) {
this.handleSpdyProxy(proxy, req, socket, head)
} else {
socket.end()
}
this.handleProxyUpgrade(proxy, req, socket, head)
}
})
spdyProxy.on("error", (err) => {
@ -68,17 +62,45 @@ export class LensProxy {
return spdyProxy
}
protected async handleSpdyProxy(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const cluster = this.clusterManager.getClusterForRequest(req)
if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "")
const apiUrl = url.parse(cluster.apiUrl)
const res = new http.ServerResponse(req)
res.assignSocket(socket)
res.setHeader("Location", proxyUrl)
res.setHeader("Host", apiUrl.hostname)
res.statusCode = 302
res.end()
const pUrl = url.parse(proxyUrl)
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }
const proxySocket = new net.Socket()
proxySocket.connect(connectOpts, () => {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`)
proxySocket.write(`Host: ${apiUrl.host}\r\n`)
for (let i = 0; i < req.rawHeaders.length; i += 2) {
const key = req.rawHeaders[i]
if (key !== "Host" && key !== "Authorization") {
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`)
}
}
proxySocket.write("\r\n")
proxySocket.write(head)
})
proxySocket.on('data', function (chunk) {
socket.write(chunk)
})
proxySocket.on('end', function () {
socket.end()
})
proxySocket.on('error', function (err) {
socket.write("HTTP/" + req.httpVersion + " 500 Connection error\r\n\r\n");
socket.end()
})
socket.on('data', function (chunk) {
proxySocket.write(chunk)
})
socket.on('end', function () {
proxySocket.end()
})
socket.on('error', function () {
proxySocket.end()
})
}
}
@ -134,7 +156,6 @@ export class LensProxy {
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.clusterManager.getClusterForRequest(req)
if (cluster) {
await cluster.contextHandler.ensureServer();
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler)
if (proxyTarget) {
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"

View File

@ -37,19 +37,19 @@ export class PrometheusOperator implements PrometheusProvider {
memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
) by (node)
)
`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`),
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`,
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`,
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`,
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`,
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`,
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`,
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`,
podUsage: `sum(kubelet_running_pod_count{node=~"${opts.nodes}"})`,
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`
}
case 'nodes':
return {
@ -62,10 +62,10 @@ export class PrometheusOperator implements PrometheusProvider {
}
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,

View File

@ -0,0 +1,83 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"
export class PrometheusStacklight implements PrometheusProvider {
id = "stacklight"
name = "Stacklight"
rateAccuracy = "1m"
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight")
const service = resp.body
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
switch(opts.category) {
case 'cluster':
return {
memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
) by (kubernetes_name)
`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`),
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
podUsage: `sum(kubelet_running_pod_count{instance=~"${opts.nodes}"})`,
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`,
fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`
}
case 'nodes':
return {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`,
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`,
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`,
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`
}
case 'pods':
return {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
}
case 'pvc':
return {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
}
case 'ingress':
const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
}
}
}
}

View File

@ -125,6 +125,8 @@ export class ShellSession extends EventEmitter {
if (this.preferences.httpsProxy) {
env["HTTPS_PROXY"] = this.preferences.httpsProxy
}
const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]]
env["NO_PROXY"] = no_proxy.filter(address => !!address).join()
if (env.DEBUG) { // do not pass debug option to bash
delete env["DEBUG"]
}

View File

@ -7,6 +7,7 @@ import version260Beta3 from "./2.6.0-beta.3"
import version270Beta0 from "./2.7.0-beta.0"
import version270Beta1 from "./2.7.0-beta.1"
import version360Beta1 from "./3.6.0-beta.1"
import snap from "./snap"
export default {
...version200Beta2,
@ -16,4 +17,5 @@ export default {
...version270Beta0,
...version270Beta1,
...version360Beta1,
...snap
}

View File

@ -0,0 +1,33 @@
// Fix embedded kubeconfig paths under snap config
import { migration } from "../migration-wrapper";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { getAppVersion } from "../../common/utils/app-version";
import fs from "fs"
export default migration({
version: getAppVersion(), // Run always after upgrade
run(store, printLog) {
if (!process.env["SNAP"]) return;
printLog("Migrating embedded kubeconfig paths")
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
printLog("Number of clusters to migrate: ", storedClusters.length)
const migratedClusters = storedClusters
.map(cluster => {
/**
* replace snap version with 'current' in kubeconfig path
*/
if (!fs.existsSync(cluster.kubeConfigPath)) {
const kubeconfigPath = cluster.kubeConfigPath.replace(/\/snap\/kontena-lens\/[0-9]*\//, "/snap/kontena-lens/current/")
cluster.kubeConfigPath = kubeconfigPath
}
return cluster;
})
store.set("clusters", migratedClusters)
}
})

View File

@ -223,6 +223,7 @@ export class Issuer extends KubeObject {
}
getConditions() {
if (!this.status?.conditions) return [];
const { conditions = [] } = this.status;
return conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;

View File

@ -50,7 +50,7 @@ export class CustomResourceDefinition extends KubeObject {
message: string;
reason: string;
status: string;
type: string;
type?: string;
}[];
acceptedNames: {
plural: string;
@ -132,7 +132,7 @@ export class CustomResourceDefinition extends KubeObject {
}
getConditions() {
if (!this.status.conditions) return [];
if (!this.status?.conditions) return [];
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {

View File

@ -115,12 +115,12 @@ export class KubeObject implements ItemObject {
return KubeObject.stringifyLabels(this.metadata.labels);
}
getAnnotations(): string[] {
getAnnotations(filter = false): string[] {
const labels = KubeObject.stringifyLabels(this.metadata.annotations);
return labels.filter(label => {
return filter ? labels.filter(label => {
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
return !skip;
})
}) : labels;
}
getOwnerRefs() {
@ -138,7 +138,7 @@ export class KubeObject implements ItemObject {
getNs(),
getId(),
...getLabels(),
...getAnnotations(),
...getAnnotations(true),
]
}

View File

@ -1,6 +1,6 @@
import "./components/app.scss"
import React from "react";
import { render } from "react-dom";
import { render, unmountComponentAtNode } from "react-dom";
import { isMac } from "../common/vars";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
@ -30,12 +30,27 @@ export async function bootstrap(App: AppComponent) {
themeStore.init(),
]);
// Register additional store listeners
clusterStore.registerIpcListener();
// init app's dependencies if any
if (App.init) {
await App.init();
extensionStore.autoEnableOnLoad(getLensRuntime);
}
render(<App/>, rootElem);
window.addEventListener("message", (ev: MessageEvent) => {
if (ev.data === "teardown") {
userStore.unregisterIpcListener()
workspaceStore.unregisterIpcListener()
clusterStore.unregisterIpcListener()
unmountComponentAtNode(rootElem)
window.location.href = "about:blank"
}
})
render(<>
{isMac && <div id="draggable-top" />}
<App />
</>, rootElem);
}
// run

View File

@ -99,10 +99,7 @@ export class AddCluster extends React.Component {
getContexts(config: KubeConfig): Map<string, KubeConfig> {
const contexts = new Map();
splitConfig(config).forEach(config => {
const isExists = clusterStore.hasContext(config.currentContext);
if (!isExists) {
contexts.set(config.currentContext, config);
}
})
return contexts
}

View File

@ -3,8 +3,8 @@ import "./helm-chart-details.scss";
import React, { Component } from "react";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { t, Trans } from "@lingui/macro";
import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { observable, toJS } from "mobx";
import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer";
import { autobind, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer";
@ -25,39 +25,41 @@ interface Props {
export class HelmChartDetails extends Component<Props> {
@observable chartVersions: HelmChart[];
@observable selectedChart: HelmChart;
@observable description: string = null;
@observable readme: string = null;
@observable error: string = null;
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
@disposeOnUnmount
chartSelector = autorun(async () => {
if (!this.props.chart) return;
this.chartVersions = null;
this.selectedChart = null;
this.description = null;
this.loadChartData();
this.chartPromise.then(data => {
this.description = data.readme;
this.chartVersions = data.versions;
this.selectedChart = data.versions[0];
});
});
async componentDidMount() {
const { chart: { name, repo, version } } = this.props
loadChartData(version?: string) {
const { chart: { name, repo } } = this.props;
if (this.chartPromise) this.chartPromise.cancel();
this.chartPromise = helmChartsApi.get(repo, name, version);
try {
const { readme, versions } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
this.readme = readme
this.chartVersions = versions
this.selectedChart = versions[0]
} catch (error) {
this.error = error
}
}
componentWillUnmount() {
this.chartPromise?.cancel();
}
@autobind()
onVersionChange(opt: SelectOption) {
const version = opt.value;
async onVersionChange({ value: version }: SelectOption) {
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
this.description = null;
this.loadChartData(version);
this.chartPromise.then(data => {
this.description = data.readme
});
this.readme = null;
try {
this.chartPromise?.cancel();
const { chart: { name, repo } } = this.props;
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
this.readme = readme;
} catch (error) {
this.error = error;
}
}
@autobind()
@ -79,7 +81,7 @@ export class HelmChartDetails extends Component<Props> {
<div className="intro-contents box grow">
<div className="description flex align-center justify-space-between">
{selectedChart.getDescription()}
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/>
<Button primary label={_i18n._(t`Install`)} onClick={this.install} />
</div>
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
<Select
@ -95,12 +97,12 @@ export class HelmChartDetails extends Component<Props> {
</DrawerItem>
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
{selectedChart.getMaintainers().map(({ name, email, url }) =>
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a>
<a key={name} href={url || `mailto:${email}`} target="_blank">{name}</a>
)}
</DrawerItem>
{selectedChart.getKeywords().length > 0 && (
<DrawerItem name={_i18n._(t`Keywords`)} labelsOnly>
{selectedChart.getKeywords().map(key => <Badge key={key} label={key}/>)}
{selectedChart.getKeywords().map(key => <Badge key={key} label={key} />)}
</DrawerItem>
)}
</div>
@ -108,14 +110,35 @@ export class HelmChartDetails extends Component<Props> {
);
}
renderReadme() {
if (this.readme === null) {
return <Spinner center />
}
return (
<div className="chart-description">
<MarkdownViewer markdown={this.readme} />
</div>
)
}
renderContent() {
if (this.selectedChart === null || this.description === null) return <Spinner center/>;
if (!this.selectedChart) {
return <Spinner center />;
}
if (this.error) {
return (
<div className="box grow">
<p className="error">{this.error}</p>
</div>
)
}
return (
<div className="box grow">
{this.renderIntroduction()}
<div className="chart-description">
<MarkdownViewer markdown={this.description}/>
</div>
{this.renderReadme()}
</div>
);
}

View File

@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
(items: HelmChart[]) => items.filter(item => !item.deprecated)
]}
customizeHeader={() => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
)}
renderTableHeader={[
{ className: "icon" },
@ -99,10 +99,12 @@ export class HelmCharts extends Component<Props> {
detailsItem={this.selectedChart}
onDetails={this.showDetails}
/>
{this.selectedChart && (
<HelmChartDetails
chart={this.selectedChart}
hideDetails={this.hideDetails}
/>
)}
</>
);
}

View File

@ -13,8 +13,9 @@ import { apiManager } from "../../api/api-manager";
import { crdStore } from "./crd.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Input } from "../input";
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
interface Props extends KubeObjectDetailsProps {
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
}
function CrdColumnValue({ value }: { value: any[] | {} | string }) {
@ -66,12 +67,14 @@ export class CrdResourceDetails extends React.Component<Props> {
})}
{showStatus && (
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly>
{object.status.conditions.map((condition: { type: string; message: string; status: string }) => {
const { type, message, status } = condition;
{object.status.conditions.map((condition, index) => {
const { type, reason, message, status } = condition;
const kind = type || reason;
if (!kind) return null;
return (
<Badge
key={type} label={type}
className={cssNames({ disabled: status === "False" }, type.toLowerCase())}
key={kind + index} label={kind}
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
tooltip={message}
/>
);

View File

@ -58,7 +58,7 @@ export class NamespaceSelect extends React.Component<Props> {
const { value, label } = option;
return label || (
<>
{showIcons && <Icon small material="layers"/>}
{showIcons && <Icon small material="layers" />}
{value}
</>
);
@ -91,14 +91,15 @@ export class NamespaceSelectFilter extends React.Component {
closeMenuOnSelect={false}
isOptionSelected={() => false}
controlShouldRenderValue={false}
onChange={({ value: namespace }: SelectOption) => toggleContext(namespace)}
isMulti
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
formatOptionLabel={({ value: namespace }: SelectOption) => {
const isSelected = hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE}/>
<FilterIcon type={FilterType.NAMESPACE} />
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
{isSelected && <Icon small material="check" className="box right" />}
</div>
)
}}

View File

@ -18,44 +18,22 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
{ value: "china", label: "China (Azure)" },
]
const save = () => {
preferences.downloadBinariesPath = downloadPath;
preferences.kubectlBinariesPath = binariesPath;
}
const renderPath = () => {
if (preferences.downloadKubectlBinaries) {
return null;
}
return (
<>
<SubTitle title="Path to Kubectl binary"/>
<Input
theme="round-black"
value={binariesPath}
validators={isPath}
onChange={setBinariesPath}
onBlur={save}
/>
<small className="hint">
<Trans>Default:</Trans>{" "}{Kubectl.bundledKubectlPath}
</small>
</>
);
}
return (
<>
<h2><Trans>Kubectl Binary</Trans></h2>
<small className="hint">
<Trans>Download kubectl binaries matching to Kubernetes cluster verison.</Trans>
</small>
<Checkbox
label={<Trans>Download kubectl binaries</Trans>}
value={preferences.downloadKubectlBinaries}
onChange={downloadKubectlBinaries => preferences.downloadKubectlBinaries = downloadKubectlBinaries}
/>
<small className="hint">
<Trans>Download kubectl binaries matching to Kubernetes cluster version.</Trans>
</small>
<SubTitle title="Download mirror" />
<Select
placeholder={<Trans>Download mirror for kubectl</Trans>}
@ -64,20 +42,32 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
disabled={!preferences.downloadKubectlBinaries}
/>
<SubTitle title="Directory for binaries"/>
<SubTitle title="Directory for binaries" />
<Input
theme="round-black"
value={downloadPath}
placeholder={`Directory to download binaries into`}
placeholder={userStore.getDefaultKubectlPath()}
validators={isPath}
onChange={setDownloadPath}
onBlur={save}
disabled={!preferences.downloadKubectlBinaries}
/>
<small>
Default: {userStore.getDefaultKubectlPath()}
<small className="hint">
The directory to download binaries into.
</small>
<SubTitle title="Path to Kubectl binary" />
<Input
theme="round-black"
placeholder={Kubectl.bundledKubectlPath}
value={binariesPath}
validators={isPath}
onChange={setBinariesPath}
onBlur={save}
disabled={preferences.downloadKubectlBinaries}
/>
<small className="hint">
<Trans>The path to the kubectl binary on the system.</Trans>
</small>
{renderPath()}
</>
);
});

View File

@ -11,11 +11,9 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
@observable metrics: IPodMetrics = null;
loadMetrics(daemonSet: DaemonSet) {
async loadMetrics(daemonSet: DaemonSet) {
const pods = this.getChildPods(daemonSet);
return podsApi.getMetrics(pods, daemonSet.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, daemonSet.getNs(), "");
}
getChildPods(daemonSet: DaemonSet): Pod[] {

View File

@ -16,11 +16,9 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
], "desc");
}
loadMetrics(deployment: Deployment) {
async loadMetrics(deployment: Deployment) {
const pods = this.getChildPods(deployment);
return podsApi.getMetrics(pods, deployment.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, deployment.getNs(), "");
}
getStatuses(deployments?: Deployment[]) {

View File

@ -5,72 +5,54 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { OverviewWorkloadStatus } from "./overview-workload-status";
import { Link } from "react-router-dom";
import { cronJobsURL, daemonSetsURL, deploymentsURL, jobsURL, podsURL, statefulSetsURL } from "../+workloads";
import { podsStore } from "../+workloads-pods/pods.store";
import { deploymentStore } from "../+workloads-deployments/deployments.store";
import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store";
import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { workloadURL, workloadStores } from "../+workloads";
import { namespaceStore } from "../+namespaces/namespace.store";
import { PageFiltersList } from "../item-object-list/page-filters-list";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
import { isAllowedResource } from "../../../common/rbac";
import { isAllowedResource, KubeResource } from "../../../common/rbac";
import { ResourceNames } from "../../../renderer/utils/rbac";
import { autobind } from "../../utils";
import { _i18n } from "../../i18n";
const resources: KubeResource[] = [
"pods",
"deployments",
"statefulsets",
"daemonsets",
"jobs",
"cronjobs",
]
@observer
export class OverviewStatuses extends React.Component {
@autobind()
renderWorkload(resource: KubeResource): React.ReactElement {
const store = workloadStores[resource];
const items = store.getAllByNs(namespaceStore.contextNs)
return (
<div className="workload" key={resource}>
<div className="title">
<Link to={workloadURL[resource]()}>{ResourceNames[resource]} ({items.length})</Link>
</div>
<OverviewWorkloadStatus status={store.getStatuses(items)} />
</div>
)
}
render() {
const { contextNs } = namespaceStore;
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs(contextNs) : [];
const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs(contextNs) : [];
const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs(contextNs) : [];
const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs(contextNs) : [];
const workloads = resources
.filter(isAllowedResource)
.map(this.renderWorkload);
return (
<div className="OverviewStatuses">
<div className="header flex gaps align-center">
<h5 className="box grow"><Trans>Overview</Trans></h5>
<NamespaceSelectFilter/>
<NamespaceSelectFilter />
</div>
<PageFiltersList/>
<PageFiltersList />
<div className="workloads">
{isAllowedResource("pods") &&
<div className="workload">
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
</div>
}
{isAllowedResource("deployments") &&
<div className="workload">
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
</div>
}
{isAllowedResource("statefulsets") &&
<div className="workload">
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
</div>
}
{isAllowedResource("daemonsets") &&
<div className="workload">
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
</div>
}
{isAllowedResource("jobs") &&
<div className="workload">
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
</div>
}
{isAllowedResource("cronjobs") &&
<div className="workload">
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>
</div>
}
{workloads}
</div>
</div>
)

View File

@ -113,7 +113,8 @@ const SecretKey = (props: SecretKeyProps) => {
const [loading, setLoading] = useState(false)
const [secret, setSecret] = useState<Secret>()
const showKey = async () => {
const showKey = async (evt: React.MouseEvent) => {
evt.preventDefault()
setLoading(true)
const secret = await secretsStore.load({ name, namespace });
setLoading(false)

View File

@ -10,11 +10,9 @@ export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
api = replicaSetApi
@observable metrics: IPodMetrics = null;
loadMetrics(replicaSet: ReplicaSet) {
async loadMetrics(replicaSet: ReplicaSet) {
const pods = this.getChildPods(replicaSet);
return podsApi.getMetrics(pods, replicaSet.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, replicaSet.getNs(), "");
}
getChildPods(replicaSet: ReplicaSet) {

View File

@ -10,11 +10,9 @@ export class StatefulSetStore extends KubeObjectStore<StatefulSet> {
api = statefulSetApi
@observable metrics: IPodMetrics = null;
loadMetrics(statefulSet: StatefulSet) {
async loadMetrics(statefulSet: StatefulSet) {
const pods = this.getChildPods(statefulSet);
return podsApi.getMetrics(pods, statefulSet.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, statefulSet.getNs(), "");
}
getChildPods(statefulSet: StatefulSet) {

View File

@ -1,3 +1,3 @@
export * from "./workloads.route"
export * from "./workloads"
export * from "./workloads.stores"

View File

@ -1,6 +1,7 @@
import { RouteProps } from "react-router"
import { Workloads } from "./workloads";
import { buildURL, IURLParams } from "../../navigation";
import { KubeResource } from "../../../common/rbac";
export const workloadsRoute: RouteProps = {
get path() {
@ -62,3 +63,12 @@ export const daemonSetsURL = buildURL<IDaemonSetsRouteParams>(daemonSetsRoute.pa
export const statefulSetsURL = buildURL<IStatefulSetsRouteParams>(statefulSetsRoute.path)
export const jobsURL = buildURL<IJobsRouteParams>(jobsRoute.path)
export const cronJobsURL = buildURL<ICronJobsRouteParams>(cronJobsRoute.path)
export const workloadURL: Partial<Record<KubeResource, ReturnType<typeof buildURL>>> = {
"pods": podsURL,
"deployments": deploymentsURL,
"daemonsets": daemonSetsURL,
"statefulsets": statefulSetsURL,
"jobs": jobsURL,
"cronjobs": cronJobsURL,
}

View File

@ -0,0 +1,17 @@
import { KubeObjectStore } from "../../kube-object.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { deploymentStore } from "../+workloads-deployments/deployments.store";
import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store";
import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { KubeResource } from "../../../common/rbac";
export const workloadStores: Partial<Record<KubeResource, KubeObjectStore>> = {
"pods": podsStore,
"deployments": deploymentStore,
"daemonsets": daemonSetStore,
"statefulsets": statefulSetStore,
"jobs": jobStore,
"cronjobs": cronJobStore,
}

View File

@ -13,6 +13,7 @@
:root {
--mainBackground: #1e2124;
--main-layout-header: 40px;
--drag-region-height: 22px
}
::selection {
@ -47,7 +48,7 @@ html, body {
left: 0;
top: 0;
width: 100%;
height: var(--main-layout-header);
height: var(--drag-region-height);
z-index: 1000;
pointer-events: none;
}

View File

@ -165,7 +165,7 @@ export const memoryOptions: ChartOptions = {
}
return bytesToUnits(parseInt(value));
}
return `${value}`;
return bytesToUnits(value);
},
stepSize: 1
}

View File

@ -55,25 +55,24 @@ export class ClusterManager extends React.Component {
render() {
return (
<div className="ClusterManager">
<div id="draggable-top"/>
<main>
<div id="lens-views"/>
<div id="lens-views" />
<Switch>
<Route component={LandingPage} {...landingRoute}/>
<Route component={Preferences} {...preferencesRoute}/>
<Route component={Workspaces} {...workspacesRoute}/>
<Route component={AddCluster} {...addClusterRoute}/>
<Route component={ClusterView} {...clusterViewRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={LandingPage} {...landingRoute} />
<Route component={Preferences} {...preferencesRoute} />
<Route component={Workspaces} {...workspacesRoute} />
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} />
<Route component={Extensions} {...extensionsRoute}/>
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact to={this.startUrl}/>
<Redirect exact to={this.startUrl} />
</Switch>
</main>
<ClustersMenu/>
<BottomBar/>
<ClustersMenu />
<BottomBar />
</div>
)
}

View File

@ -19,8 +19,11 @@ export async function initView(clusterId: ClusterId) {
if (!clusterId || lensViews.has(clusterId)) {
return;
}
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe");
iframe.name = cluster.contextName;
@ -42,9 +45,9 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
// Issue: https://github.com/lensapp/lens/issues/811
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
iframe.removeAttribute("src")
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`
iframe.removeAttribute("name")
iframe.contentWindow.postMessage("teardown", "*")
}
export function refreshViews() {

View File

@ -5,6 +5,7 @@ import { t } from "@lingui/macro";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
import { _i18n } from "../../i18n";
import { Notifications } from "../notifications";
export interface IChartInstallData {
name: string;
@ -27,45 +28,50 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
});
autorun(() => {
const { selectedTab, isOpen } = dockStore;
if (!isInstallChartTab(selectedTab)) return;
if (isOpen) {
this.loadData();
if (isInstallChartTab(selectedTab) && isOpen) {
this.loadData()
.catch(err => Notifications.error(String(err)))
}
}, { delay: 250 })
}
@action
async loadData(tabId = dockStore.selectedTabId) {
const { values } = this.getData(tabId);
const versions = this.versions.getData(tabId);
return Promise.all([
!versions && this.loadVersions(tabId),
!values && this.loadValues(tabId),
])
const promises = []
if (!this.getData(tabId).values) {
promises.push(this.loadValues(tabId))
}
if (!this.versions.getData(tabId)) {
promises.push(this.loadVersions(tabId))
}
await Promise.all(promises)
}
@action
async loadVersions(tabId: TabId) {
const { repo, name } = this.getData(tabId);
const { repo, name, version } = this.getData(tabId);
this.versions.clearData(tabId); // reset
const charts = await helmChartsApi.get(repo, name);
const charts = await helmChartsApi.get(repo, name, version);
const versions = charts.versions.map(chartVersion => chartVersion.version);
this.versions.setData(tabId, versions);
}
@action
async loadValues(tabId: TabId) {
const data = this.getData(tabId);
const { repo, name, version } = data;
let values = "";
const fetchValues = async (retry = 1, maxRetries = 3) => {
values = await helmChartsApi.getValues(repo, name, version);
if (values || retry == maxRetries) return;
await fetchValues(retry + 1);
};
this.setData(tabId, { ...data, values: undefined }); // reset
await fetchValues();
this.setData(tabId, { ...data, values });
const data = this.getData(tabId)
const { repo, name, version } = data
// This loop is for "retrying" the "getValues" call
for (const _ of Array(3)) {
const values = await helmChartsApi.getValues(repo, name, version)
if (values) {
this.setData(tabId, { ...data, values })
return
}
}
}
}

View File

@ -107,15 +107,15 @@ export class InstallChart extends Component<Props> {
render() {
const { tabId, chartData, values, versions, install } = this;
if (!chartData || chartData.values === undefined || !versions) {
return <Spinner center/>;
if (chartData?.values === undefined || !versions) {
return <Spinner center />;
}
if (this.releaseDetails) {
return (
<div className="InstallChartDone flex column gaps align-center justify-center">
<p>
<Icon material="check" big sticker/>
<Icon material="check" big sticker />
</p>
<p><Trans>Installation complete!</Trans></p>
<div className="flex gaps align-center">
@ -144,7 +144,7 @@ export class InstallChart extends Component<Props> {
const panelControls = (
<div className="install-controls flex gaps align-center">
<span><Trans>Chart</Trans></span>
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)}/>
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)} />
<span><Trans>Version</Trans></span>
<Select
className="chart-version"

View File

@ -282,7 +282,6 @@ export class Input extends React.Component<InputProps, State> {
onKeyDown: this.onKeyDown,
rows: multiLine ? (rows || 1) : null,
ref: this.bindRef,
type: "text",
spellCheck: "false",
});

View File

@ -116,6 +116,6 @@ export function openServiceAccountKubeConfig(account: ServiceAccount) {
const namespace = account.getNs()
KubeConfigDialog.open({
title: <Trans>{accountName} kubeconfig</Trans>,
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`)
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${accountName}`)
})
}

View File

@ -8,10 +8,16 @@ import { JsonApiErrorParsed } from "../../api/json-api";
export type IMessageId = string | number;
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
export enum NotificationStatus {
OK = "ok",
ERROR = "error",
INFO = "info",
}
export interface INotification {
id?: IMessageId;
message: IMessage;
status?: "ok" | "error" | "info";
status?: NotificationStatus;
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
}

View File

@ -5,7 +5,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"
import { JsonApiErrorParsed } from "../../api/json-api";
import { cssNames, prevDefault } from "../../utils";
import { IMessage, INotification, notificationsStore } from "./notifications.store";
import { IMessage, INotification, notificationsStore, NotificationStatus } from "./notifications.store";
import { Animate } from "../animate";
import { Icon } from "../icon"
@ -17,7 +17,7 @@ export class Notifications extends React.Component {
notificationsStore.add({
message: message,
timeout: 2500,
status: "ok"
status: NotificationStatus.OK
})
}
@ -25,13 +25,13 @@ export class Notifications extends React.Component {
notificationsStore.add({
message: message,
timeout: 10000,
status: "error"
status: NotificationStatus.ERROR
});
}
static info(message: IMessage, customOpts: Partial<INotification> = {}) {
return notificationsStore.add({
status: "info",
status: NotificationStatus.INFO,
timeout: 0,
message: message,
...customOpts,
@ -78,7 +78,7 @@ export class Notifications extends React.Component {
onMouseLeave={() => addAutoHideTimer(notification)}
onMouseEnter={() => removeAutoHideTimer(notification)}>
<div className="box center">
<Icon material="info_outline"/>
<Icon material="info_outline" />
</div>
<div className="message box grow">{msgText}</div>
<div className="box center">

View File

@ -32,6 +32,7 @@ html {
background: transparent;
min-height: 0;
box-shadow: 0 0 0 1px $halfGray;
cursor: pointer;
&--is-focused {
box-shadow: 0 0 0 2px $primary;
@ -88,6 +89,7 @@ html {
&__option {
white-space: nowrap;
cursor: pointer;
&:active {
background: $primary;

View File

@ -19,6 +19,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
kubeWatchApi.addListener(this, this.onWatchApiEvent);
}
getStatuses?(items: T[]): Record<string, number>;
getAllByNs(namespace: string | string[], strict = false): T[] {
const namespaces: string[] = [].concat(namespace);
if (namespaces.length) {

View File

@ -0,0 +1,28 @@
import { KubeResource } from "../../common/rbac";
import { _i18n } from "../i18n";
export const ResourceNames: Record<KubeResource, string> = {
"namespaces": _i18n._("Namespaces"),
"nodes": _i18n._("Nodes"),
"events": _i18n._("Events"),
"resourcequotas": _i18n._("Resource Quotas"),
"services": _i18n._("Services"),
"secrets": _i18n._("Secrets"),
"configmaps": _i18n._("Config Maps"),
"ingresses": _i18n._("Ingresses"),
"networkpolicies": _i18n._("Network Policies"),
"persistentvolumes": _i18n._("Persistent Volumes"),
"storageclasses": _i18n._("Storage Classes"),
"pods": _i18n._("Pods"),
"daemonsets": _i18n._("Daemon Sets"),
"deployments": _i18n._("Deployments"),
"statefulsets": _i18n._("Stateful Sets"),
"replicasets": _i18n._("Replica Sets"),
"jobs": _i18n._("Jobs"),
"cronjobs": _i18n._("Cron Jobs"),
"endpoints": _i18n._("Endpoints"),
"customresourcedefinitions": _i18n._("Custom Resource Definitions"),
"horizontalpodautoscalers": _i18n._("Horizontal Pod Autoscalers"),
"podsecuritypolicies": _i18n._("Pod Security Policies"),
"poddisruptionbudgets": _i18n._("Pod Disruption Budgets"),
}

View File

@ -2,7 +2,26 @@
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.4 (current version)
## 3.6.5-rc.1 (current version)
- Fix Notifications not to block items not visually under them from being interacted with
- Fix side bar not to scroll after clicking on lower menu item
- Auto-select context if only one context is present in pasted Kubeconfig
- Fix background image of What's New page on white theme
- Reduce height on draggable-top and only render it on macos
- Download dir option is now consistent with other settings
- Allow to add the same cluster multiple times
- Convert bytes in memory chart properly
- Fix empty dashboard screen after cluster is removed and added multiple times in a row to application
- Dropdowns have pointer cursor now
- Proxy kubectl exec requests properly
- Pass always chart version information when dealing with helm commands
- Fix app crash when conditions are not yet present in CRD objects
- Fix kubeconfig generating for service account
- Update bundled Helm binary to version 3.3.4
- Fix clusters' kubeconfig paths that point to snap config dir to use current snap config path
## 3.6.4
- Fix: deleted namespace does not get auto unselected
- Get focus to dock tab (terminal & resource editor) content after resize
- Downloading kubectl binary does not block dashboard opening anymore