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

Merge branch 'master' into extensions-api

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-10-16 07:58:47 +03:00
commit a00d26149c
54 changed files with 1549 additions and 914 deletions

View File

@ -29,7 +29,7 @@ describe("Lens integration tests", () => {
} }
const clickWhatsNew = async (app: Application) => { const clickWhatsNew = async (app: Application) => {
await app.client.waitUntilTextExists("h1", "What's new") await app.client.waitUntilTextExists("h1", "What's new?")
await app.client.click("button.primary") await app.client.click("button.primary")
await app.client.waitUntilTextExists("h1", "Welcome") await app.client.waitUntilTextExists("h1", "Welcome")
} }
@ -160,8 +160,7 @@ describe("Lens integration tests", () => {
expectedSelector: string, expectedSelector: string,
expectedText: string expectedText: string
}[] }[]
}[] = [ }[] = [{
{
drawer: "", drawer: "",
drawerId: "", drawerId: "",
pages: [{ pages: [{
@ -384,8 +383,7 @@ describe("Lens integration tests", () => {
expectedSelector: "h5.title", expectedSelector: "h5.title",
expectedText: "Custom Resources" expectedText: "Custom Resources"
}] }]
}, }];
];
tests.forEach(({ drawer = "", drawerId = "", pages }) => { tests.forEach(({ drawer = "", drawerId = "", pages }) => {
if (drawer !== "") { if (drawer !== "") {
it(`shows ${drawer} drawer`, async () => { it(`shows ${drawer} drawer`, async () => {

View File

@ -12,7 +12,7 @@ export function setup(): Application {
args: [], args: [],
path: AppPaths[process.platform], path: AppPaths[process.platform],
startTimeout: 30000, startTimeout: 30000,
waitTimeout: 30000, waitTimeout: 60000,
chromeDriverArgs: ['remote-debugging-port=9222'], chromeDriverArgs: ['remote-debugging-port=9222'],
env: { env: {
CICD: "true" CICD: "true"
@ -21,10 +21,11 @@ export function setup(): Application {
} }
export async function tearDown(app: Application) { export async function tearDown(app: Application) {
const pid = app.mainProcess.pid let mpid: any = app.mainProcess.pid
let pid = await mpid()
await app.stop() await app.stop()
try { try {
process.kill(pid, 0); process.kill(pid, "SIGKILL");
} catch (e) { } catch (e) {
return return
} }

View File

@ -87,7 +87,7 @@ msgstr "Account Name"
msgid "Active" msgid "Active"
msgstr "Active" msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:289 #: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "Add Cluster" msgid "Add Cluster"
msgstr "Add Cluster" msgstr "Add Cluster"
@ -112,7 +112,7 @@ msgstr "Add bindings to {name}"
#~ msgid "Add cluster" #~ msgid "Add cluster"
#~ msgstr "Add cluster" #~ msgstr "Add cluster"
#: src/renderer/components/+add-cluster/add-cluster.tsx:306 #: src/renderer/components/+add-cluster/add-cluster.tsx:327
msgid "Add cluster(s)" msgid "Add cluster(s)"
msgstr "Add cluster(s)" msgstr "Add cluster(s)"
@ -203,7 +203,7 @@ msgstr "All clusters within workspace will be cleared as well"
msgid "All groups" msgid "All groups"
msgstr "All groups" msgstr "All groups"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57 #: src/renderer/components/dock/pod-logs.tsx:37
msgid "All logs" msgid "All logs"
msgstr "All logs" msgstr "All logs"
@ -323,7 +323,7 @@ msgstr "Binding targets"
msgid "Bindings" msgid "Bindings"
msgstr "Bindings" msgstr "Bindings"
#: src/renderer/components/+add-cluster/add-cluster.tsx:236 #: src/renderer/components/+add-cluster/add-cluster.tsx:257
msgid "Browse" msgid "Browse"
msgstr "Browse" msgstr "Browse"
@ -404,7 +404,7 @@ msgstr "CPU:"
#: src/renderer/components/+workspaces/workspaces.tsx:133 #: src/renderer/components/+workspaces/workspaces.tsx:133
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
#: src/renderer/components/dock/info-panel.tsx:85 #: src/renderer/components/dock/info-panel.tsx:86
#: src/renderer/components/wizard/wizard.tsx:130 #: src/renderer/components/wizard/wizard.tsx:130
msgid "Cancel" msgid "Cancel"
msgstr "Cancel" msgstr "Cancel"
@ -473,7 +473,6 @@ msgstr "Claim"
msgid "Claim Name" msgid "Claim Name"
msgstr "Claim Name" msgstr "Claim Name"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close" msgid "Close"
@ -566,7 +565,7 @@ msgstr "Configuration"
msgid "Connection" msgid "Connection"
msgstr "Connection" msgstr "Connection"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246 #: src/renderer/components/dock/pod-logs.tsx:148
msgid "Container" msgid "Container"
msgstr "Container" msgstr "Container"
@ -595,8 +594,8 @@ msgid "Container runtime"
msgstr "Container runtime" msgstr "Container runtime"
#: src/renderer/components/+workloads-pods/pod-details.tsx:122 #: src/renderer/components/+workloads-pods/pod-details.tsx:122
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186
#: src/renderer/components/+workloads-pods/pods.tsx:77 #: src/renderer/components/+workloads-pods/pods.tsx:77
#: src/renderer/components/dock/pod-logs.tsx:129
msgid "Containers" msgid "Containers"
msgstr "Containers" msgstr "Containers"
@ -691,7 +690,7 @@ msgstr "Create new Secret"
msgid "Create new Service Account" msgid "Create new Service Account"
msgstr "Create new Service Account" msgstr "Create new Service Account"
#: src/renderer/components/dock/dock.tsx:93 #: src/renderer/components/dock/dock.tsx:99
msgid "Create resource" msgid "Create resource"
msgstr "Create resource" msgstr "Create resource"
@ -928,7 +927,8 @@ msgstr "Environment"
msgid "Error stack" msgid "Error stack"
msgstr "Error stack" msgstr "Error stack"
#: src/renderer/components/+add-cluster/add-cluster.tsx:109 #: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
msgid "Error while adding cluster(s): {0}" msgid "Error while adding cluster(s): {0}"
msgstr "Error while adding cluster(s): {0}" msgstr "Error while adding cluster(s): {0}"
@ -948,7 +948,7 @@ msgstr "Everything is fine in the Cluster"
#~ msgid "Excluded items with \"system:\" prefix" #~ msgid "Excluded items with \"system:\" prefix"
#~ msgstr "Excluded items with \"system:\" prefix" #~ msgstr "Excluded items with \"system:\" prefix"
#: src/renderer/components/dock/dock.tsx:98 #: src/renderer/components/dock/dock.tsx:104
msgid "Exit full size mode" msgid "Exit full size mode"
msgstr "Exit full size mode" msgstr "Exit full size mode"
@ -964,7 +964,7 @@ msgstr "External IP"
msgid "External IPs" msgid "External IPs"
msgstr "External IPs" msgstr "External IPs"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106 #: src/renderer/components/dock/pod-logs.store.ts:65
msgid "Failed to load logs: {0}" msgid "Failed to load logs: {0}"
msgstr "Failed to load logs: {0}" msgstr "Failed to load logs: {0}"
@ -989,7 +989,7 @@ msgstr "Finalizers"
msgid "First seen" msgid "First seen"
msgstr "First seen" msgstr "First seen"
#: src/renderer/components/dock/dock.tsx:98 #: src/renderer/components/dock/dock.tsx:104
msgid "Fit to window" msgid "Fit to window"
msgstr "Fit to window" msgstr "Fit to window"
@ -1006,8 +1006,8 @@ msgid "From"
msgstr "From" msgstr "From"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212
msgid "From <0>{from}</0> to <1>{to}</1>" #~ msgid "From <0>{from}</0> to <1>{to}</1>"
msgstr "From <0>{from}</0> to <1>{to}</1>" #~ msgstr "From <0>{from}</0> to <1>{to}</1>"
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125 #: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125
msgid "Fs Group" msgid "Fs Group"
@ -1068,7 +1068,7 @@ msgid "Helm branch <0>{0}</0> already in use"
msgstr "Helm branch <0>{0}</0> already in use" msgstr "Helm branch <0>{0}</0> already in use"
#: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+config-secrets/secret-details.tsx:93
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19
msgid "Hide" msgid "Hide"
msgstr "Hide" msgstr "Hide"
@ -1153,7 +1153,7 @@ msgid "Ingresses"
msgstr "Ingresses" msgstr "Ingresses"
#: src/renderer/components/+workloads-pods/pod-details.tsx:118 #: src/renderer/components/+workloads-pods/pod-details.tsx:118
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192 #: src/renderer/components/dock/pod-logs.tsx:135
msgid "Init Containers" msgid "Init Containers"
msgstr "Init Containers" msgstr "Init Containers"
@ -1314,7 +1314,7 @@ msgstr "Limited to {0}"
msgid "Limits" msgid "Limits"
msgstr "Limits" msgstr "Limits"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248 #: src/renderer/components/dock/pod-logs.tsx:150
msgid "Lines" msgid "Lines"
msgstr "Lines" msgstr "Lines"
@ -1338,8 +1338,8 @@ msgstr "Load-Balancer Ingress Points"
msgid "Loading" msgid "Loading"
msgstr "Loading" msgstr "Loading"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:90 #: src/renderer/components/+workloads-pods/pod-menu.tsx:100
#: src/renderer/components/+workloads-pods/pod-menu.tsx:91 #: src/renderer/components/+workloads-pods/pod-menu.tsx:101
msgid "Logs" msgid "Logs"
msgstr "Logs" msgstr "Logs"
@ -1445,7 +1445,7 @@ msgstr "Min Available"
msgid "Min Pods" msgid "Min Pods"
msgstr "Min Pods" msgstr "Min Pods"
#: src/renderer/components/dock/dock.tsx:99 #: src/renderer/components/dock/dock.tsx:105
msgid "Minimize" msgid "Minimize"
msgstr "Minimize" msgstr "Minimize"
@ -1600,11 +1600,11 @@ msgstr "Network File System"
msgid "Network Policies" msgid "Network Policies"
msgstr "Network Policies" msgstr "Network Policies"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231 #: src/renderer/components/dock/pod-logs.tsx:178
msgid "New logs since opening the dialog" msgid "New logs since opening the dialog"
msgstr "New logs since opening the dialog" msgstr "New logs since opening the dialog"
#: src/renderer/components/dock/dock.tsx:86 #: src/renderer/components/dock/dock.tsx:92
msgid "New tab" msgid "New tab"
msgstr "New tab" msgstr "New tab"
@ -1642,7 +1642,7 @@ msgstr "No Nodes Available."
#~ msgid "No contexts available or they already added" #~ msgid "No contexts available or they already added"
#~ msgstr "No contexts available or they already added" #~ msgstr "No contexts available or they already added"
#: src/renderer/components/+add-cluster/add-cluster.tsx:260 #: src/renderer/components/+add-cluster/add-cluster.tsx:281
msgid "No contexts available or they have been added already" msgid "No contexts available or they have been added already"
msgstr "No contexts available or they have been added already" msgstr "No contexts available or they have been added already"
@ -1738,7 +1738,7 @@ msgstr "Ok"
msgid "Ok, got it!" msgid "Ok, got it!"
msgstr "Ok, got it!" msgstr "Ok, got it!"
#: src/renderer/components/dock/dock.tsx:99 #: src/renderer/components/dock/dock.tsx:105
msgid "Open" msgid "Open"
msgstr "Open" msgstr "Open"
@ -1774,7 +1774,7 @@ msgstr "Parallelism"
msgid "Parameters" msgid "Parameters"
msgstr "Parameters" msgstr "Parameters"
#: src/renderer/components/+add-cluster/add-cluster.tsx:230 #: src/renderer/components/+add-cluster/add-cluster.tsx:251
msgid "Paste as text" msgid "Paste as text"
msgstr "Paste as text" msgstr "Paste as text"
@ -1798,7 +1798,7 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes" msgid "Persistent Volumes"
msgstr "Persistent Volumes" msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:72 #: src/renderer/components/+add-cluster/add-cluster.tsx:75
msgid "Please select at least one cluster context" msgid "Please select at least one cluster context"
msgstr "Please select at least one cluster context" msgstr "Please select at least one cluster context"
@ -1851,7 +1851,7 @@ msgstr "Pod Selector"
msgid "Pod Status" msgid "Pod Status"
msgstr "Pod Status" msgstr "Pod Status"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:67 #: src/renderer/components/+workloads-pods/pod-menu.tsx:77
msgid "Pod shell" msgid "Pod shell"
msgstr "Pod shell" msgstr "Pod shell"
@ -1914,7 +1914,7 @@ msgstr "Privileged"
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgstr "Pro-Tip: paste kubeconfig to collect available contexts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:248 #: src/renderer/components/+add-cluster/add-cluster.tsx:269
msgid "Pro-Tip: paste kubeconfig to get available contexts" msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr "Pro-Tip: paste kubeconfig to get available contexts" msgstr "Pro-Tip: paste kubeconfig to get available contexts"
@ -1922,7 +1922,7 @@ msgstr "Pro-Tip: paste kubeconfig to get available contexts"
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgstr "Pro-Tip: paste kubeconfig to parse available contexts"
#: src/renderer/components/+add-cluster/add-cluster.tsx:239 #: src/renderer/components/+add-cluster/add-cluster.tsx:260
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" 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" msgstr "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
@ -1943,7 +1943,7 @@ msgstr "Provisioner"
msgid "Proxy is used only for non-cluster communication." msgid "Proxy is used only for non-cluster communication."
msgstr "Proxy is used only for non-cluster communication." msgstr "Proxy is used only for non-cluster communication."
#: src/renderer/components/+add-cluster/add-cluster.tsx:294 #: src/renderer/components/+add-cluster/add-cluster.tsx:315
msgid "Proxy settings" msgid "Proxy settings"
msgstr "Proxy settings" msgstr "Proxy settings"
@ -1979,7 +1979,7 @@ msgstr "Readiness"
msgid "Reason" msgid "Reason"
msgstr "Reason" msgstr "Reason"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107 #: src/renderer/components/dock/pod-logs.store.ts:66
msgid "Reason: {0} ({1})" msgid "Reason: {0} ({1})"
msgstr "Reason: {0} ({1})" msgstr "Reason: {0} ({1})"
@ -2124,7 +2124,7 @@ msgstr "Required Drop Capabilities"
msgid "Required field" msgid "Required field"
msgstr "Required field" msgstr "Required field"
#: src/renderer/components/+add-cluster/add-cluster.tsx:235 #: src/renderer/components/+add-cluster/add-cluster.tsx:256
#: src/renderer/components/item-object-list/page-filters-list.tsx:31 #: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset" msgid "Reset"
msgstr "Reset" msgstr "Reset"
@ -2266,9 +2266,9 @@ msgstr "Runtime Class"
#: src/renderer/components/+apps-releases/release-details.tsx:114 #: src/renderer/components/+apps-releases/release-details.tsx:114
#: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-maps/config-map-details.tsx:78
#: src/renderer/components/+config-secrets/secret-details.tsx:97 #: src/renderer/components/+config-secrets/secret-details.tsx:97
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216
#: src/renderer/components/+workspaces/workspaces.tsx:132 #: src/renderer/components/+workspaces/workspaces.tsx:132
#: src/renderer/components/dock/edit-resource.tsx:87 #: src/renderer/components/dock/edit-resource.tsx:87
#: src/renderer/components/dock/pod-logs.tsx:161
msgid "Save" msgid "Save"
msgstr "Save" msgstr "Save"
@ -2360,7 +2360,7 @@ msgstr "Select a quota.."
#~ msgid "Select context(s)" #~ msgid "Select context(s)"
#~ msgstr "Select context(s)" #~ msgstr "Select context(s)"
#: src/renderer/components/+add-cluster/add-cluster.tsx:257 #: src/renderer/components/+add-cluster/add-cluster.tsx:278
msgid "Select contexts" msgid "Select contexts"
msgstr "Select contexts" msgstr "Select contexts"
@ -2373,8 +2373,8 @@ msgstr "Select contexts"
#~ msgid "Select custom kube-config file" #~ msgid "Select custom kube-config file"
#~ msgstr "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:63
#: src/renderer/components/+add-cluster/add-cluster.tsx:62 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Select custom kubeconfig file" msgid "Select custom kubeconfig file"
msgstr "Select custom kubeconfig file" msgstr "Select custom kubeconfig file"
@ -2390,7 +2390,7 @@ msgstr "Select custom kubeconfig file"
#~ msgid "Select kubeconfig" #~ msgid "Select kubeconfig"
#~ msgstr "Select kubeconfig" #~ msgstr "Select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:229 #: src/renderer/components/+add-cluster/add-cluster.tsx:250
msgid "Select kubeconfig file" msgid "Select kubeconfig file"
msgstr "Select kubeconfig file" msgstr "Select kubeconfig file"
@ -2418,7 +2418,7 @@ msgstr "Select service accounts"
#~ msgid "Selected contexts ({0}): <0>{1}</0>" #~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr "Selected contexts ({0}): <0>{1}</0>" #~ msgstr "Selected contexts ({0}): <0>{1}</0>"
#: src/renderer/components/+add-cluster/add-cluster.tsx:256 #: src/renderer/components/+add-cluster/add-cluster.tsx:277
msgid "Selected contexts: <0>{0}</0>" msgid "Selected contexts: <0>{0}</0>"
msgstr "Selected contexts: <0>{0}</0>" msgstr "Selected contexts: <0>{0}</0>"
@ -2475,13 +2475,13 @@ msgid "Settings"
msgstr "Settings" msgstr "Settings"
#: src/renderer/components/+nodes/node-menu.tsx:48 #: src/renderer/components/+nodes/node-menu.tsx:48
#: src/renderer/components/+workloads-pods/pod-menu.tsx:68 #: src/renderer/components/+workloads-pods/pod-menu.tsx:78
msgid "Shell" msgid "Shell"
msgstr "Shell" msgstr "Shell"
#: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+config-secrets/secret-details.tsx:93
#: src/renderer/components/+workloads-pods/pod-container-env.tsx:101 #: src/renderer/components/+workloads-pods/pod-container-env.tsx:101
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19
msgid "Show" msgid "Show"
msgstr "Show" msgstr "Show"
@ -2490,10 +2490,22 @@ msgstr "Show"
msgid "Show Notes" msgid "Show Notes"
msgstr "Show Notes" msgstr "Show Notes"
#: src/renderer/components/dock/pod-logs.tsx:160
msgid "Show current logs"
msgstr "Show current logs"
#: src/renderer/components/dock/pod-logs.tsx:160
msgid "Show previous terminated container logs"
msgstr "Show previous terminated container logs"
#: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20 #: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20
msgid "Show value" msgid "Show value"
msgstr "Show value" msgstr "Show value"
#: src/renderer/components/dock/pod-logs.tsx:154
msgid "Since"
msgstr "Since"
#: src/renderer/components/+nodes/node-charts.tsx:80 #: src/renderer/components/+nodes/node-charts.tsx:80
#: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49 #: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49
msgid "Size" msgid "Size"
@ -2588,12 +2600,12 @@ msgstr "Strategy Type"
msgid "Sub-object" msgid "Sub-object"
msgstr "Sub-object" msgstr "Sub-object"
#: src/renderer/components/dock/info-panel.tsx:93 #: src/renderer/components/dock/info-panel.tsx:95
#: src/renderer/components/wizard/wizard.tsx:131 #: src/renderer/components/wizard/wizard.tsx:131
msgid "Submit" msgid "Submit"
msgstr "Submit" msgstr "Submit"
#: src/renderer/components/dock/info-panel.tsx:94 #: src/renderer/components/dock/info-panel.tsx:96
msgid "Submitting.." msgid "Submitting.."
msgstr "Submitting.." msgstr "Submitting.."
@ -2601,7 +2613,7 @@ msgstr "Submitting.."
msgid "Subsets" msgid "Subsets"
msgstr "Subsets" msgstr "Subsets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:102 #: src/renderer/components/+add-cluster/add-cluster.tsx:122
msgid "Successfully imported <0>{0}</0> cluster(s)" msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "Successfully imported <0>{0}</0> cluster(s)" msgstr "Successfully imported <0>{0}</0> cluster(s)"
@ -2635,7 +2647,7 @@ msgstr "Telemetry & usage data is collected to continuously improve the Lens exp
msgid "Terminal" msgid "Terminal"
msgstr "Terminal" msgstr "Terminal"
#: src/renderer/components/dock/dock.tsx:89 #: src/renderer/components/dock/dock.tsx:95
msgid "Terminal session" msgid "Terminal session"
msgstr "Terminal session" msgstr "Terminal session"
@ -2643,7 +2655,7 @@ msgstr "Terminal session"
msgid "The path to the kubectl binary on the system." msgid "The path to the kubectl binary on the system."
msgstr "The path to the kubectl binary on the system." msgstr "The path to the kubectl binary on the system."
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226 #: src/renderer/components/dock/pod-logs.tsx:172
msgid "There are no logs available for container." msgid "There are no logs available for container."
msgstr "There are no logs available for container." msgstr "There are no logs available for container."
@ -2773,8 +2785,8 @@ msgstr "Upgrade version"
msgid "Usage" msgid "Usage"
msgstr "Usage" msgstr "Usage"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:64
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Use configuration" msgid "Use configuration"
msgstr "Use configuration" msgstr "Use configuration"
@ -2961,7 +2973,7 @@ msgstr "sec"
msgid "singular" msgid "singular"
msgstr "singular" msgstr "singular"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
msgid "timestamps" msgid "timestamps"
msgstr "timestamps" msgstr "timestamps"
@ -3006,8 +3018,8 @@ msgid "{metricsRemainCount} more..."
msgstr "{metricsRemainCount} more..." msgstr "{metricsRemainCount} more..."
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240
msgid "{podName} Logs" #~ msgid "{podName} Logs"
msgstr "{podName} Logs" #~ msgstr "{podName} Logs"
#: src/renderer/components/dock/edit-resource.tsx:56 #: src/renderer/components/dock/edit-resource.tsx:56
msgid "{resourceType} <0>{resourceName}</0> updated." msgid "{resourceType} <0>{resourceName}</0> updated."
@ -3017,6 +3029,6 @@ msgstr "{resourceType} <0>{resourceName}</0> updated."
msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}</1>?</0>} other {<2>Remove <3>{selectedCount}</3> items <4>{selectedNames}</4> {tail}?</2>}}" msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}</1>?</0>} other {<2>Remove <3>{selectedCount}</3> items <4>{selectedNames}</4> {tail}?</2>}}"
msgstr "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}</1>?</0>} other {<2>Remove <3>{selectedCount}</3> items <4>{selectedNames}</4> {tail}?</2>}}" msgstr "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}</1>?</0>} other {<2>Remove <3>{selectedCount}</3> items <4>{selectedNames}</4> {tail}?</2>}}"
#: src/renderer/components/dock/info-panel.tsx:88 #: src/renderer/components/dock/info-panel.tsx:89
msgid "{submitLabel} & Close" msgid "{submitLabel} & Close"
msgstr "{submitLabel} & Close" msgstr "{submitLabel} & Close"

View File

@ -87,7 +87,7 @@ msgstr ""
msgid "Active" msgid "Active"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:289 #: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "Add Cluster" msgid "Add Cluster"
msgstr "" msgstr ""
@ -112,7 +112,7 @@ msgstr ""
#~ msgid "Add cluster" #~ msgid "Add cluster"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:306 #: src/renderer/components/+add-cluster/add-cluster.tsx:327
msgid "Add cluster(s)" msgid "Add cluster(s)"
msgstr "" msgstr ""
@ -203,7 +203,7 @@ msgstr ""
msgid "All groups" msgid "All groups"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57 #: src/renderer/components/dock/pod-logs.tsx:37
msgid "All logs" msgid "All logs"
msgstr "" msgstr ""
@ -323,7 +323,7 @@ msgstr ""
msgid "Bindings" msgid "Bindings"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:236 #: src/renderer/components/+add-cluster/add-cluster.tsx:257
msgid "Browse" msgid "Browse"
msgstr "" msgstr ""
@ -404,7 +404,7 @@ msgstr ""
#: src/renderer/components/+workspaces/workspaces.tsx:133 #: src/renderer/components/+workspaces/workspaces.tsx:133
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
#: src/renderer/components/dock/info-panel.tsx:85 #: src/renderer/components/dock/info-panel.tsx:86
#: src/renderer/components/wizard/wizard.tsx:130 #: src/renderer/components/wizard/wizard.tsx:130
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@ -469,7 +469,6 @@ msgstr ""
msgid "Claim Name" msgid "Claim Name"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close" msgid "Close"
@ -562,7 +561,7 @@ msgstr ""
msgid "Connection" msgid "Connection"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246 #: src/renderer/components/dock/pod-logs.tsx:148
msgid "Container" msgid "Container"
msgstr "" msgstr ""
@ -591,8 +590,8 @@ msgid "Container runtime"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-details.tsx:122 #: src/renderer/components/+workloads-pods/pod-details.tsx:122
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186
#: src/renderer/components/+workloads-pods/pods.tsx:77 #: src/renderer/components/+workloads-pods/pods.tsx:77
#: src/renderer/components/dock/pod-logs.tsx:129
msgid "Containers" msgid "Containers"
msgstr "" msgstr ""
@ -687,7 +686,7 @@ msgstr ""
msgid "Create new Service Account" msgid "Create new Service Account"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:93 #: src/renderer/components/dock/dock.tsx:99
msgid "Create resource" msgid "Create resource"
msgstr "" msgstr ""
@ -924,7 +923,8 @@ msgstr ""
msgid "Error stack" msgid "Error stack"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:109 #: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
msgid "Error while adding cluster(s): {0}" msgid "Error while adding cluster(s): {0}"
msgstr "" msgstr ""
@ -939,7 +939,7 @@ msgstr ""
msgid "Everything is fine in the Cluster" msgid "Everything is fine in the Cluster"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:98 #: src/renderer/components/dock/dock.tsx:104
msgid "Exit full size mode" msgid "Exit full size mode"
msgstr "" msgstr ""
@ -955,7 +955,7 @@ msgstr ""
msgid "External IPs" msgid "External IPs"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106 #: src/renderer/components/dock/pod-logs.store.ts:65
msgid "Failed to load logs: {0}" msgid "Failed to load logs: {0}"
msgstr "" msgstr ""
@ -980,7 +980,7 @@ msgstr ""
msgid "First seen" msgid "First seen"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:98 #: src/renderer/components/dock/dock.tsx:104
msgid "Fit to window" msgid "Fit to window"
msgstr "" msgstr ""
@ -997,8 +997,8 @@ msgid "From"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212
msgid "From <0>{from}</0> to <1>{to}</1>" #~ msgid "From <0>{from}</0> to <1>{to}</1>"
msgstr "" #~ msgstr ""
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125 #: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125
msgid "Fs Group" msgid "Fs Group"
@ -1059,7 +1059,7 @@ msgid "Helm branch <0>{0}</0> already in use"
msgstr "" msgstr ""
#: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+config-secrets/secret-details.tsx:93
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19
msgid "Hide" msgid "Hide"
msgstr "" msgstr ""
@ -1144,7 +1144,7 @@ msgid "Ingresses"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-details.tsx:118 #: src/renderer/components/+workloads-pods/pod-details.tsx:118
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192 #: src/renderer/components/dock/pod-logs.tsx:135
msgid "Init Containers" msgid "Init Containers"
msgstr "" msgstr ""
@ -1305,7 +1305,7 @@ msgstr ""
msgid "Limits" msgid "Limits"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248 #: src/renderer/components/dock/pod-logs.tsx:150
msgid "Lines" msgid "Lines"
msgstr "" msgstr ""
@ -1329,8 +1329,8 @@ msgstr ""
msgid "Loading" msgid "Loading"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:90 #: src/renderer/components/+workloads-pods/pod-menu.tsx:100
#: src/renderer/components/+workloads-pods/pod-menu.tsx:91 #: src/renderer/components/+workloads-pods/pod-menu.tsx:101
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -1436,7 +1436,7 @@ msgstr ""
msgid "Min Pods" msgid "Min Pods"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:99 #: src/renderer/components/dock/dock.tsx:105
msgid "Minimize" msgid "Minimize"
msgstr "" msgstr ""
@ -1591,11 +1591,11 @@ msgstr ""
msgid "Network Policies" msgid "Network Policies"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231 #: src/renderer/components/dock/pod-logs.tsx:178
msgid "New logs since opening the dialog" msgid "New logs since opening the dialog"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:86 #: src/renderer/components/dock/dock.tsx:92
msgid "New tab" msgid "New tab"
msgstr "" msgstr ""
@ -1625,7 +1625,7 @@ msgstr ""
#~ msgid "No contexts available or they already added" #~ msgid "No contexts available or they already added"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:260 #: src/renderer/components/+add-cluster/add-cluster.tsx:281
msgid "No contexts available or they have been added already" msgid "No contexts available or they have been added already"
msgstr "" msgstr ""
@ -1721,7 +1721,7 @@ msgstr ""
msgid "Ok, got it!" msgid "Ok, got it!"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:99 #: src/renderer/components/dock/dock.tsx:105
msgid "Open" msgid "Open"
msgstr "" msgstr ""
@ -1757,7 +1757,7 @@ msgstr ""
msgid "Parameters" msgid "Parameters"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:230 #: src/renderer/components/+add-cluster/add-cluster.tsx:251
msgid "Paste as text" msgid "Paste as text"
msgstr "" msgstr ""
@ -1781,7 +1781,7 @@ msgstr ""
msgid "Persistent Volumes" msgid "Persistent Volumes"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:72 #: src/renderer/components/+add-cluster/add-cluster.tsx:75
msgid "Please select at least one cluster context" msgid "Please select at least one cluster context"
msgstr "" msgstr ""
@ -1834,7 +1834,7 @@ msgstr ""
msgid "Pod Status" msgid "Pod Status"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:67 #: src/renderer/components/+workloads-pods/pod-menu.tsx:77
msgid "Pod shell" msgid "Pod shell"
msgstr "" msgstr ""
@ -1897,7 +1897,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:248 #: src/renderer/components/+add-cluster/add-cluster.tsx:269
msgid "Pro-Tip: paste kubeconfig to get available contexts" msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr "" msgstr ""
@ -1905,7 +1905,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:239 #: src/renderer/components/+add-cluster/add-cluster.tsx:260
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgstr "" msgstr ""
@ -1926,7 +1926,7 @@ msgstr ""
msgid "Proxy is used only for non-cluster communication." msgid "Proxy is used only for non-cluster communication."
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:294 #: src/renderer/components/+add-cluster/add-cluster.tsx:315
msgid "Proxy settings" msgid "Proxy settings"
msgstr "" msgstr ""
@ -1962,7 +1962,7 @@ msgstr ""
msgid "Reason" msgid "Reason"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107 #: src/renderer/components/dock/pod-logs.store.ts:66
msgid "Reason: {0} ({1})" msgid "Reason: {0} ({1})"
msgstr "" msgstr ""
@ -2107,7 +2107,7 @@ msgstr ""
msgid "Required field" msgid "Required field"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:235 #: src/renderer/components/+add-cluster/add-cluster.tsx:256
#: src/renderer/components/item-object-list/page-filters-list.tsx:31 #: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
@ -2249,9 +2249,9 @@ msgstr ""
#: src/renderer/components/+apps-releases/release-details.tsx:114 #: src/renderer/components/+apps-releases/release-details.tsx:114
#: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-maps/config-map-details.tsx:78
#: src/renderer/components/+config-secrets/secret-details.tsx:97 #: src/renderer/components/+config-secrets/secret-details.tsx:97
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216
#: src/renderer/components/+workspaces/workspaces.tsx:132 #: src/renderer/components/+workspaces/workspaces.tsx:132
#: src/renderer/components/dock/edit-resource.tsx:87 #: src/renderer/components/dock/edit-resource.tsx:87
#: src/renderer/components/dock/pod-logs.tsx:161
msgid "Save" msgid "Save"
msgstr "" msgstr ""
@ -2343,7 +2343,7 @@ msgstr ""
#~ msgid "Select context(s)" #~ msgid "Select context(s)"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:257 #: src/renderer/components/+add-cluster/add-cluster.tsx:278
msgid "Select contexts" msgid "Select contexts"
msgstr "" msgstr ""
@ -2356,8 +2356,8 @@ msgstr ""
#~ msgid "Select custom kube-config file" #~ msgid "Select custom kube-config file"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:62 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
#: src/renderer/components/+add-cluster/add-cluster.tsx:62 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Select custom kubeconfig file" msgid "Select custom kubeconfig file"
msgstr "" msgstr ""
@ -2373,7 +2373,7 @@ msgstr ""
#~ msgid "Select kubeconfig" #~ msgid "Select kubeconfig"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:229 #: src/renderer/components/+add-cluster/add-cluster.tsx:250
msgid "Select kubeconfig file" msgid "Select kubeconfig file"
msgstr "" msgstr ""
@ -2401,7 +2401,7 @@ msgstr ""
#~ msgid "Selected contexts ({0}): <0>{1}</0>" #~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:256 #: src/renderer/components/+add-cluster/add-cluster.tsx:277
msgid "Selected contexts: <0>{0}</0>" msgid "Selected contexts: <0>{0}</0>"
msgstr "" msgstr ""
@ -2458,13 +2458,13 @@ msgid "Settings"
msgstr "" msgstr ""
#: src/renderer/components/+nodes/node-menu.tsx:48 #: src/renderer/components/+nodes/node-menu.tsx:48
#: src/renderer/components/+workloads-pods/pod-menu.tsx:68 #: src/renderer/components/+workloads-pods/pod-menu.tsx:78
msgid "Shell" msgid "Shell"
msgstr "" msgstr ""
#: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+config-secrets/secret-details.tsx:93
#: src/renderer/components/+workloads-pods/pod-container-env.tsx:101 #: src/renderer/components/+workloads-pods/pod-container-env.tsx:101
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19
msgid "Show" msgid "Show"
msgstr "" msgstr ""
@ -2473,10 +2473,22 @@ msgstr ""
msgid "Show Notes" msgid "Show Notes"
msgstr "" msgstr ""
#: src/renderer/components/dock/pod-logs.tsx:160
msgid "Show current logs"
msgstr ""
#: src/renderer/components/dock/pod-logs.tsx:160
msgid "Show previous terminated container logs"
msgstr ""
#: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20 #: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20
msgid "Show value" msgid "Show value"
msgstr "" msgstr ""
#: src/renderer/components/dock/pod-logs.tsx:154
msgid "Since"
msgstr ""
#: src/renderer/components/+nodes/node-charts.tsx:80 #: src/renderer/components/+nodes/node-charts.tsx:80
#: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49 #: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49
msgid "Size" msgid "Size"
@ -2571,12 +2583,12 @@ msgstr ""
msgid "Sub-object" msgid "Sub-object"
msgstr "" msgstr ""
#: src/renderer/components/dock/info-panel.tsx:93 #: src/renderer/components/dock/info-panel.tsx:95
#: src/renderer/components/wizard/wizard.tsx:131 #: src/renderer/components/wizard/wizard.tsx:131
msgid "Submit" msgid "Submit"
msgstr "" msgstr ""
#: src/renderer/components/dock/info-panel.tsx:94 #: src/renderer/components/dock/info-panel.tsx:96
msgid "Submitting.." msgid "Submitting.."
msgstr "" msgstr ""
@ -2584,7 +2596,7 @@ msgstr ""
msgid "Subsets" msgid "Subsets"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:102 #: src/renderer/components/+add-cluster/add-cluster.tsx:122
msgid "Successfully imported <0>{0}</0> cluster(s)" msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "" msgstr ""
@ -2618,7 +2630,7 @@ msgstr ""
msgid "Terminal" msgid "Terminal"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:89 #: src/renderer/components/dock/dock.tsx:95
msgid "Terminal session" msgid "Terminal session"
msgstr "" msgstr ""
@ -2626,7 +2638,7 @@ msgstr ""
msgid "The path to the kubectl binary on the system." msgid "The path to the kubectl binary on the system."
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226 #: src/renderer/components/dock/pod-logs.tsx:172
msgid "There are no logs available for container." msgid "There are no logs available for container."
msgstr "" msgstr ""
@ -2756,8 +2768,8 @@ msgstr ""
msgid "Usage" msgid "Usage"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:64
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Use configuration" msgid "Use configuration"
msgstr "" msgstr ""
@ -2944,7 +2956,7 @@ msgstr ""
msgid "singular" msgid "singular"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
msgid "timestamps" msgid "timestamps"
msgstr "" msgstr ""
@ -2989,8 +3001,8 @@ msgid "{metricsRemainCount} more..."
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240
msgid "{podName} Logs" #~ msgid "{podName} Logs"
msgstr "" #~ msgstr ""
#: src/renderer/components/dock/edit-resource.tsx:56 #: src/renderer/components/dock/edit-resource.tsx:56
msgid "{resourceType} <0>{resourceName}</0> updated." msgid "{resourceType} <0>{resourceName}</0> updated."
@ -3000,6 +3012,6 @@ msgstr ""
msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}</1>?</0>} other {<2>Remove <3>{selectedCount}</3> items <4>{selectedNames}</4> {tail}?</2>}}" msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}</1>?</0>} other {<2>Remove <3>{selectedCount}</3> items <4>{selectedNames}</4> {tail}?</2>}}"
msgstr "" msgstr ""
#: src/renderer/components/dock/info-panel.tsx:88 #: src/renderer/components/dock/info-panel.tsx:89
msgid "{submitLabel} & Close" msgid "{submitLabel} & Close"
msgstr "" msgstr ""

View File

@ -88,7 +88,7 @@ msgstr "Название аккаунта"
msgid "Active" msgid "Active"
msgstr "Активный" msgstr "Активный"
#: src/renderer/components/+add-cluster/add-cluster.tsx:289 #: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "Add Cluster" msgid "Add Cluster"
msgstr "" msgstr ""
@ -113,7 +113,7 @@ msgstr "Добавить привязки к {name}"
#~ msgid "Add cluster" #~ msgid "Add cluster"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:306 #: src/renderer/components/+add-cluster/add-cluster.tsx:327
msgid "Add cluster(s)" msgid "Add cluster(s)"
msgstr "" msgstr ""
@ -204,7 +204,7 @@ msgstr ""
msgid "All groups" msgid "All groups"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57 #: src/renderer/components/dock/pod-logs.tsx:37
msgid "All logs" msgid "All logs"
msgstr "Все логи" msgstr "Все логи"
@ -324,7 +324,7 @@ msgstr "Цели привязки"
msgid "Bindings" msgid "Bindings"
msgstr "Привязки" msgstr "Привязки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:236 #: src/renderer/components/+add-cluster/add-cluster.tsx:257
msgid "Browse" msgid "Browse"
msgstr "" msgstr ""
@ -405,7 +405,7 @@ msgstr "CPU:"
#: src/renderer/components/+workspaces/workspaces.tsx:133 #: src/renderer/components/+workspaces/workspaces.tsx:133
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
#: src/renderer/components/dock/info-panel.tsx:85 #: src/renderer/components/dock/info-panel.tsx:86
#: src/renderer/components/wizard/wizard.tsx:130 #: src/renderer/components/wizard/wizard.tsx:130
msgid "Cancel" msgid "Cancel"
msgstr "Отмена" msgstr "Отмена"
@ -474,7 +474,6 @@ msgstr "Запрос"
msgid "Claim Name" msgid "Claim Name"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close" msgid "Close"
@ -567,7 +566,7 @@ msgstr "Конфигурация"
msgid "Connection" msgid "Connection"
msgstr "Соединение" msgstr "Соединение"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246 #: src/renderer/components/dock/pod-logs.tsx:148
msgid "Container" msgid "Container"
msgstr "Контейнер" msgstr "Контейнер"
@ -596,8 +595,8 @@ msgid "Container runtime"
msgstr "Среда контейнеров" msgstr "Среда контейнеров"
#: src/renderer/components/+workloads-pods/pod-details.tsx:122 #: src/renderer/components/+workloads-pods/pod-details.tsx:122
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186
#: src/renderer/components/+workloads-pods/pods.tsx:77 #: src/renderer/components/+workloads-pods/pods.tsx:77
#: src/renderer/components/dock/pod-logs.tsx:129
msgid "Containers" msgid "Containers"
msgstr "Контейнеры" msgstr "Контейнеры"
@ -692,7 +691,7 @@ msgstr "Создать новый секрет"
msgid "Create new Service Account" msgid "Create new Service Account"
msgstr "Создать новый Service Account" msgstr "Создать новый Service Account"
#: src/renderer/components/dock/dock.tsx:93 #: src/renderer/components/dock/dock.tsx:99
msgid "Create resource" msgid "Create resource"
msgstr "Создать ресурс" msgstr "Создать ресурс"
@ -929,7 +928,8 @@ msgstr "Среда"
msgid "Error stack" msgid "Error stack"
msgstr "Стэк ошибки" msgstr "Стэк ошибки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:109 #: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
msgid "Error while adding cluster(s): {0}" msgid "Error while adding cluster(s): {0}"
msgstr "" msgstr ""
@ -949,7 +949,7 @@ msgstr "В кластере все в порядке"
#~ msgid "Excluded items with \"system:\" prefix" #~ msgid "Excluded items with \"system:\" prefix"
#~ msgstr "За исключением объектов с префиксом “system:”" #~ msgstr "За исключением объектов с префиксом “system:”"
#: src/renderer/components/dock/dock.tsx:98 #: src/renderer/components/dock/dock.tsx:104
msgid "Exit full size mode" msgid "Exit full size mode"
msgstr "Выйти из полного размера" msgstr "Выйти из полного размера"
@ -965,7 +965,7 @@ msgstr "Внешний IP"
msgid "External IPs" msgid "External IPs"
msgstr "Внешние IP" msgstr "Внешние IP"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106 #: src/renderer/components/dock/pod-logs.store.ts:65
msgid "Failed to load logs: {0}" msgid "Failed to load logs: {0}"
msgstr "Ошибка загрузки логов: {0}" msgstr "Ошибка загрузки логов: {0}"
@ -990,7 +990,7 @@ msgstr "Финализаторы"
msgid "First seen" msgid "First seen"
msgstr "Увиденно впервые" msgstr "Увиденно впервые"
#: src/renderer/components/dock/dock.tsx:98 #: src/renderer/components/dock/dock.tsx:104
msgid "Fit to window" msgid "Fit to window"
msgstr "По размеру окна" msgstr "По размеру окна"
@ -1007,8 +1007,8 @@ msgid "From"
msgstr "От" msgstr "От"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212
msgid "From <0>{from}</0> to <1>{to}</1>" #~ msgid "From <0>{from}</0> to <1>{to}</1>"
msgstr "От <0>{from}</0> до <1>{to}</1>" #~ msgstr "От <0>{from}</0> до <1>{to}</1>"
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125 #: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125
msgid "Fs Group" msgid "Fs Group"
@ -1069,7 +1069,7 @@ msgid "Helm branch <0>{0}</0> already in use"
msgstr "" msgstr ""
#: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+config-secrets/secret-details.tsx:93
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19
msgid "Hide" msgid "Hide"
msgstr "Скрыть" msgstr "Скрыть"
@ -1154,7 +1154,7 @@ msgid "Ingresses"
msgstr "Ingresses" msgstr "Ingresses"
#: src/renderer/components/+workloads-pods/pod-details.tsx:118 #: src/renderer/components/+workloads-pods/pod-details.tsx:118
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192 #: src/renderer/components/dock/pod-logs.tsx:135
msgid "Init Containers" msgid "Init Containers"
msgstr "Контейнеры инициализации" msgstr "Контейнеры инициализации"
@ -1315,7 +1315,7 @@ msgstr ""
msgid "Limits" msgid "Limits"
msgstr "Лимиты" msgstr "Лимиты"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248 #: src/renderer/components/dock/pod-logs.tsx:150
msgid "Lines" msgid "Lines"
msgstr "Строки" msgstr "Строки"
@ -1339,8 +1339,8 @@ msgstr ""
msgid "Loading" msgid "Loading"
msgstr "Загрузка" msgstr "Загрузка"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:90 #: src/renderer/components/+workloads-pods/pod-menu.tsx:100
#: src/renderer/components/+workloads-pods/pod-menu.tsx:91 #: src/renderer/components/+workloads-pods/pod-menu.tsx:101
msgid "Logs" msgid "Logs"
msgstr "Логи" msgstr "Логи"
@ -1446,7 +1446,7 @@ msgstr ""
msgid "Min Pods" msgid "Min Pods"
msgstr "Мин. подов" msgstr "Мин. подов"
#: src/renderer/components/dock/dock.tsx:99 #: src/renderer/components/dock/dock.tsx:105
msgid "Minimize" msgid "Minimize"
msgstr "Минимизировать" msgstr "Минимизировать"
@ -1601,11 +1601,11 @@ msgstr "Сетевая файловая система"
msgid "Network Policies" msgid "Network Policies"
msgstr "Network Policies" msgstr "Network Policies"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231 #: src/renderer/components/dock/pod-logs.tsx:178
msgid "New logs since opening the dialog" msgid "New logs since opening the dialog"
msgstr "Новые логи с момента открытия диалога" msgstr "Новые логи с момента открытия диалога"
#: src/renderer/components/dock/dock.tsx:86 #: src/renderer/components/dock/dock.tsx:92
msgid "New tab" msgid "New tab"
msgstr "Новая вкладка" msgstr "Новая вкладка"
@ -1643,7 +1643,7 @@ msgstr "Нет доступных нод."
#~ msgid "No contexts available or they already added" #~ msgid "No contexts available or they already added"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:260 #: src/renderer/components/+add-cluster/add-cluster.tsx:281
msgid "No contexts available or they have been added already" msgid "No contexts available or they have been added already"
msgstr "" msgstr ""
@ -1739,7 +1739,7 @@ msgstr "Ок"
msgid "Ok, got it!" msgid "Ok, got it!"
msgstr "" msgstr ""
#: src/renderer/components/dock/dock.tsx:99 #: src/renderer/components/dock/dock.tsx:105
msgid "Open" msgid "Open"
msgstr "Открыть" msgstr "Открыть"
@ -1775,7 +1775,7 @@ msgstr "Параллелизм"
msgid "Parameters" msgid "Parameters"
msgstr "Параметры" msgstr "Параметры"
#: src/renderer/components/+add-cluster/add-cluster.tsx:230 #: src/renderer/components/+add-cluster/add-cluster.tsx:251
msgid "Paste as text" msgid "Paste as text"
msgstr "" msgstr ""
@ -1799,7 +1799,7 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes" msgid "Persistent Volumes"
msgstr "Persistent Volumes" msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:72 #: src/renderer/components/+add-cluster/add-cluster.tsx:75
msgid "Please select at least one cluster context" msgid "Please select at least one cluster context"
msgstr "" msgstr ""
@ -1852,7 +1852,7 @@ msgstr "Селектор подов"
msgid "Pod Status" msgid "Pod Status"
msgstr "Статус подов" msgstr "Статус подов"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:67 #: src/renderer/components/+workloads-pods/pod-menu.tsx:77
msgid "Pod shell" msgid "Pod shell"
msgstr "Командная строка пода" msgstr "Командная строка пода"
@ -1915,7 +1915,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts" #~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:248 #: src/renderer/components/+add-cluster/add-cluster.tsx:269
msgid "Pro-Tip: paste kubeconfig to get available contexts" msgid "Pro-Tip: paste kubeconfig to get available contexts"
msgstr "" msgstr ""
@ -1923,7 +1923,7 @@ msgstr ""
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts" #~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:239 #: src/renderer/components/+add-cluster/add-cluster.tsx:260
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area" msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
msgstr "" msgstr ""
@ -1944,7 +1944,7 @@ msgstr "Комиссия"
msgid "Proxy is used only for non-cluster communication." msgid "Proxy is used only for non-cluster communication."
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:294 #: src/renderer/components/+add-cluster/add-cluster.tsx:315
msgid "Proxy settings" msgid "Proxy settings"
msgstr "" msgstr ""
@ -1980,7 +1980,7 @@ msgstr "Готовность"
msgid "Reason" msgid "Reason"
msgstr "Причина" msgstr "Причина"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107 #: src/renderer/components/dock/pod-logs.store.ts:66
msgid "Reason: {0} ({1})" msgid "Reason: {0} ({1})"
msgstr "Причина: {0} ({1})" msgstr "Причина: {0} ({1})"
@ -2125,7 +2125,7 @@ msgstr ""
msgid "Required field" msgid "Required field"
msgstr "Обязательное поле" msgstr "Обязательное поле"
#: src/renderer/components/+add-cluster/add-cluster.tsx:235 #: src/renderer/components/+add-cluster/add-cluster.tsx:256
#: src/renderer/components/item-object-list/page-filters-list.tsx:31 #: src/renderer/components/item-object-list/page-filters-list.tsx:31
msgid "Reset" msgid "Reset"
msgstr "Сбросить" msgstr "Сбросить"
@ -2267,9 +2267,9 @@ msgstr ""
#: src/renderer/components/+apps-releases/release-details.tsx:114 #: src/renderer/components/+apps-releases/release-details.tsx:114
#: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-maps/config-map-details.tsx:78
#: src/renderer/components/+config-secrets/secret-details.tsx:97 #: src/renderer/components/+config-secrets/secret-details.tsx:97
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216
#: src/renderer/components/+workspaces/workspaces.tsx:132 #: src/renderer/components/+workspaces/workspaces.tsx:132
#: src/renderer/components/dock/edit-resource.tsx:87 #: src/renderer/components/dock/edit-resource.tsx:87
#: src/renderer/components/dock/pod-logs.tsx:161
msgid "Save" msgid "Save"
msgstr "Сохранить" msgstr "Сохранить"
@ -2361,7 +2361,7 @@ msgstr "Выберите квоту..."
#~ msgid "Select context(s)" #~ msgid "Select context(s)"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:257 #: src/renderer/components/+add-cluster/add-cluster.tsx:278
msgid "Select contexts" msgid "Select contexts"
msgstr "" msgstr ""
@ -2374,8 +2374,8 @@ msgstr ""
#~ msgid "Select custom kube-config file" #~ msgid "Select custom kube-config file"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:62 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
#: src/renderer/components/+add-cluster/add-cluster.tsx:62 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Select custom kubeconfig file" msgid "Select custom kubeconfig file"
msgstr "" msgstr ""
@ -2391,7 +2391,7 @@ msgstr ""
#~ msgid "Select kubeconfig" #~ msgid "Select kubeconfig"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:229 #: src/renderer/components/+add-cluster/add-cluster.tsx:250
msgid "Select kubeconfig file" msgid "Select kubeconfig file"
msgstr "" msgstr ""
@ -2419,7 +2419,7 @@ msgstr "Выбрать сервисные аккаунты"
#~ msgid "Selected contexts ({0}): <0>{1}</0>" #~ msgid "Selected contexts ({0}): <0>{1}</0>"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:256 #: src/renderer/components/+add-cluster/add-cluster.tsx:277
msgid "Selected contexts: <0>{0}</0>" msgid "Selected contexts: <0>{0}</0>"
msgstr "" msgstr ""
@ -2476,13 +2476,13 @@ msgid "Settings"
msgstr "" msgstr ""
#: src/renderer/components/+nodes/node-menu.tsx:48 #: src/renderer/components/+nodes/node-menu.tsx:48
#: src/renderer/components/+workloads-pods/pod-menu.tsx:68 #: src/renderer/components/+workloads-pods/pod-menu.tsx:78
msgid "Shell" msgid "Shell"
msgstr "Командная строка" msgstr "Командная строка"
#: src/renderer/components/+config-secrets/secret-details.tsx:93 #: src/renderer/components/+config-secrets/secret-details.tsx:93
#: src/renderer/components/+workloads-pods/pod-container-env.tsx:101 #: src/renderer/components/+workloads-pods/pod-container-env.tsx:101
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19 #: src/renderer/components/drawer/drawer-param-toggler.tsx:19
msgid "Show" msgid "Show"
msgstr "Показать" msgstr "Показать"
@ -2491,10 +2491,22 @@ msgstr "Показать"
msgid "Show Notes" msgid "Show Notes"
msgstr "Показать логи" msgstr "Показать логи"
#: src/renderer/components/dock/pod-logs.tsx:160
msgid "Show current logs"
msgstr ""
#: src/renderer/components/dock/pod-logs.tsx:160
msgid "Show previous terminated container logs"
msgstr ""
#: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20 #: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20
msgid "Show value" msgid "Show value"
msgstr "Показать значение" msgstr "Показать значение"
#: src/renderer/components/dock/pod-logs.tsx:154
msgid "Since"
msgstr ""
#: src/renderer/components/+nodes/node-charts.tsx:80 #: src/renderer/components/+nodes/node-charts.tsx:80
#: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49 #: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49
msgid "Size" msgid "Size"
@ -2589,12 +2601,12 @@ msgstr "Тип стратегии"
msgid "Sub-object" msgid "Sub-object"
msgstr "Суб-объект" msgstr "Суб-объект"
#: src/renderer/components/dock/info-panel.tsx:93 #: src/renderer/components/dock/info-panel.tsx:95
#: src/renderer/components/wizard/wizard.tsx:131 #: src/renderer/components/wizard/wizard.tsx:131
msgid "Submit" msgid "Submit"
msgstr "Отправить" msgstr "Отправить"
#: src/renderer/components/dock/info-panel.tsx:94 #: src/renderer/components/dock/info-panel.tsx:96
msgid "Submitting.." msgid "Submitting.."
msgstr "Применение.." msgstr "Применение.."
@ -2602,7 +2614,7 @@ msgstr "Применение.."
msgid "Subsets" msgid "Subsets"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:102 #: src/renderer/components/+add-cluster/add-cluster.tsx:122
msgid "Successfully imported <0>{0}</0> cluster(s)" msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "" msgstr ""
@ -2636,7 +2648,7 @@ msgstr ""
msgid "Terminal" msgid "Terminal"
msgstr "Терминал" msgstr "Терминал"
#: src/renderer/components/dock/dock.tsx:89 #: src/renderer/components/dock/dock.tsx:95
msgid "Terminal session" msgid "Terminal session"
msgstr "Сессия терминала" msgstr "Сессия терминала"
@ -2644,7 +2656,7 @@ msgstr "Сессия терминала"
msgid "The path to the kubectl binary on the system." msgid "The path to the kubectl binary on the system."
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226 #: src/renderer/components/dock/pod-logs.tsx:172
msgid "There are no logs available for container." msgid "There are no logs available for container."
msgstr "Для контейнера нет логов." msgstr "Для контейнера нет логов."
@ -2774,8 +2786,8 @@ msgstr "Обновить версию"
msgid "Usage" msgid "Usage"
msgstr "Использование" msgstr "Использование"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:64
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Use configuration" msgid "Use configuration"
msgstr "" msgstr ""
@ -2962,7 +2974,7 @@ msgstr "сек"
msgid "singular" msgid "singular"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215 #: src/renderer/components/dock/pod-logs.tsx:159
msgid "timestamps" msgid "timestamps"
msgstr "временные метки" msgstr "временные метки"
@ -3007,8 +3019,8 @@ msgid "{metricsRemainCount} more..."
msgstr "{metricsRemainCount} еще…" msgstr "{metricsRemainCount} еще…"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240
msgid "{podName} Logs" #~ msgid "{podName} Logs"
msgstr "{podName} логи" #~ msgstr "{podName} логи"
#: src/renderer/components/dock/edit-resource.tsx:56 #: src/renderer/components/dock/edit-resource.tsx:56
msgid "{resourceType} <0>{resourceName}</0> updated." msgid "{resourceType} <0>{resourceName}</0> updated."
@ -3025,6 +3037,6 @@ msgstr ""
"other {<2>Удалить <3>{selectedCount}</3> элементов <4>{selectedNames}</4> {tail}?</2>}\n" "other {<2>Удалить <3>{selectedCount}</3> элементов <4>{selectedNames}</4> {tail}?</2>}\n"
"}" "}"
#: src/renderer/components/dock/info-panel.tsx:88 #: src/renderer/components/dock/info-panel.tsx:89
msgid "{submitLabel} & Close" msgid "{submitLabel} & Close"
msgstr "{submitLabel} и закрыть" msgstr "{submitLabel} и закрыть"

View File

@ -2,7 +2,7 @@
"name": "kontena-lens", "name": "kontena-lens",
"productName": "Lens", "productName": "Lens",
"description": "Lens - The Kubernetes IDE", "description": "Lens - The Kubernetes IDE",
"version": "3.6.5", "version": "3.6.6",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.", "copyright": "© 2020, Mirantis, Inc.",
"license": "MIT", "license": "MIT",
@ -183,6 +183,7 @@
"@kubernetes/client-node": "^0.12.0", "@kubernetes/client-node": "^0.12.0",
"array-move": "^3.0.0", "array-move": "^3.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"command-exists": "1.2.9",
"conf": "^7.0.1", "conf": "^7.0.1",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"electron-updater": "^4.3.1", "electron-updater": "^4.3.1",
@ -311,6 +312,7 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"include-media": "^1.4.9", "include-media": "^1.4.9",
"jest": "^26.0.1", "jest": "^26.0.1",
"jest-mock-extended": "^1.0.10",
"make-plural": "^6.2.1", "make-plural": "^6.2.1",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",

View File

@ -6,14 +6,10 @@ import { tracker } from "./tracker";
export const clusterIpc = { export const clusterIpc = {
activate: createIpcChannel({ activate: createIpcChannel({
channel: "cluster:activate", channel: "cluster:activate",
handle: (clusterId: ClusterId, frameId?: number) => { handle: (clusterId: ClusterId, force = false) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
if (frameId) { return cluster.activate(force);
cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
}
extensionLoader.broadcastExtensions(frameId)
return cluster.activate();
} }
}, },
}), }),
@ -24,6 +20,7 @@ export const clusterIpc = {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
extensionLoader.broadcastExtensions(frameId)
return cluster.pushState(); return cluster.pushState();
} }
}, },

View File

@ -0,0 +1,12 @@
export class ExecValidationNotFoundError extends Error {
constructor(execPath: string, isAbsolute: boolean) {
super(`User Exec command "${execPath}" not found on host.`);
let message = `User Exec command "${execPath}" not found on host.`;
if (!isAbsolute) {
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
}
this.message = message;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

View File

@ -4,6 +4,8 @@ import path from "path"
import os from "os" import os from "os"
import yaml from "js-yaml" import yaml from "js-yaml"
import logger from "../main/logger"; import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config'); export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config');
@ -140,3 +142,28 @@ export function getNodeWarningConditions(node: V1Node) {
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
) )
} }
/**
* Validates kubeconfig supplied in the add clusters screen. At present this will just validate
* the User struct, specifically the command passed to the exec substructure.
*/
export function validateKubeConfig (config: KubeConfig) {
// we only receive a single context, cluster & user object here so lets validate them as this
// will be called when we add a new cluster to Lens
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
// Validate the User Object
const user = config.getCurrentUser();
if (user.exec) {
const execCommand = user.exec["command"];
// check if the command is absolute or not
const isAbsolute = path.isAbsolute(execCommand);
// validate the exec struct in the user object, start with the command field
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
if (!commandExists.sync(execCommand)) {
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`);
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
}
}
}

View File

@ -0,0 +1,9 @@
// Debouncing promise evaluation
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
let timer: NodeJS.Timeout;
return (...params: any[]) => new Promise((resolve, reject) => {
clearTimeout(timer);
timer = setTimeout(() => resolve(func.apply(this, params)), timeout);
});
}

View File

@ -1,7 +1,14 @@
// Common utils (main/renderer) // Common utils (main OR renderer)
export * from "./app-version"
export * from "./autobind"
export * from "./base64" export * from "./base64"
export * from "./camelCase" export * from "./camelCase"
export * from "./splitArray" export * from "./cloneJson"
export * from "./getRandId" export * from "./debouncePromise"
export * from "./defineGlobal"
export * from "./getRandId"
export * from "./splitArray"
export * from "./saveToAppFiles"
export * from "./singleton"
export * from "./cloneJson" export * from "./cloneJson"

View File

@ -0,0 +1,165 @@
const logger = {
silly: jest.fn(),
debug: jest.fn(),
log: jest.fn(),
info: jest.fn(),
error: jest.fn(),
crit: jest.fn(),
};
jest.mock("winston", () => ({
format: {
colorize: jest.fn(),
combine: jest.fn(),
simple: jest.fn(),
label: jest.fn(),
timestamp: jest.fn(),
printf: jest.fn()
},
createLogger: jest.fn().mockReturnValue(logger),
transports: {
Console: jest.fn(),
File: jest.fn(),
}
}))
jest.mock("../../common/ipc")
jest.mock("../context-handler")
jest.mock("request")
jest.mock("request-promise-native")
import { Console } from "console";
import mockFs from "mock-fs";
import { workspaceStore } from "../../common/workspace-store";
import { Cluster } from "../cluster"
import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port";
import { V1ResourceAttributes } from "@kubernetes/client-node";
import { apiResources } from "../../common/rbac";
import request from "request-promise-native"
const mockedRequest = request as jest.MockedFunction<typeof request>
console = new Console(process.stdout, process.stderr) // fix mockFS
describe("create clusters", () => {
beforeEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
const mockOpts = {
"minikube-config.yml": JSON.stringify({
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
preferences: {},
})
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => {
const c = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
expect(c.apiUrl).toBe("https://192.168.64.3:8443")
})
it("init should not throw if everything is in order", async () => {
const c = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
await c.init(await getFreePort())
expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), {
id: "foo",
apiUrl: "https://192.168.64.3:8443",
context: "minikube",
})
})
it("activating cluster should try to connect to cluster and do a refresh", async () => {
const port = await getFreePort()
jest.spyOn(ContextHandler.prototype, "ensureServer");
const mockListNSs = jest.fn()
const mockKC = {
makeApiClient() {
return {
listNamespace: mockListNSs,
}
}
}
jest.spyOn(Cluster.prototype, "canI")
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default")
expect(attr.resource).toBe("pods")
expect(attr.verb).toBe("list")
return Promise.resolve(true)
})
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default")
expect(attr.verb).toBe("list")
return Promise.resolve(true)
})
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any)
mockListNSs.mockImplementationOnce(() => ({
body: {
items: [{
metadata: {
name: "default",
}
}]
}
}))
mockedRequest.mockImplementationOnce(((uri: any, _options: any) => {
expect(uri).toBe(`http://localhost:${port}/api-kube/version`)
return Promise.resolve({ gitVersion: "1.2.3" })
}) as any)
const c = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
await c.init(port)
await c.activate()
expect(ContextHandler.prototype.ensureServer).toBeCalled()
expect(mockedRequest).toBeCalled()
expect(c.accessible).toBe(true)
expect(c.allowedNamespaces.length).toBe(1)
expect(c.allowedResources.length).toBe(apiResources.length)
jest.resetAllMocks()
})
})

View File

@ -0,0 +1,130 @@
const logger = {
silly: jest.fn(),
debug: jest.fn(),
log: jest.fn(),
info: jest.fn(),
error: jest.fn(),
crit: jest.fn(),
};
jest.mock("winston", () => ({
format: {
colorize: jest.fn(),
combine: jest.fn(),
simple: jest.fn(),
label: jest.fn(),
timestamp: jest.fn(),
printf: jest.fn()
},
createLogger: jest.fn().mockReturnValue(logger),
transports: {
Console: jest.fn(),
File: jest.fn(),
}
}))
jest.mock("../../common/ipc")
jest.mock("child_process")
jest.mock("tcp-port-used")
import { Cluster } from "../cluster"
import { KubeAuthProxy } from "../kube-auth-proxy"
import { getFreePort } from "../port"
import { broadcastIpc } from "../../common/ipc"
import { ChildProcess, spawn, SpawnOptions } from "child_process"
import { bundledKubectlPath, Kubectl } from "../kubectl"
import { mock, MockProxy } from 'jest-mock-extended';
import { waitUntilUsed } from 'tcp-port-used';
import { Readable } from "stream"
const mockBroadcastIpc = broadcastIpc as jest.MockedFunction<typeof broadcastIpc>
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>
describe("kube auth proxy tests", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("calling exit multiple times shouldn't throw", async () => {
const port = await getFreePort()
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
kap.exit()
kap.exit()
kap.exit()
})
describe("spawn tests", () => {
let port: number
let mockedCP: MockProxy<ChildProcess>
let listeners: Record<string, (...args: any[]) => void>
beforeEach(async () => {
port = await getFreePort()
mockedCP = mock<ChildProcess>()
listeners = {}
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true))
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false))
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
listeners[event] = listener
return mockedCP
})
mockedCP.stderr = mock<Readable>()
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stderr/${event}`] = listener
return mockedCP.stderr
})
mockedCP.stdout = mock<Readable>()
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stdout/${event}`] = listener
return mockedCP.stdout
})
mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => {
expect(command).toBe(bundledKubectlPath())
return mockedCP
})
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve())
})
it("should call spawn and broadcast errors", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
listeners["error"]({ message: "foobarbat" })
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "foobarbat", error: true }] })
})
it("should call spawn and broadcast exit", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
listeners["exit"](0)
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "proxy exited with code: 0", error: false }] })
})
it("should call spawn and broadcast errors from stderr", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
listeners["stderr/data"]("an error")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "an error", error: true }] })
})
it("should call spawn and broadcast stdout serving info", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
listeners["stdout/data"]("Starting to serve on")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "Authentication proxy started\n" }] })
})
it("should call spawn and broadcast stdout other info", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
listeners["stdout/data"]("some info")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "some info" }] })
})
})
})

View File

@ -0,0 +1,112 @@
const logger = {
silly: jest.fn(),
debug: jest.fn(),
log: jest.fn(),
info: jest.fn(),
error: jest.fn(),
crit: jest.fn(),
};
jest.mock("winston", () => ({
format: {
colorize: jest.fn(),
combine: jest.fn(),
simple: jest.fn(),
label: jest.fn(),
timestamp: jest.fn(),
printf: jest.fn()
},
createLogger: jest.fn().mockReturnValue(logger),
transports: {
Console: jest.fn(),
File: jest.fn(),
}
}))
import { KubeconfigManager } from "../kubeconfig-manager"
import mockFs from "mock-fs"
import { Cluster } from "../cluster";
import { workspaceStore } from "../../common/workspace-store";
import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port";
import fse from "fs-extra"
import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console";
console = new Console(process.stdout, process.stderr) // fix mockFS
describe("kubeconfig manager tests", () => {
beforeEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
const mockOpts = {
"minikube-config.yml": JSON.stringify({
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
preferences: {},
})
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("should create 'temp' kube config with proxy", async () => {
const cluster = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
const contextHandler = new ContextHandler(cluster)
const port = await getFreePort()
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
expect(logger.error).not.toBeCalled()
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo")
const file = await fse.readFile(kubeConfManager.getPath())
const yml = loadYaml<any>(file.toString())
expect(yml["current-context"]).toBe("minikube")
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`)
expect(yml["users"][0]["name"]).toBe("proxy")
})
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const cluster = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
})
const contextHandler = new ContextHandler(cluster)
const port = await getFreePort()
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
const configPath = kubeConfManager.getPath()
expect(await fse.pathExists(configPath)).toBe(true)
await kubeConfManager.unlink()
expect(await fse.pathExists(configPath)).toBe(false)
await kubeConfManager.unlink() // doesn't throw
expect(kubeConfManager.getPath()).toBeUndefined()
})
})

View File

@ -46,6 +46,7 @@ export class Cluster implements ClusterModel {
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager; protected kubeconfigManager: KubeconfigManager;
protected eventDisposers: Function[] = []; protected eventDisposers: Function[] = [];
protected activated = false;
whenInitialized = when(() => this.initialized); whenInitialized = when(() => this.initialized);
whenReady = when(() => this.ready); whenReady = when(() => this.ready);
@ -93,7 +94,7 @@ export class Cluster implements ClusterModel {
async init(port: number) { async init(port: number) {
try { try {
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler, port); this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
this.initialized = true; this.initialized = true;
logger.info(`[CLUSTER]: "${this.contextName}" init success`, { logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
@ -126,7 +127,10 @@ export class Cluster implements ClusterModel {
} }
@action @action
async activate() { async activate(force = false ) {
if (this.activated && !force) {
return this.pushState();
}
logger.info(`[CLUSTER]: activate`, this.getMeta()); logger.info(`[CLUSTER]: activate`, this.getMeta());
await this.whenInitialized; await this.whenInitialized;
if (!this.eventDisposers.length) { if (!this.eventDisposers.length) {
@ -142,6 +146,7 @@ export class Cluster implements ClusterModel {
this.kubeCtl = new Kubectl(this.version) this.kubeCtl = new Kubectl(this.version)
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
} }
this.activated = true
return this.pushState(); return this.pushState();
} }
@ -162,6 +167,7 @@ export class Cluster implements ClusterModel {
this.online = false; this.online = false;
this.accessible = false; this.accessible = false;
this.ready = false; this.ready = false;
this.activated = false;
this.pushState(); this.pushState();
} }
@ -184,6 +190,7 @@ export class Cluster implements ClusterModel {
this.refreshEvents(), this.refreshEvents(),
this.refreshAllowedResources(), this.refreshAllowedResources(),
]); ]);
this.ready = true
} }
this.pushState(); this.pushState();
} }
@ -234,7 +241,7 @@ export class Cluster implements ClusterModel {
const apiUrl = this.kubeProxyUrl + path; const apiUrl = this.kubeProxyUrl + path;
return request(apiUrl, { return request(apiUrl, {
json: true, json: true,
timeout: 5000, timeout: 30000,
...options, ...options,
headers: { headers: {
Host: `${this.id}.${new URL(this.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest() Host: `${this.id}.${new URL(this.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()

View File

@ -2,7 +2,7 @@ import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl" import { Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
export interface KubeAuthProxyLog { export interface KubeAuthProxyLog {
@ -23,7 +23,7 @@ export class KubeAuthProxy {
this.env = env this.env = env
this.port = port this.port = port
this.cluster = cluster this.cluster = cluster
this.kubectl = bundledKubectl this.kubectl = Kubectl.bundled()
} }
public async run(): Promise<void> { public async run(): Promise<void> {

View File

@ -11,8 +11,12 @@ export class KubeconfigManager {
protected configDir = app.getPath("temp") protected configDir = app.getPath("temp")
protected tempFile: string; protected tempFile: string;
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
this.init();
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
const kcm = new KubeconfigManager(cluster, contextHandler, port)
await kcm.init()
return kcm
} }
protected async init() { protected async init() {
@ -72,8 +76,13 @@ export class KubeconfigManager {
return tempFile; return tempFile;
} }
unlink() { async unlink() {
if (!this.tempFile) {
return
}
logger.info('Deleting temporary kubeconfig: ' + this.tempFile) logger.info('Deleting temporary kubeconfig: ' + this.tempFile)
fs.unlinkSync(this.tempFile) await fs.unlink(this.tempFile)
this.tempFile = undefined
} }
} }

View File

@ -36,6 +36,9 @@ const packageMirrors: Map<string, string> = new Map([
let bundledPath: string let bundledPath: string
const initScriptVersionString = "# lens-initscript v3\n" const initScriptVersionString = "# lens-initscript v3\n"
export function bundledKubectlPath(): string {
if (bundledPath) { return bundledPath }
if (isDevelopment || isTestEnv) { if (isDevelopment || isTestEnv) {
const platformName = isWindows ? "windows" : process.platform const platformName = isWindows ? "windows" : process.platform
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl") bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
@ -47,6 +50,9 @@ if (isWindows) {
bundledPath = `${bundledPath}.exe` bundledPath = `${bundledPath}.exe`
} }
return bundledPath
}
export class Kubectl { export class Kubectl {
public kubectlVersion: string public kubectlVersion: string
protected directory: string protected directory: string
@ -58,7 +64,6 @@ export class Kubectl {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl") return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
} }
public static readonly bundledKubectlPath = bundledPath
public static readonly bundledKubectlVersion: string = bundledVersion public static readonly bundledKubectlVersion: string = bundledVersion
public static invalidBundle = false public static invalidBundle = false
private static bundledInstance: Kubectl; private static bundledInstance: Kubectl;
@ -102,7 +107,7 @@ export class Kubectl {
} }
public getBundledPath() { public getBundledPath() {
return Kubectl.bundledKubectlPath return bundledKubectlPath()
} }
public getPathFromPreferences() { public getPathFromPreferences() {
@ -125,19 +130,19 @@ export class Kubectl {
// return binary name if bundled path is not functional // return binary name if bundled path is not functional
if (!await this.checkBinary(this.getBundledPath(), false)) { if (!await this.checkBinary(this.getBundledPath(), false)) {
Kubectl.invalidBundle = true Kubectl.invalidBundle = true
return path.basename(bundledPath) return path.basename(this.getBundledPath())
} }
try { try {
if (!await this.ensureKubectl()) { if (!await this.ensureKubectl()) {
logger.error("Failed to ensure kubectl, fallback to the bundled version") logger.error("Failed to ensure kubectl, fallback to the bundled version")
return Kubectl.bundledKubectlPath return this.getBundledPath()
} }
return this.path return this.path
} catch (err) { } catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version") logger.error("Failed to ensure kubectl, fallback to the bundled version")
logger.error(err) logger.error(err)
return Kubectl.bundledKubectlPath return this.getBundledPath()
} }
} }
@ -183,7 +188,7 @@ export class Kubectl {
try { try {
const exist = await pathExists(this.path) const exist = await pathExists(this.path)
if (!exist) { if (!exist) {
await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path) await fs.promises.copyFile(this.getBundledPath(), this.path)
await fs.promises.chmod(this.path, 0o755) await fs.promises.chmod(this.path, 0o755)
} }
return true return true
@ -332,6 +337,3 @@ export class Kubectl {
return packageMirrors.get("default") // MacOS packages are only available from default return packageMirrors.get("default") // MacOS packages are only available from default
} }
} }
const bundledKubectl = Kubectl.bundled()
export { bundledKubectl }

View File

@ -1,20 +1,20 @@
import packageInfo from "../../package.json" import packageInfo from "../../package.json"
import path from "path" import path from "path"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl"; import { Kubectl } from "../../src/main/kubectl";
import { isWindows } from "../common/vars"; import { isWindows } from "../common/vars";
jest.mock("../common/user-store"); jest.mock("../common/user-store");
describe("kubectlVersion", () => { describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => { it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(bundledKubectl.kubectlVersion) const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
}) })
it("returns bundled version if same major.minor version is used", async () => { it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config; const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion); const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
}) })
}) })

View File

@ -1,7 +1,7 @@
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import { spawn, ChildProcessWithoutNullStreams } from "child_process" import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { bundledKubectl } from "../kubectl" import { Kubectl } from "../kubectl"
import { getFreePort } from "../port" import { getFreePort } from "../port"
import { shell } from "electron" import { shell } from "electron"
import * as tcpPortUsed from "tcp-port-used" import * as tcpPortUsed from "tcp-port-used"
@ -37,7 +37,7 @@ class PortForward {
public async start() { public async start() {
this.localPort = await getFreePort() this.localPort = await getFreePort()
const kubectlBin = await bundledKubectl.getPath() const kubectlBin = await Kubectl.bundled().getPath()
const args = [ const args = [
"--kubeconfig", this.kubeConfig, "--kubeconfig", this.kubeConfig,
"port-forward", "port-forward",

View File

@ -38,7 +38,7 @@ export class ShellSession extends EventEmitter {
public async open() { public async open() {
this.kubectlBinDir = await this.kubectl.binDir() this.kubectlBinDir = await this.kubectl.binDir()
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath()
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences) this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
this.helmBinDir = helmCli.getBinaryDir() this.helmBinDir = helmCli.getBinaryDir()
const env = await this.getCachedShellEnv() const env = await this.getCachedShellEnv()

View File

@ -3,7 +3,7 @@ import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuP
import type React from "react"; import type React from "react";
import { observable } from "mobx"; import { observable } from "mobx";
import { autobind } from "../utils/autobind"; import { autobind } from "../utils";
import { KubeApi } from "./kube-api"; import { KubeApi } from "./kube-api";
export interface ApiComponents { export interface ApiComponents {

View File

@ -42,11 +42,14 @@ export interface IPodMetrics<T = IMetrics> {
networkTransmit: T; networkTransmit: T;
} }
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
export interface IPodLogsQuery { export interface IPodLogsQuery {
container?: string; container?: string;
tailLines?: number; tailLines?: number;
timestamps?: boolean; timestamps?: boolean;
sinceTime?: string; // Date.toISOString()-format sinceTime?: string; // Date.toISOString()-format
follow?: boolean;
previous?: boolean;
} }
export enum PodStatus { export enum PodStatus {

View File

@ -13,7 +13,7 @@ import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { WizardLayout } from "../layout/wizard-layout"; import { WizardLayout } from "../layout/wizard-layout";
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig } from "../../../common/kube-helpers"; import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
@ -23,6 +23,7 @@ import { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Tab, Tabs } from "../tabs"; import { Tab, Tabs } from "../tabs";
import { ExecValidationNotFoundError } from "../../../common/custom-errors";
enum KubeConfigSourceTab { enum KubeConfigSourceTab {
FILE = "file", FILE = "file",
@ -118,6 +119,9 @@ export class AddCluster extends React.Component {
} }
addClusters = () => { addClusters = () => {
const configValidationErrors:string[] = [];
let newClusters: ClusterModel[] = [];
try { try {
if (!this.selectedContexts.length) { if (!this.selectedContexts.length) {
this.error = <Trans>Please select at least one cluster context</Trans> this.error = <Trans>Please select at least one cluster context</Trans>
@ -125,7 +129,22 @@ export class AddCluster extends React.Component {
} }
this.error = "" this.error = ""
this.isWaiting = true this.isWaiting = true
const newClusters: ClusterModel[] = this.selectedContexts.map(context => {
newClusters = this.selectedContexts.filter(context => {
try {
const kubeConfig = this.kubeContexts.get(context);
validateKubeConfig(kubeConfig);
return true;
} catch (err) {
this.error = String(err.message)
if (err instanceof ExecValidationNotFoundError ) {
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false;
} else {
throw new Error(err);
}
}
}).map(context => {
const clusterId = uuid(); const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context); const kubeConfig = this.kubeContexts.get(context);
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE
@ -141,7 +160,8 @@ export class AddCluster extends React.Component {
httpsProxy: this.proxyServer || undefined, httpsProxy: this.proxyServer || undefined,
}, },
} }
}); })
runInAction(() => { runInAction(() => {
clusterStore.addCluster(...newClusters); clusterStore.addCluster(...newClusters);
if (newClusters.length === 1) { if (newClusters.length === 1) {
@ -149,10 +169,12 @@ export class AddCluster extends React.Component {
clusterStore.setActive(clusterId); clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
} else { } else {
if (newClusters.length > 1) {
Notifications.ok( Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans> <Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
); );
} }
}
}) })
this.refreshContexts(); this.refreshContexts();
} catch (err) { } catch (err) {

View File

@ -46,8 +46,9 @@ export class ClusterSettings extends React.Component<Props> {
} }
} }
refreshCluster = () => { refreshCluster = async () => {
if(this.cluster) { if(this.cluster) {
await clusterIpc.activate.invokeFromRenderer(this.cluster.id);
clusterIpc.refresh.invokeFromRenderer(this.cluster.id); clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
} }
} }

View File

@ -6,7 +6,7 @@ import { Input } from '../input';
import { SubTitle } from '../layout/sub-title'; import { SubTitle } from '../layout/sub-title';
import { UserPreferences, userStore } from '../../../common/user-store'; import { UserPreferences, userStore } from '../../../common/user-store';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Kubectl } from '../../../main/kubectl'; import { bundledKubectlPath } from '../../../main/kubectl';
import { SelectOption, Select } from '../select'; import { SelectOption, Select } from '../select';
export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => { export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => {
@ -58,7 +58,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
<SubTitle title="Path to Kubectl binary" /> <SubTitle title="Path to Kubectl binary" />
<Input <Input
theme="round-black" theme="round-black"
placeholder={Kubectl.bundledKubectlPath} placeholder={bundledKubectlPath()}
value={binariesPath} value={binariesPath}
validators={isPath} validators={isPath}
onChange={setBinariesPath} onChange={setBinariesPath}

View File

@ -1,110 +0,0 @@
.PodLogsDialog {
--log-line-height: 16px;
.Wizard {
width: 90vw;
max-height: none;
.WizardStep {
& > .step-content.scrollable {
max-height: none;
}
& > :last-child {
padding: $padding * 2;
}
}
}
.log-controls {
padding-bottom: $padding * 2;
.time-range {
flex-grow: 2;
text-align: center;
}
.controls {
width: 100%;
}
.control-buttons {
margin-right: 0;
white-space: nowrap;
.Icon {
border-radius: $radius;
padding: 3px;
&:hover {
color: $textColorPrimary;
background: #f4f4f4;
}
&.active {
color: $primary;
background: #f4f4f4;
}
}
}
@include media("<=desktop") {
flex-direction: column;
align-items: start;
.container {
width: 100%;
}
.controls {
margin-top: $margin * 2;
.time-range {
text-align: left;
}
}
}
}
.logs-area {
position: relative;
@include custom-scrollbar;
// fix for `this.logsArea.scrollTop = this.logsArea.scrollHeight`
// `overflow: overlay` don't allow scroll to the last line
overflow: auto;
color: #C5C8C6;
background: #1D1F21;
line-height: var(--log-line-height);
border-radius: 2px;
height: 45vh;
padding: $padding / 4 $padding;
font-family: $font-monospace;
font-size: smaller;
white-space: pre;
.no-logs {
text-align: center;
}
}
.new-logs-sep {
position: relative;
display: block;
height: 0;
border-top: 1px solid $primary;
margin: $margin * 2;
&:after {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
content: 'new';
background: $primary;
color: white;
padding: $padding / 3 $padding /2;
border-radius: $radius;
}
}
}

View File

@ -1,307 +0,0 @@
import "./pod-logs-dialog.scss";
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { IPodContainer, Pod, podsApi } from "../../api/endpoints";
import { Icon } from "../icon";
import { Select, SelectOption } from "../select";
import { Spinner } from "../spinner";
import { cssNames, downloadFile, interval } from "../../utils";
import AnsiUp from "ansi_up";
import DOMPurify from "dompurify"
interface IPodLogsDialogData {
pod: Pod;
container?: IPodContainer;
}
interface Props extends Partial<DialogProps> {
}
@observer
export class PodLogsDialog extends React.Component<Props> {
@observable static isOpen = false;
@observable static data: IPodLogsDialogData = null;
static open(pod: Pod, container?: IPodContainer) {
PodLogsDialog.isOpen = true;
PodLogsDialog.data = { pod, container };
}
static close() {
PodLogsDialog.isOpen = false;
}
get data() {
return PodLogsDialog.data;
}
private logsArea: HTMLDivElement;
private refresher = interval(5, () => this.load());
private containers: IPodContainer[] = []
private initContainers: IPodContainer[] = []
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
private colorConverter = new AnsiUp();
@observable logs = ""; // latest downloaded logs for pod
@observable newLogs = ""; // new logs since dialog is open
@observable logsReady = false;
@observable selectedContainer: IPodContainer;
@observable showTimestamps = true;
@observable tailLines = 1000;
lineOptions = [
{ label: _i18n._(t`All logs`), value: Number.MAX_SAFE_INTEGER },
{ label: 1000, value: 1000 },
{ label: 10000, value: 10000 },
{ label: 100000, value: 100000 },
]
onOpen = async () => {
const { pod, container } = this.data;
this.containers = pod.getContainers();
this.initContainers = pod.getInitContainers();
this.selectedContainer = container || this.containers[0];
await this.load();
this.refresher.start();
}
onClose = () => {
this.resetLogs();
this.refresher.stop();
}
close = () => {
PodLogsDialog.close();
}
load = async () => {
if (!this.data) return;
const { pod } = this.data;
try {
// if logs already loaded, check the latest timestamp for getting updates only from this point
const logsTimestamps = this.getTimestamps(this.newLogs || this.logs);
let lastLogDate = new Date(0)
if (logsTimestamps) {
lastLogDate = new Date(logsTimestamps.slice(-1)[0]);
lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second
}
const namespace = pod.getNs();
const name = pod.getName();
const logs = await podsApi.getLogs({ namespace, name }, {
container: this.selectedContainer.name,
timestamps: true,
tailLines: this.tailLines ? this.tailLines : undefined,
sinceTime: lastLogDate.toISOString(),
});
if (!this.logs) {
this.logs = logs;
}
else if (logs) {
this.newLogs = `${this.newLogs}\n${logs}`.trim();
}
} catch (error) {
this.logs = [
_i18n._(t`Failed to load logs: ${error.message}`),
_i18n._(t`Reason: ${error.reason} (${error.code})`),
].join("\n")
}
this.logsReady = true;
}
reload = async () => {
this.resetLogs();
this.refresher.stop();
await this.load();
this.refresher.start();
}
componentDidUpdate() {
// scroll logs only when it's already in the end,
// otherwise it can interrupt reading by jumping after loading new logs update
if (this.logsArea && this.lastLineIsShown) {
this.logsArea.scrollTop = this.logsArea.scrollHeight;
}
}
onScroll = (evt: React.UIEvent<HTMLDivElement>) => {
const logsArea = evt.currentTarget;
const { scrollHeight, clientHeight, scrollTop } = logsArea;
this.lastLineIsShown = clientHeight + scrollTop === scrollHeight;
};
getLogs() {
const { logs, newLogs, showTimestamps } = this;
return {
logs: showTimestamps ? logs : this.removeTimestamps(logs),
newLogs: showTimestamps ? newLogs : this.removeTimestamps(newLogs),
}
}
getTimestamps(logs: string) {
return logs.match(/^\d+\S+/gm);
}
removeTimestamps(logs: string) {
return logs.replace(/^\d+.*?\s/gm, "");
}
resetLogs() {
this.logs = "";
this.newLogs = "";
this.lastLineIsShown = true;
this.logsReady = false;
}
onContainerChange = (option: SelectOption) => {
this.selectedContainer = this.containers
.concat(this.initContainers)
.find(container => container.name === option.value);
this.reload();
}
onTailLineChange = (option: SelectOption) => {
this.tailLines = option.value;
this.reload();
}
formatOptionLabel = (option: SelectOption) => {
const { value, label } = option;
return label || <><Icon small material="view_carousel"/> {value}</>;
}
toggleTimestamps = () => {
this.showTimestamps = !this.showTimestamps;
}
downloadLogs = () => {
const { logs, newLogs } = this.getLogs();
const fileName = this.selectedContainer.name + ".log";
const fileContents = logs + newLogs;
downloadFile(fileName, fileContents, "text/plain");
}
get containerSelectOptions() {
return [
{
label: _i18n._(t`Containers`),
options: this.containers.map(container => {
return { value: container.name }
}),
},
{
label: _i18n._(t`Init Containers`),
options: this.initContainers.map(container => {
return { value: container.name }
}),
}
];
}
renderControlsPanel() {
const { logsReady, showTimestamps } = this;
if (!logsReady) return;
const timestamps = this.getTimestamps(this.logs + this.newLogs);
let from = "";
let to = "";
if (timestamps) {
from = new Date(timestamps[0]).toLocaleString();
to = new Date(timestamps[timestamps.length - 1]).toLocaleString();
}
return (
<div className="controls flex align-center">
<div className="time-range">
{timestamps && <Trans>From <b>{from}</b> to <b>{to}</b></Trans>}
</div>
<div className="control-buttons flex gaps">
<Icon
material="av_timer"
onClick={this.toggleTimestamps}
className={cssNames("timestamps-icon", { active: showTimestamps })}
tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)}
/>
<Icon
material="get_app"
onClick={this.downloadLogs}
tooltip={_i18n._(t`Save`)}
/>
</div>
</div>
)
}
renderLogs() {
if (!this.logsReady) {
return <Spinner center/>
}
const { logs, newLogs } = this.getLogs();
if (!logs && !newLogs) {
return <p className="no-logs"><Trans>There are no logs available for container.</Trans></p>
}
return (
<>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(logs))}} />
{newLogs && (
<>
<p className="new-logs-sep" title={_i18n._(t`New logs since opening the dialog`)}/>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(newLogs))}} />
</>
)}
</>
);
}
render() {
const { ...dialogProps } = this.props;
const { selectedContainer, tailLines } = this;
const podName = this.data ? this.data.pod.getName() : "";
const header = <h5><Trans>{podName} Logs</Trans></h5>;
return (
<Dialog
{...dialogProps}
isOpen={PodLogsDialog.isOpen}
className="PodLogsDialog"
onOpen={this.onOpen}
onClose={this.onClose}
close={this.close}
>
<Wizard header={header} done={this.close}>
<WizardStep hideNextBtn prevLabel={<Trans>Close</Trans>}>
<div className="log-controls flex gaps align-center justify-space-between">
<div className="container flex gaps align-center">
<span><Trans>Container</Trans></span>
{selectedContainer && (
<Select
className="container-selector"
options={this.containerSelectOptions}
themeName="light"
value={{ value: selectedContainer.name }}
onChange={this.onContainerChange}
formatOptionLabel={this.formatOptionLabel}
autoConvertOptions={false}
/>
)}
<span><Trans>Lines</Trans></span>
<Select
value={tailLines}
options={this.lineOptions}
onChange={this.onTailLineChange}
themeName="light"
/>
</div>
{this.renderControlsPanel()}
</div>
<div className="logs-area" onScroll={this.onScroll} ref={e => this.logsArea = e}>
{this.renderLogs()}
</div>
</WizardStep>
</Wizard>
</Dialog>
)
}
}

View File

@ -3,15 +3,15 @@ import "./pod-menu.scss";
import React from "react"; import React from "react";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { MenuItem, SubMenu } from "../menu"; import { MenuItem, SubMenu } from "../menu";
import { IPodContainer, Pod, nodesApi } from "../../api/endpoints"; import { IPodContainer, Pod } from "../../api/endpoints";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { StatusBrick } from "../status-brick"; import { StatusBrick } from "../status-brick";
import { PodLogsDialog } from "./pod-logs-dialog";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { cssNames, prevDefault } from "../../utils"; import { cssNames, prevDefault } from "../../utils";
import { terminalStore, createTerminalTab } from "../dock/terminal.store"; import { terminalStore, createTerminalTab } from "../dock/terminal.store";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { hideDetails } from "../../navigation"; import { hideDetails } from "../../navigation";
import { createPodLogsTab } from "../dock/pod-logs.store";
interface Props extends KubeObjectMenuProps<Pod> { interface Props extends KubeObjectMenuProps<Pod> {
} }
@ -42,7 +42,17 @@ export class PodMenu extends React.Component<Props> {
} }
showLogs(container: IPodContainer) { showLogs(container: IPodContainer) {
PodLogsDialog.open(this.props.object, container); hideDetails();
const pod = this.props.object;
createPodLogsTab({
pod,
containers: pod.getContainers(),
initContainers: pod.getInitContainers(),
selectedContainer: container,
showTimestamps: false,
previous: false,
tailLines: 1000
});
} }
renderShellMenu() { renderShellMenu() {

View File

@ -23,7 +23,6 @@ import { eventRoute } from "./+events";
import { Apps, appsRoute } from "./+apps"; import { Apps, appsRoute } from "./+apps";
import { KubeObjectDetails } from "./kube-object/kube-object-details"; import { KubeObjectDetails } from "./kube-object/kube-object-details";
import { AddRoleBindingDialog } from "./+user-management-roles-bindings"; import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
import { PodLogsDialog } from "./+workloads-pods/pod-logs-dialog";
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog"; import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog"; import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
import { CustomResources } from "./+custom-resources/custom-resources"; import { CustomResources } from "./+custom-resources/custom-resources";
@ -90,7 +89,6 @@ export class App extends React.Component {
<KubeObjectDetails/> <KubeObjectDetails/>
<KubeConfigDialog/> <KubeConfigDialog/>
<AddRoleBindingDialog/> <AddRoleBindingDialog/>
<PodLogsDialog/>
<DeploymentScaleDialog/> <DeploymentScaleDialog/>
<CronJobTriggerDialog/> <CronJobTriggerDialog/>
</ErrorBoundary> </ErrorBoundary>

View File

@ -47,13 +47,14 @@ export class ClusterStatus extends React.Component<Props> {
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`); ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
} }
activateCluster = async () => { activateCluster = async (force = false) => {
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId); await clusterIpc.activate.invokeFromRenderer(this.props.clusterId, force);
} }
reconnect = async () => { reconnect = async () => {
this.authOutput = []
this.isReconnecting = true; this.isReconnecting = true;
await this.activateCluster(); await this.activateCluster(true);
this.isReconnecting = false; this.isReconnecting = false;
} }

View File

@ -9,7 +9,7 @@ import { hasLoadedView } from "./lens-views";
export class ClusterView extends React.Component { export class ClusterView extends React.Component {
render() { render() {
const cluster = getMatchedCluster(); const cluster = getMatchedCluster();
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id)) const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready)
return ( return (
<div className="ClusterView"> <div className="ClusterView">
{showStatus && ( {showStatus && (

View File

@ -11,6 +11,7 @@ export enum TabKind {
EDIT_RESOURCE = "edit-resource", EDIT_RESOURCE = "edit-resource",
INSTALL_CHART = "install-chart", INSTALL_CHART = "install-chart",
UPGRADE_CHART = "upgrade-chart", UPGRADE_CHART = "upgrade-chart",
POD_LOGS = "pod-logs",
} }
export interface IDockTab { export interface IDockTab {

View File

@ -22,6 +22,8 @@ import { createResourceTab, isCreateResourceTab } from "./create-resource.store"
import { isEditResourceTab } from "./edit-resource.store"; import { isEditResourceTab } from "./edit-resource.store";
import { isInstallChartTab } from "./install-chart.store"; import { isInstallChartTab } from "./install-chart.store";
import { isUpgradeChartTab } from "./upgrade-chart.store"; import { isUpgradeChartTab } from "./upgrade-chart.store";
import { PodLogs } from "./pod-logs";
import { isPodLogsTab } from "./pod-logs.store";
interface Props { interface Props {
className?: string; className?: string;
@ -59,6 +61,9 @@ export class Dock extends React.Component<Props> {
if (isInstallChartTab(tab) || isUpgradeChartTab(tab)) { if (isInstallChartTab(tab) || isUpgradeChartTab(tab)) {
return <DockTab value={tab} icon={<Icon svg="install" />} /> return <DockTab value={tab} icon={<Icon svg="install" />} />
} }
if (isPodLogsTab(tab)) {
return <DockTab value={tab} icon="subject" />
}
} }
renderTabContent() { renderTabContent() {
@ -71,6 +76,7 @@ export class Dock extends React.Component<Props> {
{isInstallChartTab(tab) && <InstallChart tab={tab} />} {isInstallChartTab(tab) && <InstallChart tab={tab} />}
{isUpgradeChartTab(tab) && <UpgradeChart tab={tab} />} {isUpgradeChartTab(tab) && <UpgradeChart tab={tab} />}
{isTerminalTab(tab) && <TerminalWindow tab={tab} />} {isTerminalTab(tab) && <TerminalWindow tab={tab} />}
{isPodLogsTab(tab) && <PodLogs tab={tab} />}
</div> </div>
) )
} }

View File

@ -13,7 +13,7 @@ import { Notifications } from "../notifications";
interface Props extends OptionalProps { interface Props extends OptionalProps {
tabId: TabId; tabId: TabId;
submit: () => Promise<ReactNode | string>; submit?: () => Promise<ReactNode | string>;
} }
interface OptionalProps { interface OptionalProps {
@ -23,6 +23,7 @@ interface OptionalProps {
submitLabel?: ReactNode; submitLabel?: ReactNode;
submittingMessage?: ReactNode; submittingMessage?: ReactNode;
disableSubmit?: boolean; disableSubmit?: boolean;
showButtons?: boolean
showSubmitClose?: boolean; showSubmitClose?: boolean;
showInlineInfo?: boolean; showInlineInfo?: boolean;
showNotifications?: boolean; showNotifications?: boolean;
@ -33,6 +34,7 @@ export class InfoPanel extends Component<Props> {
static defaultProps: OptionalProps = { static defaultProps: OptionalProps = {
submitLabel: <Trans>Submit</Trans>, submitLabel: <Trans>Submit</Trans>,
submittingMessage: <Trans>Submitting..</Trans>, submittingMessage: <Trans>Submitting..</Trans>,
showButtons: true,
showSubmitClose: true, showSubmitClose: true,
showInlineInfo: true, showInlineInfo: true,
showNotifications: true, showNotifications: true,
@ -87,7 +89,7 @@ export class InfoPanel extends Component<Props> {
} }
render() { render() {
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showSubmitClose } = this.props; const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props;
const { submit, close, submitAndClose, waiting } = this; const { submit, close, submitAndClose, waiting } = this;
const isDisabled = !!(disableSubmit || waiting || error); const isDisabled = !!(disableSubmit || waiting || error);
return ( return (
@ -98,6 +100,8 @@ export class InfoPanel extends Component<Props> {
<div className="info flex gaps align-center"> <div className="info flex gaps align-center">
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()} {waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
</div> </div>
{showButtons && (
<>
<Button plain label={<Trans>Cancel</Trans>} onClick={close} /> <Button plain label={<Trans>Cancel</Trans>} onClick={close} />
<Button <Button
active active
@ -115,6 +119,8 @@ export class InfoPanel extends Component<Props> {
disabled={isDisabled} disabled={isDisabled}
/> />
)} )}
</>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,43 @@
.PodLogs {
.logs {
@include custom-scrollbar;
// fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight`
// `overflow: overlay` don't allow scroll to the last line
overflow: auto;
color: $textColorAccent;
background: $logsBackground;
line-height: var(--log-line-height);
border-radius: 2px;
padding: $padding * 2;
font-family: $font-monospace;
font-size: smaller;
white-space: pre;
flex-grow: 1;
> div {
// Provides font better readability on large screens
-webkit-font-smoothing: subpixel-antialiased;
}
}
.new-logs-sep {
position: relative;
display: block;
height: 0;
border-top: 1px solid $primary;
margin: $margin * 2;
&:after {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
content: 'new';
background: $primary;
color: white;
padding: $padding / 3;
border-radius: $radius;
}
}
}

View File

@ -0,0 +1,128 @@
import { autorun, observable } from "mobx";
import { Pod, IPodContainer, podsApi } from "../../api/endpoints";
import { autobind, interval } from "../../utils";
import { DockTabStore } from "./dock-tab.store";
import { dockStore, IDockTab, TabKind } from "./dock.store";
import { t } from "@lingui/macro";
import { _i18n } from "../../i18n";
export interface IPodLogsData {
pod: Pod;
selectedContainer: IPodContainer
containers: IPodContainer[]
initContainers: IPodContainer[]
showTimestamps: boolean
tailLines: number
previous: boolean
}
type TabId = string;
interface PodLogs {
oldLogs?: string
newLogs?: string
}
@autobind()
export class PodLogsStore extends DockTabStore<IPodLogsData> {
private refresher = interval(10, () => this.load(dockStore.selectedTabId));
@observable logs = observable.map<TabId, PodLogs>();
constructor() {
super({
storageName: "pod_logs"
});
autorun(() => {
const { selectedTab, isOpen } = dockStore;
if (isPodLogsTab(selectedTab) && isOpen) {
this.refresher.start();
} else {
this.refresher.stop();
}
}, { delay: 500 });
}
load = async (tabId: TabId) => {
if (!this.logs.has(tabId)) {
this.logs.set(tabId, { oldLogs: "", newLogs: "" })
}
const data = this.getData(tabId);
const { oldLogs, newLogs } = this.logs.get(tabId);
const { selectedContainer, tailLines, previous } = data;
const pod = new Pod(data.pod);
try {
// if logs already loaded, check the latest timestamp for getting updates only from this point
const logsTimestamps = this.getTimestamps(newLogs || oldLogs);
let lastLogDate = new Date(0);
if (logsTimestamps) {
lastLogDate = new Date(logsTimestamps.slice(-1)[0]);
lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second
}
const namespace = pod.getNs();
const name = pod.getName();
const loadedLogs = await podsApi.getLogs({ namespace, name }, {
sinceTime: lastLogDate.toISOString(),
timestamps: true, // Always setting timestampt to separate old logs from new ones
container: selectedContainer.name,
tailLines,
previous
});
if (!oldLogs) {
this.logs.set(tabId, { oldLogs: loadedLogs, newLogs });
} else {
this.logs.set(tabId, { oldLogs, newLogs: loadedLogs });
}
} catch ({error}) {
this.logs.set(tabId, {
oldLogs: [
_i18n._(t`Failed to load logs: ${error.message}`),
_i18n._(t`Reason: ${error.reason} (${error.code})`)
].join("\n"),
newLogs
});
}
}
getTimestamps(logs: string) {
return logs.match(/^\d+\S+/gm);
}
removeTimestamps(logs: string) {
return logs.replace(/^\d+.*?\s/gm, "");
}
clearLogs(tabId: TabId) {
this.logs.delete(tabId);
}
clearData(tabId: TabId) {
this.data.delete(tabId);
this.clearLogs(tabId);
}
}
export const podLogsStore = new PodLogsStore();
export function createPodLogsTab(data: IPodLogsData, tabParams: Partial<IDockTab> = {}) {
const podId = data.pod.getId();
let tab = dockStore.getTabById(podId);
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
return;
}
// If no existent tab found
tab = dockStore.createTab({
id: podId,
kind: TabKind.POD_LOGS,
title: `Logs: ${data.pod.getName()}`,
...tabParams
}, false);
podLogsStore.setData(tab.id, data);
return tab;
}
export function isPodLogsTab(tab: IDockTab) {
return tab && tab.kind === TabKind.POD_LOGS;
}

View File

@ -0,0 +1,246 @@
import "./pod-logs.scss";
import React from "react";
import AnsiUp from "ansi_up";
import DOMPurify from "dompurify";
import { t, Trans } from "@lingui/macro";
import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { _i18n } from "../../i18n";
import { autobind, cssNames, downloadFile } from "../../utils";
import { Icon } from "../icon";
import { Select, SelectOption } from "../select";
import { Spinner } from "../spinner";
import { IDockTab } from "./dock.store";
import { InfoPanel } from "./info-panel";
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
interface Props {
className?: string
tab: IDockTab
}
@observer
export class PodLogs extends React.Component<Props> {
@observable ready = false;
private logsElement: HTMLDivElement;
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
private colorConverter = new AnsiUp();
private lineOptions = [
{ label: _i18n._(t`All logs`), value: Number.MAX_SAFE_INTEGER },
{ label: 1000, value: 1000 },
{ label: 10000, value: 10000 },
{ label: 100000, value: 100000 },
];
componentDidMount() {
disposeOnUnmount(this,
reaction(() => this.props.tab.id, async () => {
if (podLogsStore.logs.has(this.tabId)) {
this.ready = true;
return;
}
await this.load();
}, { fireImmediately: true })
);
}
componentDidUpdate() {
// scroll logs only when it's already in the end,
// otherwise it can interrupt reading by jumping after loading new logs update
if (this.logsElement && this.lastLineIsShown) {
this.logsElement.scrollTop = this.logsElement.scrollHeight;
}
}
get tabData() {
return podLogsStore.getData(this.tabId);
}
get tabId() {
return this.props.tab.id;
}
@autobind()
save(data: Partial<IPodLogsData>) {
podLogsStore.setData(this.tabId, { ...this.tabData, ...data });
}
load = async () => {
this.ready = false;
await podLogsStore.load(this.tabId);
this.ready = true;
}
reload = async () => {
podLogsStore.clearLogs(this.tabId);
this.lastLineIsShown = true;
await this.load();
}
@computed
get logs() {
if (!podLogsStore.logs.has(this.tabId)) return;
const { oldLogs, newLogs } = podLogsStore.logs.get(this.tabId);
const { getData, removeTimestamps } = podLogsStore;
const { showTimestamps } = getData(this.tabId);
return {
oldLogs: showTimestamps ? oldLogs : removeTimestamps(oldLogs),
newLogs: showTimestamps ? newLogs : removeTimestamps(newLogs)
}
}
toggleTimestamps = () => {
this.save({ showTimestamps: !this.tabData.showTimestamps });
}
togglePrevious = () => {
this.save({ previous: !this.tabData.previous });
this.reload();
}
onScroll = (evt: React.UIEvent<HTMLDivElement>) => {
const logsArea = evt.currentTarget;
const { scrollHeight, clientHeight, scrollTop } = logsArea;
this.lastLineIsShown = clientHeight + scrollTop === scrollHeight;
};
downloadLogs = () => {
const { oldLogs, newLogs } = this.logs;
const { pod, selectedContainer } = this.tabData;
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
const fileContents = oldLogs + newLogs;
downloadFile(fileName + ".log", fileContents, "text/plain");
}
onContainerChange = (option: SelectOption) => {
const { containers, initContainers } = this.tabData;
this.save({
selectedContainer: containers
.concat(initContainers)
.find(container => container.name === option.value)
})
this.reload();
}
onTailLineChange = (option: SelectOption) => {
this.save({ tailLines: option.value })
this.reload();
}
get containerSelectOptions() {
const { containers, initContainers } = this.tabData;
return [
{
label: _i18n._(t`Containers`),
options: containers.map(container => {
return { value: container.name }
}),
},
{
label: _i18n._(t`Init Containers`),
options: initContainers.map(container => {
return { value: container.name }
}),
}
];
}
formatOptionLabel = (option: SelectOption) => {
const { value, label } = option;
return label || <><Icon small material="view_carousel"/> {value}</>;
}
renderControls() {
if (!this.ready) return null;
const { selectedContainer, showTimestamps, tailLines, previous } = this.tabData;
const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs);
return (
<div className="controls flex gaps align-center">
<span><Trans>Container</Trans></span>
<Select
options={this.containerSelectOptions}
value={{ value: selectedContainer.name }}
formatOptionLabel={this.formatOptionLabel}
onChange={this.onContainerChange}
autoConvertOptions={false}
/>
<span><Trans>Lines</Trans></span>
<Select
value={tailLines}
options={this.lineOptions}
onChange={this.onTailLineChange}
/>
<div className="time-range">
{timestamps && (
<>
<Trans>Since</Trans>{" "}
<b>{new Date(timestamps[0]).toLocaleString()}</b>
</>
)}
</div>
<div className="flex gaps">
<Icon
material="av_timer"
onClick={this.toggleTimestamps}
className={cssNames("timestamps-icon", { active: showTimestamps })}
tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)}
/>
<Icon
material="undo"
onClick={this.togglePrevious}
className={cssNames("undo-icon", { active: previous })}
tooltip={(previous ? _i18n._(t`Show current logs`) : _i18n._(t`Show previous terminated container logs`))}
/>
<Icon
material="get_app"
onClick={this.downloadLogs}
tooltip={_i18n._(t`Save`)}
/>
</div>
</div>
);
}
renderLogs() {
if (!this.ready) {
return <Spinner center/>;
}
const { oldLogs, newLogs } = this.logs;
if (!oldLogs && !newLogs) {
return (
<div className="flex align-center justify-center">
<Trans>There are no logs available for container.</Trans>
</div>
);
}
return (
<>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(oldLogs))}} />
{newLogs && (
<>
<p className="new-logs-sep" title={_i18n._(t`New logs since opening the dialog`)}/>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(newLogs))}} />
</>
)}
</>
);
}
render() {
const { className } = this.props;
return (
<div className={cssNames("PodLogs flex column", className)}>
<InfoPanel
tabId={this.props.tab.id}
controls={this.renderControls()}
showSubmitClose={false}
showButtons={false}
/>
<div className="logs" onScroll={this.onScroll} ref={e => this.logsElement = e}>
{this.renderLogs()}
</div>
</div>
);
}
}

View File

@ -5,7 +5,7 @@ import { FitAddon } from "xterm-addon-fit";
import { dockStore, TabId } from "./dock.store"; import { dockStore, TabId } from "./dock.store";
import { TerminalApi } from "../../api/terminal-api"; import { TerminalApi } from "../../api/terminal-api";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { autobind } from "../../utils/autobind"; import { autobind } from "../../utils";
export class Terminal { export class Terminal {
static spawningPool: HTMLElement; static spawningPool: HTMLElement;

View File

@ -39,7 +39,7 @@ export const isNumber: Validator = {
export const isUrl: Validator = { export const isUrl: Validator = {
condition: ({ type }) => type === "url", condition: ({ type }) => type === "url",
message: () => _i18n._(t`Wrong url format`), message: () => _i18n._(t`Wrong url format`),
validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
}; };
export const isPath: Validator = { export const isPath: Validator = {

View File

@ -7,7 +7,11 @@
> label { > label {
color: inherit; color: inherit;
border-radius: $radius; border-radius: $radius;
padding: $padding / 1.33 $padding * 1.25; padding: 6px 6px 6px 10px;
.Icon {
height: $margin * 2;
}
} }
&.compact { &.compact {

View File

@ -15,6 +15,14 @@
white-space: nowrap; white-space: nowrap;
} }
.NamespaceSelect {
.Select {
&__value-container {
margin-bottom: 0;
}
}
}
.SearchInput { .SearchInput {
label { label {
background: none; background: none;

View File

@ -1,6 +1,4 @@
.MainLayout { .MainLayout {
--sidebar-max-size: 200px;
display: grid; display: grid;
grid-template-areas: grid-template-areas:
"aside header" "aside header"
@ -35,7 +33,7 @@
transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);
&.pinned { &.pinned {
width: var(--sidebar-max-size); width: var(--sidebar-width);
} }
&:not(.pinned) { &:not(.pinned) {
@ -45,7 +43,7 @@
overflow: hidden; overflow: hidden;
&.accessible:hover { &.accessible:hover {
width: var(--sidebar-max-size); width: var(--sidebar-width);
transition-delay: 750ms; transition-delay: 750ms;
box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35); box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35);
z-index: $zIndex-sidebar-hover; z-index: $zIndex-sidebar-hover;

View File

@ -3,11 +3,12 @@ import "./main-layout.scss";
import React from "react"; import React from "react";
import { observable, reaction } from "mobx"; import { observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { createStorage, cssNames } from "../../utils"; import { autobind, createStorage, cssNames } from "../../utils";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { ErrorBoundary } from "../error-boundary"; import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock"; import { Dock } from "../dock";
import { getHostedCluster } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
export interface MainLayoutProps { export interface MainLayoutProps {
className?: any; className?: any;
@ -18,22 +19,42 @@ export interface MainLayoutProps {
@observer @observer
export class MainLayout extends React.Component<MainLayoutProps> { export class MainLayout extends React.Component<MainLayoutProps> {
public storage = createStorage("main_layout", { pinnedSidebar: true }); public storage = createStorage("main_layout", {
pinnedSidebar: true,
sidebarWidth: 200,
});
@observable isPinned = this.storage.get().pinnedSidebar; @observable isPinned = this.storage.get().pinnedSidebar;
@observable isAccessible = true; @observable isAccessible = true;
@observable sidebarWidth = this.storage.get().sidebarWidth
@disposeOnUnmount syncPinnedStateWithStorage = reaction( @disposeOnUnmount syncPinnedStateWithStorage = reaction(
() => this.isPinned, () => this.isPinned,
(isPinned) => this.storage.merge({ pinnedSidebar: isPinned }) (isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
); );
@disposeOnUnmount syncWidthStateWithStorage = reaction(
() => this.sidebarWidth,
(sidebarWidth) => this.storage.merge({ sidebarWidth })
);
toggleSidebar = () => { toggleSidebar = () => {
this.isPinned = !this.isPinned; this.isPinned = !this.isPinned;
this.isAccessible = false; this.isAccessible = false;
setTimeout(() => (this.isAccessible = true), 250); setTimeout(() => (this.isAccessible = true), 250);
}; };
getSidebarSize = () => {
return {
"--sidebar-width": `${this.sidebarWidth}px`,
}
}
@autobind()
adjustWidth(newWidth: number): void {
this.sidebarWidth = newWidth
}
render() { render() {
const { className, headerClass, footer, footerClass, children } = this.props; const { className, headerClass, footer, footerClass, children } = this.props;
const cluster = getHostedCluster(); const cluster = getHostedCluster();
@ -41,20 +62,31 @@ export class MainLayout extends React.Component<MainLayoutProps> {
return null; // fix: skip render when removing active (visible) cluster return null; // fix: skip render when removing active (visible) cluster
} }
return ( return (
<div className={cssNames("MainLayout", className)}> <div className={cssNames("MainLayout", className)} style={this.getSidebarSize() as any}>
<header className={cssNames("flex gaps align-center", headerClass)}> <header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span> <span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
</header> </header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}> <aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
<Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} /> <Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} />
<ResizingAnchor
direction={ResizeDirection.HORIZONTAL}
placement={ResizeSide.TRAILING}
growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT}
getCurrentExtent={() => this.sidebarWidth}
onDrag={this.adjustWidth}
onDoubleClick={this.toggleSidebar}
disabled={!this.isPinned}
minExtent={120}
maxExtent={400}
/>
</aside> </aside>
<main> <main>
<ErrorBoundary>{children}</ErrorBoundary> <ErrorBoundary>{children}</ErrorBoundary>
</main> </main>
<footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer> <footer className={footerClass}>{footer ?? <Dock />}</footer>
</div> </div>
); );
} }

View File

@ -1,5 +1,5 @@
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { autobind } from "./utils/autobind"; import { autobind } from "./utils";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import logger from "../main/logger"; import logger from "../main/logger";

View File

@ -60,6 +60,7 @@
"dockHeadBackground": "#2e3136", "dockHeadBackground": "#2e3136",
"dockInfoBackground": "#1e2125", "dockInfoBackground": "#1e2125",
"dockInfoBorderColor": "#303136", "dockInfoBorderColor": "#303136",
"logsBackground": "#000000",
"terminalBackground": "#000000", "terminalBackground": "#000000",
"terminalForeground": "#ffffff", "terminalForeground": "#ffffff",
"terminalCursor": "#ffffff", "terminalCursor": "#ffffff",

View File

@ -61,6 +61,7 @@
"dockHeadBackground": "#e8e8e8", "dockHeadBackground": "#e8e8e8",
"dockInfoBackground": "#e8e8e8", "dockInfoBackground": "#e8e8e8",
"dockInfoBorderColor": "#c9cfd3", "dockInfoBorderColor": "#c9cfd3",
"logsBackground": "#ffffff",
"terminalBackground": "#ffffff", "terminalBackground": "#ffffff",
"terminalForeground": "#2d2d2d", "terminalForeground": "#2d2d2d",
"terminalCursor": "#2d2d2d", "terminalCursor": "#2d2d2d",

View File

@ -91,6 +91,9 @@ $terminalBrightMagenta: var(--terminalBrightMagenta);
$terminalBrightCyan: var(--terminalBrightCyan); $terminalBrightCyan: var(--terminalBrightCyan);
$terminalBrightWhite: var(--terminalBrightWhite); $terminalBrightWhite: var(--terminalBrightWhite);
// Logs
$logsBackground: var(--logsBackground);
// Dialogs // Dialogs
$dialogTextColor: var(--dialogTextColor); $dialogTextColor: var(--dialogTextColor);
$dialogBackground: var(--dialogBackground); $dialogBackground: var(--dialogBackground);

View File

@ -1,9 +0,0 @@
// Debouncing promise evaluation
export const debouncePromise = function (promisedFunc: Function, timeout = 0) {
let timer: number;
return (...params: any[]) => new Promise((resolve, reject) => {
clearTimeout(timer);
timer = window.setTimeout(() => resolve(promisedFunc.apply(this, params)), timeout);
});
};

View File

@ -3,21 +3,18 @@
export const noop: any = Function(); export const noop: any = Function();
export const isElectron = !!navigator.userAgent.match(/Electron/); export const isElectron = !!navigator.userAgent.match(/Electron/);
export * from '../../common/utils/camelCase' export * from "../../common/utils"
export * from '../../common/utils/base64'
export * from './autobind' export * from "./cssVar"
export * from './cssVar' export * from "./cssNames"
export * from './cssNames' export * from "./eventEmitter"
export * from './eventEmitter' export * from "./downloadFile"
export * from './downloadFile' export * from "./prevDefault"
export * from './prevDefault' export * from "./createStorage"
export * from './createStorage' export * from "./interval"
export * from './interval' export * from "./copyToClipboard"
export * from './debouncePromise' export * from "./formatDuration"
export * from './copyToClipboard' export * from "./isReactNode"
export * from './formatDuration' export * from "./convertMemory"
export * from './isReactNode' export * from "./convertCpu"
export * from './convertMemory' export * from "./metricUnitsToNumber"
export * from './convertCpu'
export * from './metricUnitsToNumber'

View File

@ -2,7 +2,12 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 3.6.5 (current version) ## 3.6.6 (current version)
- Fix labels' word boundary to cover only drawer badges
- Fix cluster dashboard opening not to start authentication proxy twice
- Fix: Refresh cluster connection status also when connection is disconnected
## 3.6.5
- Prevent drawer close when revealing secret value - Prevent drawer close when revealing secret value
- Fix app crash when CRD conditions were not present - Fix app crash when CRD conditions were not present
- Add support for Stacklight prometheus metrics - Add support for Stacklight prometheus metrics

16
types/command-exists.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// Type definitions for command-exists 1.2
// Project: https://github.com/mathisonian/command-exists
// Definitions by: BendingBender <https://github.com/BendingBender>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export = commandExists;
declare function commandExists(commandName: string): Promise<string>;
declare function commandExists(
commandName: string,
cb: (error: null, exists: boolean) => void
): void;
declare namespace commandExists {
function sync(commandName: string): boolean;
}

View File

@ -4122,6 +4122,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
command-exists@1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
commander@*, commander@^5.1.0: commander@*, commander@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
@ -7599,6 +7604,13 @@ jest-message-util@^26.0.1:
slash "^3.0.0" slash "^3.0.0"
stack-utils "^2.0.2" stack-utils "^2.0.2"
jest-mock-extended@^1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.10.tgz#a4b1f5b0bb1121acf7c58cd5423d04c473532702"
integrity sha512-R2wKiOgEUPoHZ2kLsAQeQP2IfVEgo3oQqWLSXKdMXK06t3UHkQirA2Xnsdqg/pX6KPWTsdnrzE2ig6nqNjdgVw==
dependencies:
ts-essentials "^4.0.0"
jest-mock@^26.0.1: jest-mock@^26.0.1:
version "26.0.1" version "26.0.1"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.0.1.tgz#7fd1517ed4955397cf1620a771dc2d61fad8fd40" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.0.1.tgz#7fd1517ed4955397cf1620a771dc2d61fad8fd40"
@ -12692,6 +12704,11 @@ truncate-utf8-bytes@^1.0.0:
dependencies: dependencies:
utf8-byte-length "^1.0.1" utf8-byte-length "^1.0.1"
ts-essentials@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab"
integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==
ts-jest@^26.1.0: ts-jest@^26.1.0:
version "26.3.0" version "26.3.0"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.3.0.tgz#6b2845045347dce394f069bb59358253bc1338a9" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.3.0.tgz#6b2845045347dce394f069bb59358253bc1338a9"