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:
commit
dd77d9be5e
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 "Использовать тоже имя для привязки ролей"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
})
|
||||
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
11
src/common/utils/saveToAppFiles.ts
Normal file
11
src/common/utils/saveToAppFiles.ts
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -66,6 +66,10 @@
|
||||
word-break: break-word;
|
||||
color: var(--textColorSecondary);
|
||||
}
|
||||
|
||||
.link {
|
||||
@include pseudo-link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
font-size: $font-size-small;
|
||||
background-color: #3d90ce;
|
||||
padding: $spacing $padding;
|
||||
padding: $padding / 4 $padding;
|
||||
color: white;
|
||||
|
||||
#current-workspace {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -24,8 +24,12 @@
|
||||
&:not(.isOpen) {
|
||||
height: auto !important;
|
||||
|
||||
.Tab:not(:focus):after {
|
||||
display: none;
|
||||
.Tab {
|
||||
--color-active: inherit;
|
||||
|
||||
&:not(:focus):after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
66
src/renderer/components/input/file-input.tsx
Normal file
66
src/renderer/components/input/file-input.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './input'
|
||||
export * from './search-input'
|
||||
export * from './file-input'
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -11,10 +11,6 @@
|
||||
> .Tabs {
|
||||
grid-area: tabs;
|
||||
background: $layoutTabsBackground;
|
||||
|
||||
.Tab {
|
||||
--color-active: #{$layoutTabsActiveColor};
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ export class Select extends React.Component<SelectProps> {
|
||||
static defaultProps: SelectProps = {
|
||||
autoConvertOptions: true,
|
||||
menuPortalTarget: document.body,
|
||||
menuPlacement: "auto",
|
||||
}
|
||||
|
||||
@computed get theme() {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 }}>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user