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

Merge branch 'master' into previous-logs

This commit is contained in:
Nox 2020-08-29 14:29:14 +02:00 committed by GitHub
commit dd77d9be5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1238 additions and 326 deletions

View File

@ -128,8 +128,9 @@ jobs:
sudo apt-get install libgconf-2-4 conntrack -y
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
export CHANGE_MINIKUBE_NONE_USER=true
sudo minikube start --driver=none
# Although the kube and minikube config files are in placed $HOME they are owned by root
sudo chown -R $USER $HOME/.kube $HOME/.minikube
displayName: Install integration test dependencies
- script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
displayName: Run integration tests

View File

@ -2,7 +2,7 @@ import { Application } from "spectron"
import * as util from "../helpers/utils"
import { spawnSync } from "child_process"
jest.setTimeout(20000)
jest.setTimeout(30000)
const BACKSPACE = "\uE003"
@ -16,8 +16,8 @@ describe("app start", () => {
const addMinikubeCluster = async (app: Application) => {
await app.client.click("div.add-cluster")
await app.client.waitUntilTextExists("p", "Choose config")
await app.client.click("div#kubecontext-select")
await app.client.waitUntilTextExists("div", "Select kubeconfig file")
await app.client.click("div.Select__control")
await app.client.waitUntilTextExists("div", "minikube")
await app.client.click("div.minikube")
await app.client.click("button.primary")
@ -26,11 +26,8 @@ describe("app start", () => {
const waitForMinikubeDashboard = async (app: Application) => {
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started")
let windowCount = await app.client.getWindowCount()
// wait for webview to appear on window count
while (windowCount == 1) {
windowCount = await app.client.getWindowCount()
}
await app.client.windowByIndex(windowCount - 1)
await app.client.waitForExist(`iframe[name="minikube"]`)
await app.client.frame("minikube")
await app.client.waitUntilTextExists("span.link-text", "Cluster")
}
@ -39,10 +36,10 @@ describe("app start", () => {
await app.start()
await app.client.waitUntilWindowLoaded()
let windowCount = await app.client.getWindowCount()
while (windowCount > 1) {
while (windowCount > 1) { // Wait for splash screen to be closed
windowCount = await app.client.getWindowCount()
}
await app.client.windowByIndex(windowCount - 1)
await app.client.windowByIndex(0)
await app.client.waitUntilWindowLoaded()
}, 20000)
@ -60,7 +57,7 @@ describe("app start", () => {
await addMinikubeCluster(app)
await waitForMinikubeDashboard(app)
await app.client.click('a[href="/nodes"]')
await app.client.waitUntilTextExists("div.TableCell", "minikube")
await app.client.waitUntilTextExists("div.TableCell", "Ready")
})
it('allows to create a pod', async () => {
@ -75,7 +72,7 @@ describe("app start", () => {
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods")
await app.client.click('a[href="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver-minikube")
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
await app.client.click('.Icon.new-dock-tab')
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource")
await app.client.click("li.MenuItem.create-resource-tab")

View File

@ -37,6 +37,10 @@ msgstr "(empty) (Allowing the specific traffic to all pods in this namespace)"
#~ msgid "(new)"
#~ msgstr "(new)"
#: src/renderer/components/+add-cluster/add-cluster.tsx:213
#~ msgid "* Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#~ msgstr "* Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#: src/renderer/components/item-object-list/item-list-layout.tsx:224
msgid "<0>Filtered</0>: {itemsCount} / {allItemsCount}"
msgstr "<0>Filtered</0>: {itemsCount} / {allItemsCount}"
@ -108,7 +112,7 @@ msgstr "Add bindings to {name}"
#~ msgid "Add cluster"
#~ msgstr "Add cluster"
#: src/renderer/components/+add-cluster/add-cluster.tsx:192
#: src/renderer/components/+add-cluster/add-cluster.tsx:320
msgid "Add cluster(s)"
msgstr "Add cluster(s)"
@ -128,6 +132,10 @@ msgstr "Add field"
#~ msgid "Added repos:"
#~ msgstr "Added repos:"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Adding clusters: <0>{0}</0>"
#~ msgstr "Adding clusters: <0>{0}</0>"
#: src/renderer/components/+preferences/preferences.tsx:103
msgid "Adding helm branch <0>{0}</0> has failed: {1}"
msgstr "Adding helm branch <0>{0}</0> has failed: {1}"
@ -285,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:106
#: src/renderer/components/cluster-manager/clusters-menu.tsx:108
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."
@ -315,6 +323,10 @@ msgstr "Binding targets"
msgid "Bindings"
msgstr "Bindings"
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
msgid "Browse"
msgstr "Browse"
#: src/renderer/components/error-boundary/error-boundary.tsx:37
#~ msgid "Build version"
#~ msgstr "Build version"
@ -440,6 +452,18 @@ msgstr "Charts"
#~ msgid "Checking update"
#~ msgstr "Checking update"
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#~ msgstr "Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or from manually pasted configuration contents"
#~ msgstr "Choose how to import clusters: from selected kube-config file or from manually pasted configuration contents"
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or from pasted yaml configuration"
#~ msgstr "Choose how to import clusters: from selected kube-config file or from pasted yaml configuration"
#: src/renderer/components/+storage-volumes/volume-details.tsx:68
#: src/renderer/components/+storage-volumes/volumes.tsx:43
msgid "Claim"
@ -580,6 +604,14 @@ msgstr "Containers"
msgid "Context"
msgstr "Context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Contexts: <0>{0}</0>"
#~ msgstr "Contexts: <0>{0}</0>"
#: src/renderer/components/+add-cluster/add-cluster.tsx:249
#~ msgid "Contexts: {0}"
#~ msgstr "Contexts: {0}"
#: src/renderer/components/+workloads-pods/pods.tsx:79
#: src/renderer/components/kube-object/kube-object-meta.tsx:39
msgid "Controlled By"
@ -710,9 +742,9 @@ msgstr "Currently applied filters:"
msgid "Custom Resources"
msgstr "Custom Resources"
#: src/renderer/components/+add-cluster/add-cluster.tsx:116
msgid "Custom.."
msgstr "Custom.."
#: src/renderer/components/+add-cluster/add-cluster.tsx:155
#~ msgid "Custom.."
#~ msgstr "Custom.."
#: src/renderer/components/+custom-resources/certmanager.k8s.io/certificate-details.tsx:95
msgid "DNS Provider"
@ -883,6 +915,10 @@ msgstr "Environment"
msgid "Error stack"
msgstr "Error stack"
#: src/renderer/components/+add-cluster/add-cluster.tsx:109
msgid "Error while adding cluster(s): {0}"
msgstr "Error while adding cluster(s): {0}"
#: src/renderer/components/+events/events.tsx:56
#: src/renderer/components/+events/kube-event-details.tsx:34
#: src/renderer/components/+events/kube-event-details.tsx:39
@ -1586,6 +1622,14 @@ msgstr "No"
msgid "No Nodes Available."
msgstr "No Nodes Available."
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
#~ 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
msgid "No contexts available or they have been added already"
msgstr "No contexts available or they have been added already"
#: src/renderer/components/item-object-list/page-filters-select.tsx:84
msgid "No filters available."
msgstr "No filters available."
@ -1714,6 +1758,10 @@ msgstr "Parallelism"
msgid "Parameters"
msgstr "Parameters"
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
msgid "Paste as text"
msgstr "Paste as text"
#: src/renderer/components/+custom-resources/certmanager.k8s.io/issuer-details.tsx:94
#: src/renderer/components/+custom-resources/certmanager.k8s.io/issuer-details.tsx:102
#: src/renderer/components/+network-ingresses/ingress-details.tsx:42
@ -1734,9 +1782,29 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:72
msgid "Please select at least one cluster context"
msgstr "Please select at least one cluster context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:146
#~ msgid "Please select at least one context to add a cluster"
#~ msgstr "Please select at least one context to add a cluster"
#: src/renderer/components/+add-cluster/add-cluster.tsx:106
#~ msgid "Please select kube-config's context"
#~ msgstr "Please select kube-config's context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig"
msgstr "Please select kubeconfig"
#~ msgid "Please select kubeconfig"
#~ msgstr "Please select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
#~ msgid "Please select kubeconfig context"
#~ msgstr "Please select kubeconfig context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:106
#~ msgid "Please select kubeconfig's context"
#~ msgstr "Please select kubeconfig's context"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50
msgid "Pod"
@ -1823,6 +1891,34 @@ msgstr "Private Key Secret"
msgid "Privileged"
msgstr "Privileged"
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig (text/yaml) to get available contexts"
#~ msgstr "Pro-Tip: paste kubeconfig (text/yaml) to get available contexts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ 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
msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr "Pro-Tip: paste kubeconfig to get available contexts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ 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
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"
#: src/renderer/components/+add-cluster/add-cluster.tsx:225
#~ msgid "Pro-tip: you can also drag-n-drop kube-config file in the left-side area"
#~ msgstr "Pro-tip: you can also drag-n-drop kube-config file in the left-side area"
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
#~ msgid "Pro-tip: you can also drag-n-drop kube-config file to this area"
#~ msgstr "Pro-tip: you can also drag-n-drop kube-config file to this area"
#: src/renderer/components/+storage-classes/storage-class-details.tsx:28
#: src/renderer/components/+storage-classes/storage-classes.tsx:35
msgid "Provisioner"
@ -1832,7 +1928,7 @@ msgstr "Provisioner"
msgid "Proxy is used only for non-cluster communication."
msgstr "Proxy is used only for non-cluster communication."
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/+add-cluster/add-cluster.tsx:308
msgid "Proxy settings"
msgstr "Proxy settings"
@ -2013,6 +2109,7 @@ msgstr "Required Drop Capabilities"
msgid "Required field"
msgstr "Required field"
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset"
msgstr "Reset"
@ -2021,6 +2118,18 @@ msgstr "Reset"
msgid "Reset filters?"
msgstr "Reset filters?"
#: src/renderer/components/+add-cluster/add-cluster.tsx:65
#~ msgid "Resetting config to {0}"
#~ msgstr "Resetting config to {0}"
#: src/renderer/components/+add-cluster/add-cluster.tsx:68
#~ msgid "Resetting kube-config to current {0}"
#~ msgstr "Resetting kube-config to current {0}"
#: src/renderer/components/+add-cluster/add-cluster.tsx:68
#~ msgid "Resetting kube-config to default: {kubeConfigDefaultPath}"
#~ msgstr "Resetting kube-config to default: {kubeConfigDefaultPath}"
#: src/renderer/components/+custom-resources/crd-details.tsx:44
#: src/renderer/components/+custom-resources/crd-list.tsx:73
msgid "Resource"
@ -2216,13 +2325,59 @@ msgstr "Secret type"
msgid "Secrets"
msgstr "Secrets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:253
#~ msgid "Select a context"
#~ msgstr "Select a context"
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.."
msgstr "Select a quota.."
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig"
msgstr "Select kubeconfig"
#~ msgid "Select context"
#~ msgstr "Select context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
#~ 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:272
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
#~ msgid "Select custom kube-config file"
#~ msgstr "Select custom kube-config file"
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
msgid "Select custom kubeconfig file"
msgstr "Select custom kubeconfig file"
#: src/renderer/components/+add-cluster/add-cluster.tsx:212
#~ msgid "Select file"
#~ msgstr "Select file"
#: src/renderer/components/+add-cluster/add-cluster.tsx:221
#~ msgid "Select kube-config file"
#~ msgstr "Select kube-config file"
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr "Select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
msgid "Select kubeconfig file"
msgstr "Select kubeconfig file"
#: src/renderer/components/+add-cluster/add-cluster.tsx:224
#~ msgid "Select or drop file"
#~ msgstr "Select or drop file"
#: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository"
@ -2236,6 +2391,22 @@ msgstr "Select role.."
msgid "Select service accounts"
msgstr "Select service accounts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Selected clusters: <0>{0}</0>"
#~ msgstr "Selected clusters: <0>{0}</0>"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr "Selected contexts ({0}): <0>{1}</0>"
#: src/renderer/components/+add-cluster/add-cluster.tsx:271
msgid "Selected contexts: <0>{0}</0>"
msgstr "Selected contexts: <0>{0}</0>"
#: src/renderer/components/+add-cluster/add-cluster.tsx:246
#~ msgid "Selected contexts: {0}"
#~ msgstr "Selected contexts: {0}"
#: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:27
#: src/renderer/components/+network-services/service-details.tsx:37
#: src/renderer/components/+network-services/services.tsx:50
@ -2420,6 +2591,10 @@ msgstr "Submitting.."
msgid "Subsets"
msgstr "Subsets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:102
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "Successfully imported <0>{0}</0> cluster(s)"
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:128
msgid "Supplemental Groups"
msgstr "Supplemental Groups"
@ -2466,7 +2641,7 @@ msgstr "There are no logs available."
msgid "This field is required"
msgstr "This field is required"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:104
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
msgid "This is the quick launch menu."
msgstr "This is the quick launch menu."
@ -2580,6 +2755,11 @@ msgstr "Upgrade version"
msgid "Usage"
msgstr "Usage"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Use configuration"
msgstr "Use configuration"
#: src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx:190
msgid "Use same name for RoleBinding"
msgstr "Use same name for RoleBinding"

View File

@ -37,6 +37,10 @@ msgstr ""
#~ msgid "(new)"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:213
#~ msgid "* Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#~ msgstr ""
#: src/renderer/components/item-object-list/item-list-layout.tsx:224
msgid "<0>Filtered</0>: {itemsCount} / {allItemsCount}"
msgstr ""
@ -108,7 +112,7 @@ msgstr ""
#~ msgid "Add cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:192
#: src/renderer/components/+add-cluster/add-cluster.tsx:320
msgid "Add cluster(s)"
msgstr ""
@ -128,6 +132,10 @@ msgstr ""
#~ msgid "Added repos:"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Adding clusters: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:103
msgid "Adding helm branch <0>{0}</0> has failed: {1}"
msgstr ""
@ -285,7 +293,7 @@ msgstr ""
msgid "Arguments"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
#: src/renderer/components/cluster-manager/clusters-menu.tsx:108
msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button."
msgstr ""
@ -315,6 +323,10 @@ msgstr ""
msgid "Bindings"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
msgid "Browse"
msgstr ""
#: src/renderer/components/error-boundary/error-boundary.tsx:37
#~ msgid "Build version"
#~ msgstr ""
@ -436,6 +448,18 @@ msgstr ""
msgid "Charts"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or from manually pasted configuration contents"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or from pasted yaml configuration"
#~ msgstr ""
#: src/renderer/components/+storage-volumes/volume-details.tsx:68
#: src/renderer/components/+storage-volumes/volumes.tsx:43
msgid "Claim"
@ -576,6 +600,14 @@ msgstr ""
msgid "Context"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Contexts: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:249
#~ msgid "Contexts: {0}"
#~ msgstr ""
#: src/renderer/components/+workloads-pods/pods.tsx:79
#: src/renderer/components/kube-object/kube-object-meta.tsx:39
msgid "Controlled By"
@ -706,9 +738,9 @@ msgstr ""
msgid "Custom Resources"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:116
msgid "Custom.."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:155
#~ msgid "Custom.."
#~ msgstr ""
#: src/renderer/components/+custom-resources/certmanager.k8s.io/certificate-details.tsx:95
msgid "DNS Provider"
@ -879,6 +911,10 @@ msgstr ""
msgid "Error stack"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:109
msgid "Error while adding cluster(s): {0}"
msgstr ""
#: src/renderer/components/+events/events.tsx:56
#: src/renderer/components/+events/kube-event-details.tsx:34
#: src/renderer/components/+events/kube-event-details.tsx:39
@ -1569,6 +1605,14 @@ msgstr ""
msgid "No Nodes Available."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
#~ msgid "No contexts available or they already added"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
msgid "No contexts available or they have been added already"
msgstr ""
#: src/renderer/components/item-object-list/page-filters-select.tsx:84
msgid "No filters available."
msgstr ""
@ -1697,6 +1741,10 @@ msgstr ""
msgid "Parameters"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
msgid "Paste as text"
msgstr ""
#: src/renderer/components/+custom-resources/certmanager.k8s.io/issuer-details.tsx:94
#: src/renderer/components/+custom-resources/certmanager.k8s.io/issuer-details.tsx:102
#: src/renderer/components/+network-ingresses/ingress-details.tsx:42
@ -1717,10 +1765,30 @@ msgstr ""
msgid "Persistent Volumes"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:72
msgid "Please select at least one cluster context"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:146
#~ msgid "Please select at least one context to add a cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:106
#~ msgid "Please select kube-config's context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
#~ msgid "Please select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
#~ msgid "Please select kubeconfig context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:106
#~ msgid "Please select kubeconfig's context"
#~ msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50
msgid "Pod"
msgstr ""
@ -1806,6 +1874,34 @@ msgstr ""
msgid "Privileged"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig (text/yaml) to get available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:263
msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:254
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:225
#~ msgid "Pro-tip: you can also drag-n-drop kube-config file in the left-side area"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
#~ msgid "Pro-tip: you can also drag-n-drop kube-config file to this area"
#~ msgstr ""
#: src/renderer/components/+storage-classes/storage-class-details.tsx:28
#: src/renderer/components/+storage-classes/storage-classes.tsx:35
msgid "Provisioner"
@ -1815,7 +1911,7 @@ msgstr ""
msgid "Proxy is used only for non-cluster communication."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/+add-cluster/add-cluster.tsx:308
msgid "Proxy settings"
msgstr ""
@ -1996,6 +2092,7 @@ msgstr ""
msgid "Required field"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset"
msgstr ""
@ -2004,6 +2101,18 @@ msgstr ""
msgid "Reset filters?"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:65
#~ msgid "Resetting config to {0}"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:68
#~ msgid "Resetting kube-config to current {0}"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:68
#~ msgid "Resetting kube-config to default: {kubeConfigDefaultPath}"
#~ msgstr ""
#: src/renderer/components/+custom-resources/crd-details.tsx:44
#: src/renderer/components/+custom-resources/crd-list.tsx:73
msgid "Resource"
@ -2199,14 +2308,60 @@ msgstr ""
msgid "Secrets"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:253
#~ msgid "Select a context"
#~ msgstr ""
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig"
#~ msgid "Select context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
#~ 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:272
msgid "Select contexts (available: {0})"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#~ msgid "Select custom kube-config file"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
msgid "Select custom kubeconfig file"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:212
#~ msgid "Select file"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:221
#~ msgid "Select kube-config file"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
msgid "Select kubeconfig file"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:224
#~ msgid "Select or drop file"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository"
#~ msgstr ""
@ -2219,6 +2374,22 @@ msgstr ""
msgid "Select service accounts"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Selected clusters: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:271
msgid "Selected contexts: <0>{0}</0>"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:246
#~ msgid "Selected contexts: {0}"
#~ msgstr ""
#: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:27
#: src/renderer/components/+network-services/service-details.tsx:37
#: src/renderer/components/+network-services/services.tsx:50
@ -2403,6 +2574,10 @@ msgstr ""
msgid "Subsets"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:102
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr ""
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:128
msgid "Supplemental Groups"
msgstr ""
@ -2449,7 +2624,7 @@ msgstr ""
msgid "This field is required"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:104
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
msgid "This is the quick launch menu."
msgstr ""
@ -2563,6 +2738,11 @@ msgstr ""
msgid "Usage"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Use configuration"
msgstr ""
#: src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx:190
msgid "Use same name for RoleBinding"
msgstr ""

View File

@ -38,6 +38,10 @@ msgstr "(Пусто) (Допускается трафик ко всем пода
#~ msgid "(new)"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:213
#~ msgid "* Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#~ msgstr ""
#: src/renderer/components/item-object-list/item-list-layout.tsx:224
msgid "<0>Filtered</0>: {itemsCount} / {allItemsCount}"
msgstr "<0>Отфильтровано</0>: {itemsCount} / {allItemsCount}"
@ -109,7 +113,7 @@ msgstr "Добавить привязки к {name}"
#~ msgid "Add cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:192
#: src/renderer/components/+add-cluster/add-cluster.tsx:320
msgid "Add cluster(s)"
msgstr ""
@ -129,6 +133,10 @@ msgstr "Добавить поле"
#~ msgid "Added repos:"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Adding clusters: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:103
msgid "Adding helm branch <0>{0}</0> has failed: {1}"
msgstr ""
@ -286,7 +294,7 @@ msgstr "Выполнить команду drain для ноды <0>{nodeName}</0
msgid "Arguments"
msgstr "Аргументы"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
#: src/renderer/components/cluster-manager/clusters-menu.tsx:108
msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button."
msgstr ""
@ -316,6 +324,10 @@ msgstr "Цели привязки"
msgid "Bindings"
msgstr "Привязки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
msgid "Browse"
msgstr ""
#: src/renderer/components/error-boundary/error-boundary.tsx:37
#~ msgid "Build version"
#~ msgstr "Версия билда"
@ -441,6 +453,18 @@ msgstr "Чарты"
#~ msgid "Checking update"
#~ msgstr "Проверка обновлений"
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or by manually pasting kube-config's content as a text"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or from manually pasted configuration contents"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:218
#~ msgid "Choose how to import clusters: from selected kube-config file or from pasted yaml configuration"
#~ msgstr ""
#: src/renderer/components/+storage-volumes/volume-details.tsx:68
#: src/renderer/components/+storage-volumes/volumes.tsx:43
msgid "Claim"
@ -581,6 +605,14 @@ msgstr "Контейнеры"
msgid "Context"
msgstr "Контекст"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Contexts: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:249
#~ msgid "Contexts: {0}"
#~ msgstr ""
#: src/renderer/components/+workloads-pods/pods.tsx:79
#: src/renderer/components/kube-object/kube-object-meta.tsx:39
msgid "Controlled By"
@ -711,9 +743,9 @@ msgstr "Текущие фильтры:"
msgid "Custom Resources"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:116
msgid "Custom.."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:155
#~ msgid "Custom.."
#~ msgstr ""
#: src/renderer/components/+custom-resources/certmanager.k8s.io/certificate-details.tsx:95
msgid "DNS Provider"
@ -884,6 +916,10 @@ msgstr "Среда"
msgid "Error stack"
msgstr "Стэк ошибки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:109
msgid "Error while adding cluster(s): {0}"
msgstr ""
#: src/renderer/components/+events/events.tsx:56
#: src/renderer/components/+events/kube-event-details.tsx:34
#: src/renderer/components/+events/kube-event-details.tsx:39
@ -1587,6 +1623,14 @@ msgstr "Нет"
msgid "No Nodes Available."
msgstr "Нет доступных нод."
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
#~ msgid "No contexts available or they already added"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:275
msgid "No contexts available or they have been added already"
msgstr ""
#: src/renderer/components/item-object-list/page-filters-select.tsx:84
msgid "No filters available."
msgstr "Нет доступных фильтров."
@ -1715,6 +1759,10 @@ msgstr "Параллелизм"
msgid "Parameters"
msgstr "Параметры"
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
msgid "Paste as text"
msgstr ""
#: src/renderer/components/+custom-resources/certmanager.k8s.io/issuer-details.tsx:94
#: src/renderer/components/+custom-resources/certmanager.k8s.io/issuer-details.tsx:102
#: src/renderer/components/+network-ingresses/ingress-details.tsx:42
@ -1735,10 +1783,30 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:72
msgid "Please select at least one cluster context"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:146
#~ msgid "Please select at least one context to add a cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:106
#~ msgid "Please select kube-config's context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
#~ msgid "Please select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
#~ msgid "Please select kubeconfig context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:106
#~ msgid "Please select kubeconfig's context"
#~ msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50
msgid "Pod"
msgstr ""
@ -1824,6 +1892,34 @@ msgstr "Секрет приватного ключа"
msgid "Privileged"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig (text/yaml) to get available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:263
msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:264
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:254
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:225
#~ msgid "Pro-tip: you can also drag-n-drop kube-config file in the left-side area"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
#~ msgid "Pro-tip: you can also drag-n-drop kube-config file to this area"
#~ msgstr ""
#: src/renderer/components/+storage-classes/storage-class-details.tsx:28
#: src/renderer/components/+storage-classes/storage-classes.tsx:35
msgid "Provisioner"
@ -1833,7 +1929,7 @@ msgstr "Комиссия"
msgid "Proxy is used only for non-cluster communication."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/+add-cluster/add-cluster.tsx:308
msgid "Proxy settings"
msgstr ""
@ -2014,6 +2110,7 @@ msgstr ""
msgid "Required field"
msgstr "Обязательное поле"
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset"
msgstr "Сбросить"
@ -2022,6 +2119,18 @@ msgstr "Сбросить"
msgid "Reset filters?"
msgstr "Сбросить фильтры?"
#: src/renderer/components/+add-cluster/add-cluster.tsx:65
#~ msgid "Resetting config to {0}"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:68
#~ msgid "Resetting kube-config to current {0}"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:68
#~ msgid "Resetting kube-config to default: {kubeConfigDefaultPath}"
#~ msgstr ""
#: src/renderer/components/+custom-resources/crd-details.tsx:44
#: src/renderer/components/+custom-resources/crd-list.tsx:73
msgid "Resource"
@ -2217,14 +2326,60 @@ msgstr "Тип секрета"
msgid "Secrets"
msgstr "Secrets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:253
#~ msgid "Select a context"
#~ msgstr ""
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.."
msgstr "Выберите квоту..."
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig"
#~ msgid "Select context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:245
#~ 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:272
msgid "Select contexts (available: {0})"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#: src/renderer/components/+add-cluster/add-cluster.tsx:76
#~ msgid "Select custom kube-config file"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
msgid "Select custom kubeconfig file"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:212
#~ msgid "Select file"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:221
#~ msgid "Select kube-config file"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
msgid "Select kubeconfig file"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:224
#~ msgid "Select or drop file"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository"
#~ msgstr ""
@ -2237,6 +2392,22 @@ msgstr "Выбрать роль.."
msgid "Select service accounts"
msgstr "Выбрать сервисные аккаунты"
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Selected clusters: <0>{0}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:244
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:271
msgid "Selected contexts: <0>{0}</0>"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:246
#~ msgid "Selected contexts: {0}"
#~ msgstr ""
#: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:27
#: src/renderer/components/+network-services/service-details.tsx:37
#: src/renderer/components/+network-services/services.tsx:50
@ -2421,6 +2592,10 @@ msgstr "Применение.."
msgid "Subsets"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:102
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr ""
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:128
msgid "Supplemental Groups"
msgstr ""
@ -2467,7 +2642,7 @@ msgstr "Логи отсутствуют."
msgid "This field is required"
msgstr "Это обязательное поле"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:104
#: src/renderer/components/cluster-manager/clusters-menu.tsx:106
msgid "This is the quick launch menu."
msgstr ""
@ -2581,6 +2756,11 @@ msgstr "Обновить версию"
msgid "Usage"
msgstr "Использование"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Use configuration"
msgstr ""
#: src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx:190
msgid "Use same name for RoleBinding"
msgstr "Использовать тоже имя для привязки ролей"

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.0-dev",
"version": "3.6.0-beta.1",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",

View File

@ -1,5 +1,6 @@
import type { WorkspaceId } from "./workspace-store";
import { ipcRenderer } from "electron";
import path from "path";
import { app, ipcRenderer, remote } from "electron";
import { unlink } from "fs-extra";
import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
@ -7,6 +8,9 @@ import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store"
import logger from "../main/logger";
import { tracker } from "./tracker";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
export interface ClusterIconUpload {
clusterId: string;
@ -49,6 +53,17 @@ export interface ClusterPreferences {
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
static getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
}
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
saveToAppFiles(filePath, fileContents);
return filePath;
}
private constructor() {
super({
configName: "lens-cluster-store",
@ -81,6 +96,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.activeClusterId === id;
}
@action
setActive(id: ClusterId) {
this.activeClusterId = id;
}
@ -102,12 +118,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
@action
async addCluster(model: ClusterModel, activate = true): Promise<Cluster> {
tracker.event("cluster", "add");
const cluster = new Cluster(model);
this.clusters.set(model.id, cluster);
if (activate) this.activeClusterId = model.id;
return cluster;
addCluster(...models: ClusterModel[]) {
models.forEach(model => {
tracker.event("cluster", "add");
const cluster = new Cluster(model);
this.clusters.set(model.id, cluster);
})
}
@action
@ -119,7 +135,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
if (this.activeClusterId === clusterId) {
this.activeClusterId = null;
}
unlink(cluster.kubeConfigPath).catch(() => null);
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
unlink(cluster.kubeConfigPath).catch(() => null);
}
}
}

View File

@ -4,7 +4,6 @@ import yaml from "js-yaml";
import { Cluster } from "../main/cluster";
import { ClusterStore } from "./cluster-store";
import { workspaceStore } from "./workspace-store";
import { saveConfigToAppFiles } from "./kube-helpers";
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png")
@ -37,7 +36,7 @@ describe("empty config", () => {
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube"
},
kubeConfigPath: saveConfigToAppFiles("foo", "fancy foo config"),
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"),
workspace: workspaceStore.currentWorkspaceId
});
clusterStore.addCluster(cluster);
@ -58,7 +57,7 @@ describe("empty config", () => {
preferences: {
clusterName: "prod"
},
kubeConfigPath: saveConfigToAppFiles("prod", "fancy config"),
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"),
workspace: "workstation"
});
const devCluster = new Cluster({
@ -66,7 +65,7 @@ describe("empty config", () => {
preferences: {
clusterName: "dev"
},
kubeConfigPath: saveConfigToAppFiles("dev", "fancy config"),
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"),
workspace: "workstation"
});
clusterStore.addCluster(prodCluster);
@ -84,17 +83,13 @@ describe("empty config", () => {
expect(wsClusters[1].id).toBe("dev");
})
it("checks if last added cluster becomes active", () => {
expect(clusterStore.activeCluster.id).toBe("dev");
})
it("sets active cluster", () => {
clusterStore.setActive("foo");
expect(clusterStore.activeCluster.id).toBe("foo");
})
it("check if cluster's kubeconfig file saved", () => {
const file = saveConfigToAppFiles("boo", "kubeconfig");
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
})

View File

@ -1,11 +1,12 @@
import { app, remote } from "electron";
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
import fse, { ensureDirSync, readFile, writeFileSync } from "fs-extra";
import fse from "fs-extra";
import path from "path"
import os from "os"
import yaml from "js-yaml"
import logger from "../main/logger";
export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config');
function resolveTilde(filePath: string) {
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
return filePath.replace("~", os.homedir());
@ -139,29 +140,3 @@ export function getNodeWarningConditions(node: V1Node) {
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
)
}
// Write kubeconfigs to "embedded" store, i.e. "/Users/ixrock/Library/Application Support/Lens/kubeconfigs"
export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig | string): string {
const userData = (app || remote.app).getPath("userData");
const kubeConfigFile = path.join(userData, `kubeconfigs/${clusterId}`)
const kubeConfigContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
ensureDirSync(path.dirname(kubeConfigFile));
writeFileSync(kubeConfigFile, kubeConfigContents);
return kubeConfigFile;
}
export async function getKubeConfigLocal(): Promise<string> {
try {
const configFile = path.join(os.homedir(), '.kube', 'config');
const file = await readFile(configFile, "utf8");
const obj = yaml.safeLoad(file);
if (obj.contexts) {
obj.contexts = obj.contexts.filter((ctx: any) => ctx?.context?.cluster && ctx?.name)
}
return yaml.safeDump(obj);
} catch (err) {
logger.debug(`Cannot read local kube-config: ${err}`)
return "";
}
}

View File

@ -1,13 +1,16 @@
import type { ThemeId } from "../renderer/theme.store";
import semver from "semver"
import { readFile } from "fs-extra"
import { action, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store"
import { getAppVersion } from "./utils/app-version";
import { getKubeConfigLocal, loadConfig } from "./kube-helpers";
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { tracker } from "./tracker";
import logger from "../main/logger";
export interface UserStoreModel {
kubeConfigPath: string;
lastSeenAppVersion: string;
seenContexts: string[];
preferences: UserPreferences;
@ -37,10 +40,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
// refresh new contexts
this.whenLoaded.then(this.refreshNewContexts);
reaction(() => this.seenContexts.size, this.refreshNewContexts);
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
}
@observable lastSeenAppVersion = "0.0.0"
@observable kubeConfigPath = kubeConfigDefaultPath; // used in add-cluster page for providing context
@observable seenContexts = observable.set<string>();
@observable newContexts = observable.set<string>();
@ -55,6 +59,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}
@action
resetKubeConfigPath() {
this.kubeConfigPath = kubeConfigDefaultPath;
}
@action
resetTheme() {
this.preferences.colorTheme = UserStore.defaultTheme;
@ -67,14 +76,18 @@ export class UserStore extends BaseStore<UserStoreModel> {
}
protected refreshNewContexts = async () => {
const kubeConfig = await getKubeConfigLocal();
if (kubeConfig) {
this.newContexts.clear();
const localContexts = loadConfig(kubeConfig).getContexts();
localContexts
.filter(ctx => ctx.cluster)
.filter(ctx => !this.seenContexts.has(ctx.name))
.forEach(ctx => this.newContexts.add(ctx.name));
try {
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
if (kubeConfig) {
this.newContexts.clear();
loadConfig(kubeConfig).getContexts()
.filter(ctx => ctx.cluster)
.filter(ctx => !this.seenContexts.has(ctx.name))
.forEach(ctx => this.newContexts.add(ctx.name));
}
} catch (err) {
logger.error(err);
this.resetKubeConfigPath();
}
}
@ -86,17 +99,21 @@ export class UserStore extends BaseStore<UserStoreModel> {
}
@action
protected fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences } = data
protected async fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data
if (lastSeenAppVersion) {
this.lastSeenAppVersion = lastSeenAppVersion;
}
if (kubeConfigPath) {
this.kubeConfigPath = kubeConfigPath;
}
this.seenContexts.replace(seenContexts);
Object.assign(this.preferences, preferences);
}
toJSON(): UserStoreModel {
const model: UserStoreModel = {
kubeConfigPath: this.kubeConfigPath,
lastSeenAppVersion: this.lastSeenAppVersion,
seenContexts: Array.from(this.seenContexts),
preferences: this.preferences,

View File

@ -0,0 +1,11 @@
// Save file to electron app directory (e.g. "/Users/$USER/Library/Application Support/Lens" for MacOS)
import path from "path";
import { app, remote } from "electron";
import { ensureDirSync, writeFileSync } from "fs-extra";
export function saveToAppFiles(filePath: string, contents: any): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
ensureDirSync(path.dirname(absPath));
writeFileSync(absPath, contents);
return absPath;
}

View File

@ -1,6 +1,8 @@
import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store"
import { landingURL } from "../renderer/components/+landing-page/landing-page.route";
import { navigate } from "../renderer/navigation";
export type WorkspaceId = string;
@ -50,12 +52,18 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
}
@action
setActive(id = WorkspaceStore.defaultId) {
setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) {
if (id === this.currentWorkspaceId) return;
if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`);
}
this.currentWorkspaceId = id;
if (resetActiveCluster) {
clusterStore.setActive(null)
}
if (redirectToLanding) {
navigate(landingURL())
}
}
@action

View File

@ -27,6 +27,7 @@ export interface ClusterState extends ClusterModel {
online: boolean;
disconnected: boolean;
accessible: boolean;
ready: boolean;
failureReason: string;
nodes: number;
eventCount: number;
@ -47,6 +48,7 @@ export class Cluster implements ClusterModel {
protected eventDisposers: Function[] = [];
whenInitialized = when(() => this.initialized);
whenReady = when(() => this.ready);
@observable initialized = false;
@observable contextName: string;
@ -56,6 +58,7 @@ export class Cluster implements ClusterModel {
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable online: boolean;
@observable accessible: boolean;
@observable ready: boolean;
@observable disconnected: boolean;
@observable failureReason: string;
@observable nodes = 0;
@ -149,6 +152,7 @@ export class Cluster implements ClusterModel {
this.disconnected = true;
this.online = false;
this.accessible = false;
this.ready = false;
this.pushState();
}
@ -172,6 +176,7 @@ export class Cluster implements ClusterModel {
this.refreshEvents(),
this.refreshAllowedResources(),
]);
this.ready = true
}
}
@ -370,6 +375,7 @@ export class Cluster implements ClusterModel {
initialized: this.initialized,
apiUrl: this.apiUrl,
online: this.online,
ready: this.ready,
disconnected: this.disconnected,
accessible: this.accessible,
failureReason: this.failureReason,

View File

@ -20,12 +20,12 @@ export class ResourceApplier {
}
protected async kubectlApply(content: string): Promise<string> {
const { kubeCtl, kubeConfigPath } = this.cluster;
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath()
return new Promise<string>((resolve, reject) => {
const fileName = tempy.file({ name: "resource.yaml" })
fs.writeFileSync(fileName, content)
const cmd = `"${kubectlPath}" apply --kubeconfig "${kubeConfigPath}" -o json -f "${fileName}"`
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`
logger.debug("shooting manifests with: " + cmd);
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
const httpsProxy = this.cluster.preferences?.httpsProxy
@ -46,7 +46,7 @@ export class ResourceApplier {
}
public async kubectlApplyAll(resources: string[]): Promise<string> {
const { kubeCtl, kubeConfigPath } = this.cluster;
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath()
return new Promise((resolve, reject) => {
const tmpDir = tempy.directory()
@ -54,7 +54,7 @@ export class ResourceApplier {
resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
})
const cmd = `"${kubectlPath}" apply --kubeconfig "${kubeConfigPath}" -o json -f "${tmpDir}"`
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`
console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => {
if (error) {

View File

@ -28,7 +28,7 @@ export class ShellSession extends EventEmitter {
constructor(socket: WebSocket, cluster: Cluster) {
super()
this.websocket = socket
this.kubeconfigPath = cluster.kubeConfigPath
this.kubeconfigPath = cluster.getProxyKubeconfigPath()
this.kubectl = new Kubectl(cluster.version)
this.preferences = cluster.preferences || {}
this.clusterId = cluster.id

View File

@ -5,8 +5,8 @@ import path from "path"
import { app, remote } from "electron"
import { migration } from "../migration-wrapper";
import fse from "fs-extra"
import { ClusterModel } from "../../common/cluster-store";
import { loadConfig, saveConfigToAppFiles } from "../../common/kube-helpers";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { loadConfig } from "../../common/kube-helpers";
import makeSynchronous from "make-synchronous"
const AsyncFunction = Object.getPrototypeOf(async function () { return }).constructor;
@ -17,7 +17,7 @@ export default migration({
version: "3.6.0-beta.1",
run(store, printLog) {
const userDataPath = (app || remote.app).getPath("userData")
const kubeConfigBase = path.join(userDataPath, "kubeconfigs")
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
@ -31,7 +31,7 @@ export default migration({
*/
try {
// take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = saveConfigToAppFiles(cluster.id, cluster.kubeConfig)
cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext();
delete cluster.kubeConfig;

View File

@ -1,5 +1,39 @@
.AddCluster {
.droppable {
box-shadow: 0 0 0 5px inset $primary;
> * {
pointer-events: none;
}
}
.hint {
margin-top: -$padding;
color: $textColorSecondary;
> * {
vertical-align: middle;
}
}
.AceEditor {
min-height: 200px;
max-height: 400px;
}
.Select {
.kube-context {
--flex-gap: #{$padding};
}
// todo: extract to component, merge with namespace-select.scss
&__placeholder {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
@ -8,4 +42,4 @@
code {
color: $pink-400;
}
}
}

View File

@ -1,112 +1,163 @@
import "./add-cluster.scss"
import os from "os";
import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
import { action, observable, runInAction } from "mobx";
import { remote } from "electron";
import { KubeConfig } from "@kubernetes/client-node";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
import { Select, SelectOption } from "../select";
import { Input } from "../input";
import { AceEditor } from "../ace-editor";
import { Button } from "../button";
import { Icon } from "../icon";
import { WizardLayout } from "../layout/wizard-layout";
import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
import { clusterStore } from "../../../common/cluster-store";
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig } from "../../../common/kube-helpers";
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
import { v4 as uuid } from "uuid"
import { navigate } from "../../navigation";
import { userStore } from "../../../common/user-store";
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
import { Tab, Tabs } from "../tabs";
enum KubeConfigSourceTab {
FILE = "file",
TEXT = "text"
}
@observer
export class AddCluster extends React.Component {
readonly custom: any = "custom"
@observable.ref clusterConfig: KubeConfig;
@observable.ref kubeConfig: KubeConfig; // local ~/.kube/config (if available)
@observable.ref kubeConfigLocal: KubeConfig;
@observable.ref error: React.ReactNode;
@observable kubeContexts = observable.map<string, KubeConfig>(); // available contexts from kubeconfig-file or user-input
@observable selectedContexts = observable.array<string>();
@observable sourceTab = KubeConfigSourceTab.FILE;
@observable kubeConfigPath = "";
@observable customConfig = ""
@observable proxyServer = ""
@observable isWaiting = false
@observable showSettings = false
@observable proxyServer = ""
@observable customConfig = ""
@observable dropAreaActive = false;
async componentDidMount() {
const kubeConfig: string = await getKubeConfigLocal()
if (kubeConfig) {
this.kubeConfig = loadConfig(kubeConfig)
}
componentDidMount() {
this.setKubeConfig(userStore.kubeConfigPath);
}
componentWillUnmount() {
userStore.markNewContextsAsSeen();
}
@computed get isCustom() {
return this.clusterConfig === this.custom;
}
@computed get clusterOptions() {
const options: SelectOption<KubeConfig>[] = [];
if (this.kubeConfig) {
splitConfig(this.kubeConfig).forEach(kubeConfig => {
const context = kubeConfig.currentContext;
const hasContext = clusterStore.hasContext(context);
if (!hasContext) {
options.push({
value: kubeConfig,
label: context,
});
}
})
}
options.push({
label: <Trans>Custom..</Trans>,
value: this.custom,
});
return options;
}
protected formatClusterContextLabel = ({ value, label }: SelectOption<KubeConfig>) => {
if (value instanceof KubeConfig) {
const context = value.currentContext;
const isNew = userStore.newContexts.has(context);
const className = `${context} kube-context flex gaps align-center`
return (
<div className={className}>
<span>{context}</span>
{isNew && <Icon material="fiber_new"/>}
</div>
)
}
return label;
};
addCluster = async () => {
const { clusterConfig, customConfig, proxyServer } = this;
const clusterId = uuid();
this.isWaiting = true
this.error = ""
@action
setKubeConfig(filePath: string, { throwError = false } = {}) {
try {
const config = this.isCustom ? loadConfig(customConfig) : clusterConfig;
if (!config) {
this.error = <Trans>Please select kubeconfig</Trans>
this.kubeConfigLocal = loadConfig(filePath);
validateConfig(this.kubeConfigLocal);
this.refreshContexts();
this.kubeConfigPath = filePath;
userStore.kubeConfigPath = filePath; // save to store
} catch (err) {
Notifications.error(
<div>Can't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
);
if (throwError) {
throw err;
}
}
}
@action
refreshContexts() {
this.selectedContexts.clear();
this.kubeContexts.clear();
switch (this.sourceTab) {
case KubeConfigSourceTab.FILE:
const contexts = this.getContexts(this.kubeConfigLocal);
this.kubeContexts.replace(contexts);
break;
case KubeConfigSourceTab.TEXT:
try {
this.error = ""
const contexts = this.getContexts(loadConfig(this.customConfig || "{}"));
this.kubeContexts.replace(contexts);
} catch (err) {
this.error = String(err);
}
break;
}
}
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
}
selectKubeConfigDialog = async () => {
const { dialog, BrowserWindow } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
defaultPath: this.kubeConfigPath,
properties: ["openFile", "showHiddenFiles"],
message: _i18n._(t`Select custom kubeconfig file`),
buttonLabel: _i18n._(t`Use configuration`),
});
if (!canceled && filePaths.length) {
this.setKubeConfig(filePaths[0]);
}
}
addClusters = () => {
try {
if (!this.selectedContexts.length) {
this.error = <Trans>Please select at least one cluster context</Trans>
return;
}
validateConfig(config);
await clusterStore.addCluster({
id: clusterId,
kubeConfigPath: saveConfigToAppFiles(clusterId, config),
workspace: workspaceStore.currentWorkspaceId,
contextName: config.currentContext,
preferences: {
clusterName: config.currentContext,
httpsProxy: proxyServer || undefined,
},
this.error = ""
this.isWaiting = true
const newClusters: ClusterModel[] = this.selectedContexts.map(context => {
const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context);
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE
? this.kubeConfigPath // save link to original kubeconfig in file-system
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return {
id: clusterId,
kubeConfigPath: kubeConfigPath,
workspace: workspaceStore.currentWorkspaceId,
contextName: kubeConfig.currentContext,
preferences: {
clusterName: kubeConfig.currentContext,
httpsProxy: this.proxyServer || undefined,
},
}
});
navigate(clusterViewURL({ params: { clusterId } }))
runInAction(() => {
clusterStore.addCluster(...newClusters);
if (newClusters.length === 1) {
const clusterId = newClusters[0].id;
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
} else {
Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
);
}
})
this.refreshContexts();
} catch (err) {
this.error = String(err);
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
} finally {
this.isWaiting = false;
}
@ -163,19 +214,156 @@ export class AddCluster extends React.Component {
)
}
renderKubeConfigSource() {
return (
<>
<Tabs withBorder onChange={this.onKubeConfigTabChange}>
<Tab
value={KubeConfigSourceTab.FILE}
label={<Trans>Select kubeconfig file</Trans>}
active={this.sourceTab == KubeConfigSourceTab.FILE}/>
<Tab
value={KubeConfigSourceTab.TEXT}
label={<Trans>Paste as text</Trans>}
active={this.sourceTab == KubeConfigSourceTab.TEXT}
/>
</Tabs>
{this.sourceTab === KubeConfigSourceTab.FILE && (
<>
<div className="kube-config-select flex gaps align-center">
<Input
theme="round-black"
className="kube-config-path box grow"
value={this.kubeConfigPath}
onChange={v => this.kubeConfigPath = v}
onBlur={this.onKubeConfigInputBlur}
/>
{this.kubeConfigPath !== kubeConfigDefaultPath && (
<Icon
material="settings_backup_restore"
onClick={() => this.setKubeConfig(kubeConfigDefaultPath)}
tooltip={<Trans>Reset</Trans>}
/>
)}
<Icon
material="folder"
onClick={this.selectKubeConfigDialog}
tooltip={<Trans>Browse</Trans>}
/>
</div>
<small className="hint">
<Trans>Pro-Tip: you can also drag-n-drop kubeconfig file to this area</Trans>
</small>
</>
)}
{this.sourceTab === KubeConfigSourceTab.TEXT && (
<>
<AceEditor
autoFocus
showGutter={false}
mode="yaml"
value={this.customConfig}
onChange={value => {
this.customConfig = value;
this.refreshContexts();
}}
/>
<small className="hint">
<Trans>Pro-Tip: paste kubeconfig to get available contexts</Trans>
</small>
</>
)}
</>
)
}
renderContextSelector() {
const allContexts = Array.from(this.kubeContexts.keys());
const placeholder = this.selectedContexts.length > 0
? <Trans>Selected contexts: <b>{this.selectedContexts.length}</b></Trans>
: <Trans>Select contexts</Trans>;
return (
<>
<Select
id="kubecontext-select" // todo: provide better mapping for integration tests (e.g. data-test-id="..")
placeholder={placeholder}
controlShouldRenderValue={false}
closeMenuOnSelect={false}
isOptionSelected={() => false}
options={allContexts}
formatOptionLabel={this.formatContextLabel}
noOptionsMessage={() => _i18n._(t`No contexts available or they have been added already`)}
onChange={({ value: ctx }: SelectOption<string>) => {
if (this.selectedContexts.includes(ctx)) {
this.selectedContexts.remove(ctx)
} else {
this.selectedContexts.push(ctx);
}
}}
/>
{this.selectedContexts.length > 0 && (
<small className="hint">
<span>Applying contexts:</span>{" "}
<code>{this.selectedContexts.join(", ")}</code>
</small>
)}
</>
)
}
onKubeConfigInputBlur = (evt: React.FocusEvent<HTMLInputElement>) => {
const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath;
if (isChanged) {
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
try {
this.setKubeConfig(this.kubeConfigPath, { throwError: true });
} catch (err) {
this.setKubeConfig(userStore.kubeConfigPath); // revert to previous valid path
}
}
}
onKubeConfigTabChange = (tabId: KubeConfigSourceTab) => {
this.sourceTab = tabId;
this.error = "";
this.refreshContexts();
}
protected formatContextLabel = ({ value: context }: SelectOption<string>) => {
const isNew = userStore.newContexts.has(context);
const isSelected = this.selectedContexts.includes(context);
return (
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
{isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right"/>}
</div>
)
};
render() {
return (
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
<WizardLayout
className="AddCluster"
infoPanel={this.renderInfo()}
contentClass={{ droppable: this.dropAreaActive }}
contentProps={{
onDragEnter: event => this.dropAreaActive = true,
onDragLeave: event => this.dropAreaActive = false,
onDragOver: event => {
event.preventDefault(); // enable onDrop()-callback
event.dataTransfer.dropEffect = "move"
},
onDrop: event => {
this.sourceTab = KubeConfigSourceTab.FILE;
this.dropAreaActive = false
this.setKubeConfig(event.dataTransfer.files[0].path)
}
}}
>
<h2><Trans>Add Cluster</Trans></h2>
<p>Choose config:</p>
<Select
placeholder={<Trans>Select kubeconfig</Trans>}
value={this.clusterConfig}
options={this.clusterOptions}
onChange={({ value }: SelectOption) => this.clusterConfig = value}
formatOptionLabel={this.formatClusterContextLabel}
id="kubecontext-select"
/>
{this.renderKubeConfigSource()}
{this.renderContextSelector()}
<div className="cluster-settings">
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
<Trans>Proxy settings</Trans>
@ -195,18 +383,6 @@ export class AddCluster extends React.Component {
</small>
</div>
)}
{this.isCustom && (
<div className="custom-kubeconfig flex column gaps box grow">
<p>Kubeconfig:</p>
<AceEditor
autoFocus
showGutter={false}
mode="yaml"
value={this.customConfig}
onChange={value => this.customConfig = value}
/>
</div>
)}
{this.error && (
<div className="error">{this.error}</div>
)}
@ -214,7 +390,7 @@ export class AddCluster extends React.Component {
<Button
primary
label={<Trans>Add cluster(s)</Trans>}
onClick={this.addCluster}
onClick={this.addClusters}
waiting={this.isWaiting}
/>
</div>

View File

@ -66,6 +66,10 @@
word-break: break-word;
color: var(--textColorSecondary);
}
.link {
@include pseudo-link;
}
}
}
}

View File

@ -2,12 +2,21 @@ import React from "react";
import { Cluster } from "../../../main/cluster";
import { SubTitle } from "../layout/sub-title";
import { Table, TableCell, TableRow } from "../table";
import { autobind } from "../../utils";
import { shell } from "electron";
interface Props {
cluster: Cluster;
}
export class Status extends React.Component<Props> {
@autobind()
openKubeconfig() {
const { cluster } = this.props;
shell.showItemInFolder(cluster.kubeConfigPath)
}
renderStatusRows() {
const { cluster } = this.props;
const rows = [
@ -27,6 +36,10 @@ export class Status extends React.Component<Props> {
</TableRow>
);
})}
<TableRow>
<TableCell>Kubeconfig</TableCell>
<TableCell className="link value" onClick={this.openKubeconfig}>{cluster.kubeConfigPath}</TableCell>
</TableRow>
</Table>
);
}

View File

@ -9,7 +9,7 @@
background-position: 0 35%;
background-size: 85%;
background-clip: content-box;
opacity: 1;
opacity: .75;
top: 0;
left: 0;
bottom: 0;
@ -21,4 +21,39 @@
opacity: 0.2;
}
}
.startup-hint {
$bgc: $mainBackground;
$arrowSize: 10px;
position: absolute;
left: 0;
top: 25px;
margin: $padding;
padding: $padding * 2;
width: 320px;
background: $bgc;
color: $textColorAccent;
filter: drop-shadow(0 0px 2px #ffffff33);
&:before {
content: "";
position: absolute;
width: 0;
height: 0;
border-top: $arrowSize solid transparent;
border-bottom: $arrowSize solid transparent;
border-right: $arrowSize solid $bgc;
right: 100%;
}
.theme-light & {
filter: drop-shadow(0 0px 2px #777);
background: white;
&:before {
border-right-color: white;
}
}
}
}

View File

@ -1,5 +1,6 @@
import "./landing-page.scss"
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { clusterStore } from "../../../common/cluster-store";
@ -7,11 +8,24 @@ import { workspaceStore } from "../../../common/workspace-store";
@observer
export class LandingPage extends React.Component {
@observable showHint = true;
render() {
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
const noClustersInScope = !clusters.length;
const showStartupHint = this.showHint && noClustersInScope;
return (
<div className="LandingPage flex">
{showStartupHint && (
<div className="startup-hint flex column gaps" onMouseEnter={() => this.showHint = false}>
<p><Trans>This is the quick launch menu.</Trans></p>
<p>
<Trans>
Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button.
</Trans>
</p>
</div>
)}
{noClustersInScope && (
<div className="no-clusters flex column gaps box center">
<h1>

View File

@ -3,17 +3,19 @@
import "./ace-editor.scss"
import React from "react"
import { observer, disposeOnUnmount } from "mobx-react";
import AceBuild, { Ace } from "ace-builds"
import { autobind, cssNames } from "../../utils";
import { themeStore } from "../../theme.store";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import AceBuild, { Ace } from "ace-builds"
import { autobind, cssNames, noop } from "../../utils";
import { themeStore } from "../../theme.store";
interface Props extends Partial<Ace.EditorOptions> {
className?: string;
autoFocus?: boolean;
hidden?: boolean;
cursorPos?: Ace.Point;
onFocus?(evt: FocusEvent, value: string): void;
onBlur?(evt: FocusEvent, value: string): void;
onChange?(value: string, delta: Ace.Delta): void;
onCursorPosChange?(point: Ace.Point): void;
}
@ -30,6 +32,8 @@ const defaultProps: Partial<Props> = {
foldStyle: "markbegin",
printMargin: false,
useWorker: false,
onBlur: noop,
onFocus: noop,
};
@observer
@ -64,7 +68,7 @@ export class AceEditor extends React.Component<Props, State> {
async componentDidMount() {
const {
mode, autoFocus, className, hidden, cursorPos,
onChange, onCursorPosChange, children,
onBlur, onFocus, onChange, onCursorPosChange, children,
...options
} = this.props;
@ -75,6 +79,8 @@ export class AceEditor extends React.Component<Props, State> {
this.setCursorPos(cursorPos);
// bind events
this.editor.on("blur", (evt: any) => onBlur(evt, this.getValue()));
this.editor.on("focus", (evt: any) => onFocus(evt, this.getValue()));
this.editor.on("change", this.onChange);
this.editor.selection.on("changeCursor", this.onCursorPosChange);

View File

@ -30,7 +30,7 @@
position: absolute;
right: 0;
bottom: 0;
margin: -$padding * 1.5;
margin: -$padding;
font-size: $font-size-small;
background: $colorError;
color: white;

View File

@ -3,7 +3,7 @@
font-size: $font-size-small;
background-color: #3d90ce;
padding: $spacing $padding;
padding: $padding / 4 $padding;
color: white;
#current-workspace {

View File

@ -11,7 +11,7 @@ export class BottomBar extends React.Component {
const { currentWorkspace } = workspaceStore;
return (
<div className="BottomBar flex gaps">
<div id="current-workspace" className="flex gaps align-center box right">
<div id="current-workspace" className="flex gaps align-center box">
<Icon small material="layers"/>
<span className="workspace-name">{currentWorkspace.name}</span>
</div>

View File

@ -3,58 +3,28 @@
position: relative;
text-align: center;
padding: $spacing;
background: $clusterMenuBackground;
border-right: 1px solid $clusterMenuBorderColor;
padding: $spacing 0;
min-width: 75px;
.is-mac & {
padding-top: $spacing * 2;
}
> .startup-tooltip {
$bgc: $mainBackground;
$arrowSize: 10px;
position: absolute;
top: 20px;
left: 100%;
margin: $padding;
padding: $spacing;
width: 320px;
background: $bgc;
color: $textColorAccent;
filter: drop-shadow(0 0px 2px #ffffff33);
pointer-events: none;
&:before {
content: "";
position: absolute;
width: 0;
height: 0;
border-top: $arrowSize solid transparent;
border-bottom: $arrowSize solid transparent;
border-right: $arrowSize solid $bgc;
right: 100%;
}
.theme-light & {
filter: drop-shadow(0 0px 2px #777);
background: white;
&:before {
border-right-color: white;
}
}
.is-mac &:before {
content: "";
height: 20px; // extra spacing for mac-os "traffic-light" buttons
}
.clusters {
@include hidden-scrollbar;
padding: 0 $spacing; // extra spacing for cluster-icon's badge
margin-bottom: $spacing;
&:empty {
display: none;
}
}
> .add-cluster {
position: relative;
margin-top: $padding;
min-width: 43px;
.Icon {
border-radius: $radius;

View File

@ -2,7 +2,6 @@ import "./clusters-menu.scss"
import { remote } from "electron"
import React from "react";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
import type { Cluster } from "../../../main/cluster";
@ -13,7 +12,7 @@ import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
import { navigate, navigation } from "../../navigation";
import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
import { clusterSettingsURL } from "../+cluster-settings";
import { landingURL } from "../+landing-page";
@ -30,8 +29,6 @@ interface Props {
@observer
export class ClustersMenu extends React.Component<Props> {
@observable showHint = true;
showCluster = (clusterId: ClusterId) => {
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
@ -76,8 +73,10 @@ export class ClustersMenu extends React.Component<Props> {
label: _i18n._(t`Remove`),
},
ok: () => {
if (clusterStore.activeClusterId === cluster.id) {
navigate(landingURL());
}
clusterStore.removeById(cluster.id);
navigate(landingURL());
},
message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>,
})
@ -92,24 +91,8 @@ export class ClustersMenu extends React.Component<Props> {
const { className } = this.props;
const { newContexts } = userStore;
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
const noClustersInScope = clusters.length === 0;
const isLanding = navigation.getPath() === landingURL();
const showStartupHint = this.showHint && isLanding && noClustersInScope;
return (
<div
className={cssNames("ClustersMenu flex column gaps", className)}
onMouseEnter={() => this.showHint = false}
>
{showStartupHint && (
<div className="startup-tooltip flex column gaps">
<p><Trans>This is the quick launch menu.</Trans></p>
<p>
<Trans>
Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button.
</Trans>
</p>
</div>
)}
<div className={cssNames("ClustersMenu flex column", className)}>
<div className="clusters flex column gaps">
{clusters.map(cluster => {
return (

View File

@ -1,4 +1,4 @@
import { observable } from "mobx";
import { observable, when } from "mobx";
import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { getMatchedCluster } from "./cluster-view.route"
import logger from "../../../main/logger";
@ -21,10 +21,10 @@ export async function initView(clusterId: ClusterId) {
}
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
const cluster = clusterStore.getById(clusterId);
await cluster.whenInitialized;
await cluster.whenReady;
const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe");
iframe.name = cluster.preferences.clusterName;
iframe.name = cluster.contextName;
iframe.setAttribute("src", `//${clusterId}.${location.host}`)
iframe.addEventListener("load", async () => {
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`)
@ -32,6 +32,12 @@ export async function initView(clusterId: ClusterId) {
})
lensViews.set(clusterId, { clusterId, view: iframe });
parentElem.appendChild(iframe);
// auto-clean when cluster removed
await when(() => !clusterStore.getById(clusterId));
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`)
parentElem.removeChild(iframe)
lensViews.delete(clusterId)
}
export function refreshViews() {

View File

@ -24,8 +24,12 @@
&:not(.isOpen) {
height: auto !important;
.Tab:not(:focus):after {
display: none;
.Tab {
--color-active: inherit;
&:not(:focus):after {
display: none;
}
}
}

View File

@ -0,0 +1,66 @@
import React, { InputHTMLAttributes } from "react";
export interface FileInputSelection<T = string> {
file: File;
data?: T | any; // not available when readAsTexts={false}
error?: string;
}
interface Props extends InputHTMLAttributes<any> {
id?: string; // could be used with <label htmlFor={id}/> to open filesystem dialog
accept?: string; // allowed file types to select, e.g. "application/json"
readAsText?: boolean; // provide files content as text in selection-callback
multiple?: boolean;
onSelectFiles(...selectedFiles: FileInputSelection[]): void;
}
export class FileInput extends React.Component<Props> {
protected input: HTMLInputElement;
protected style: React.CSSProperties = {
position: "absolute",
display: "none",
};
selectFiles = () => {
this.input.click(); // opens system dialog for selecting files
}
protected onChange = async (evt: React.ChangeEvent<HTMLInputElement>) => {
const fileList = Array.from(evt.target.files);
if (!fileList.length) {
return;
}
let selectedFiles: FileInputSelection[] = fileList.map(file => ({ file }));
if (this.props.readAsText) {
const readingFiles: Promise<FileInputSelection>[] = fileList.map(file => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve({
file: file,
data: reader.result,
error: reader.error ? String(reader.error) : null,
})
};
reader.readAsText(file);
})
});
selectedFiles = await Promise.all(readingFiles);
}
this.props.onSelectFiles(...selectedFiles);
}
render() {
const { onSelectFiles, readAsText, ...props } = this.props;
return (
<input
type="file"
style={this.style}
onChange={this.onChange}
ref={e => this.input = e}
{...props}
/>
)
}
}

View File

@ -1,2 +1,3 @@
export * from './input'
export * from './search-input'
export * from './file-input'

View File

@ -17,6 +17,7 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
theme?: "round-black";
className?: string;
value?: T;
autoSelectOnFocus?: boolean
multiLine?: boolean; // use text-area as input field
maxRows?: number; // when multiLine={true} define max rows size
dirty?: boolean; // show validation errors even if the field wasn't touched yet
@ -112,8 +113,7 @@ export class Input extends React.Component<InputProps, State> {
const result = validator.validate(value, this.props);
if (isBoolean(result) && !result) {
errors.push(this.getValidatorError(value, validator));
}
else if (result instanceof Promise) {
} else if (result instanceof Promise) {
if (!validationId) {
this.validationId = validationId = uniqueId("validation_id_");
}
@ -176,8 +176,9 @@ export class Input extends React.Component<InputProps, State> {
@autobind()
onFocus(evt: React.FocusEvent<InputElement>) {
const { onFocus } = this.props;
const { onFocus, autoSelectOnFocus } = this.props;
if (onFocus) onFocus(evt);
if (autoSelectOnFocus) this.select();
this.setState({ focused: true });
}
@ -255,13 +256,14 @@ export class Input extends React.Component<InputProps, State> {
}
render() {
/* eslint-disable */
let { multiLine, showValidationLine, validators, theme, maxRows, children, iconLeft, iconRight, ...inputProps } = this.props;
let { className, maxLength, rows, disabled, } = this.props;
/* eslint-enable */
const {
multiLine, showValidationLine, validators, theme, maxRows, children,
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight,
...inputProps
} = this.props;
const { focused, dirty, valid, validating, errors } = this.state;
className = cssNames("Input", className, {
const className = cssNames("Input", this.props.className, {
[`theme ${theme}`]: theme,
focused: focused,
disabled: disabled,
@ -271,10 +273,6 @@ export class Input extends React.Component<InputProps, State> {
validatingLine: validating && showValidationLine,
});
// normalize icons
if (isString(iconLeft)) iconLeft = <Icon material={iconLeft}/>
if (isString(iconRight)) iconRight = <Icon material={iconRight}/>
// prepare input props
Object.assign(inputProps, {
className: "input box grow",
@ -291,9 +289,9 @@ export class Input extends React.Component<InputProps, State> {
return (
<div className={className}>
<label className="input-area flex gaps align-center">
{iconLeft}
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
{multiLine ? <textarea {...inputProps as any}/> : <input {...inputProps as any}/>}
{iconRight}
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight}
</label>
<div className="input-info flex gaps">
{!valid && dirty && (

View File

@ -11,10 +11,6 @@
> .Tabs {
grid-area: tabs;
background: $layoutTabsBackground;
.Tab {
--color-active: #{$layoutTabsActiveColor};
}
}
header {

View File

@ -48,11 +48,14 @@ export class MainLayout extends React.Component<Props> {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
const routePath = navigation.location.pathname;
const cluster = getHostedCluster();
if (!cluster) {
return null; // fix: skip render when removing active (visible) cluster
}
return (
<div className={cssNames("MainLayout", className)}>
<header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">
{cluster.preferences?.clusterName || cluster.contextName}
{cluster.preferences.clusterName || cluster.contextName}
</span>
</header>

View File

@ -3,7 +3,7 @@ import React from "react";
import { observer } from "mobx-react";
import { cssNames, IClassName } from "../../utils";
interface Props {
interface Props extends React.DOMAttributes<any> {
className?: IClassName;
header?: React.ReactNode;
headerClass?: IClassName;
@ -11,22 +11,26 @@ interface Props {
infoPanelClass?: IClassName;
infoPanel?: React.ReactNode;
centered?: boolean; // Centering content horizontally
contentProps?: React.DOMAttributes<HTMLElement>
}
@observer
export class WizardLayout extends React.Component<Props> {
render() {
const { className, contentClass, infoPanelClass, infoPanel, header, headerClass, centered, children: content } = this.props;
const {
className, contentClass, infoPanelClass, infoPanel, header, headerClass, centered,
children, contentProps = {}, ...props
} = this.props;
return (
<div className={cssNames("WizardLayout", { centered }, className)}>
<div {...props} className={cssNames("WizardLayout", { centered }, className)}>
{header && (
<div className={cssNames("head-col flex gaps align-center", headerClass)}>
{header}
</div>
)}
<div className={cssNames("content-col flex column gaps", contentClass)}>
<div {...contentProps} className={cssNames("content-col flex column gaps", contentClass)}>
<div className="flex column gaps">
{content}
{children}
</div>
</div>
{infoPanel && (

View File

@ -29,11 +29,12 @@ export class Notifications extends React.Component {
});
}
static info(message: IMessage) {
static info(message: IMessage, customOpts: Partial<INotification> = {}) {
return notificationsStore.add({
message: message,
status: "info",
timeout: 0,
status: "info"
message: message,
...customOpts,
});
}

View File

@ -36,6 +36,7 @@ export class Select extends React.Component<SelectProps> {
static defaultProps: SelectProps = {
autoConvertOptions: true,
menuPortalTarget: document.body,
menuPlacement: "auto",
}
@computed get theme() {

View File

@ -4,6 +4,10 @@
-webkit-user-select: none; /* safari */
-moz-user-select: none; /* firefox */
&.withBorder {
border-bottom: 1px solid $borderFaintColor;
}
&.wrap {
flex-wrap: wrap;
}
@ -24,7 +28,7 @@
}
.Tab {
--color-active: inherit;
--color-active: #{$layoutTabsActiveColor};
--line-color-active: #{$primary};
--line-color-focus: currentColor;

View File

@ -7,6 +7,7 @@ const TabsContext = React.createContext<TabsContextValue>({});
interface TabsContextValue<D = any> {
autoFocus?: boolean;
withBorder?: boolean;
value?: D;
onChange?(value: D): void;
}
@ -29,16 +30,12 @@ export class Tabs extends React.PureComponent<TabsProps> {
}
render() {
const {
center, wrap, onChange, value, autoFocus,
scrollable = true,
...elemProps
} = this.props;
let { className } = this.props;
className = cssNames("Tabs", className, {
"center": center,
"wrap": wrap,
"scrollable": scrollable,
const { center, wrap, onChange, value, autoFocus, scrollable = true, withBorder, ...elemProps } = this.props;
const className = cssNames("Tabs", this.props.className, {
center: center,
wrap: wrap,
scrollable: scrollable,
withBorder: withBorder,
});
return (
<TabsContext.Provider value={{ autoFocus, value, onChange }}>

View File

@ -2,7 +2,34 @@
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.5.3 (current version)
## 3.6.0-beta.1 (current version)
- Allow user to select Kubeconfig from filesystem
- Store reference to added Kubeconfig files
- Show the path of the cluster's Kubeconfig in cluster settings
- Add support for PodDisruptionBudgets
- Add port-forwarding for containers in pod
- Add shortcut keys to menu items
- Improve light theme support
- Show GKE ingress IP
- Allow to remove clusters from right click
- Allow to trigger cronjobs
- Show devtools in menu
- Open last active cluster as default
- Fix format duration rounding days error
- Handle unsupported resources properly after they've been created from editor
- Fix CRD api parsing
- Fix: allow to edit Endpoint resources
- Fix: handle status values that contains an object
- Fix: incorrect path to install/uninstall feature
- Fix: increase timeout when doing port-forward
- Fix: change manifests order for Metrics feature
- Fix: Master donut graph for memory usage only appears to show one master
- Fix: Error during creation of Kubernetes secret
- Fix: Show age of resource in seconds
- Fix: Node shell pods are pending
- Fix: Wrong created time in resource details
## 3.5.3
- Updated [EULA](https://k8slens.dev/licenses/eula.md)
## 3.5.2