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:
commit
a00d26149c
@ -8,8 +8,8 @@ import { Application } from "spectron"
|
||||
import * as util from "../helpers/utils"
|
||||
import { spawnSync } from "child_process"
|
||||
|
||||
const describeif = (condition : boolean) => condition ? describe : describe.skip
|
||||
const itif = (condition : boolean) => condition ? it : it.skip
|
||||
const describeif = (condition: boolean) => condition ? describe : describe.skip
|
||||
const itif = (condition: boolean) => condition ? it : it.skip
|
||||
|
||||
jest.setTimeout(60000)
|
||||
|
||||
@ -29,7 +29,7 @@ describe("Lens integration tests", () => {
|
||||
}
|
||||
|
||||
const clickWhatsNew = async (app: Application) => {
|
||||
await app.client.waitUntilTextExists("h1", "What's new")
|
||||
await app.client.waitUntilTextExists("h1", "What's new?")
|
||||
await app.client.click("button.primary")
|
||||
await app.client.waitUntilTextExists("h1", "Welcome")
|
||||
}
|
||||
@ -151,7 +151,7 @@ describe("Lens integration tests", () => {
|
||||
}
|
||||
})
|
||||
|
||||
const tests : {
|
||||
const tests: {
|
||||
drawer?: string
|
||||
drawerId?: string
|
||||
pages: {
|
||||
@ -160,232 +160,230 @@ describe("Lens integration tests", () => {
|
||||
expectedSelector: string,
|
||||
expectedText: string
|
||||
}[]
|
||||
}[] = [
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [ {
|
||||
name: "Cluster",
|
||||
href: "cluster",
|
||||
expectedSelector: "div.ClusterNoMetrics p",
|
||||
expectedText: "Metrics are not available due"
|
||||
}]
|
||||
}[] = [{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Cluster",
|
||||
href: "cluster",
|
||||
expectedSelector: "div.ClusterNoMetrics p",
|
||||
expectedText: "Metrics are not available due"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Nodes",
|
||||
href: "nodes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Nodes"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Workloads",
|
||||
drawerId: "workloads",
|
||||
pages: [{
|
||||
name: "Overview",
|
||||
href: "workloads",
|
||||
expectedSelector: "h5.box",
|
||||
expectedText: "Overview"
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [ {
|
||||
name: "Nodes",
|
||||
href: "nodes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Nodes"
|
||||
}]
|
||||
name: "Pods",
|
||||
href: "pods",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pods"
|
||||
},
|
||||
{
|
||||
drawer: "Workloads",
|
||||
drawerId: "workloads",
|
||||
pages: [ {
|
||||
name: "Overview",
|
||||
href: "workloads",
|
||||
expectedSelector: "h5.box",
|
||||
expectedText: "Overview"
|
||||
},
|
||||
{
|
||||
name: "Pods",
|
||||
href: "pods",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pods"
|
||||
},
|
||||
{
|
||||
name: "Deployments",
|
||||
href: "deployments",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Deployments"
|
||||
},
|
||||
{
|
||||
name: "DaemonSets",
|
||||
href: "daemonsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Daemon Sets"
|
||||
},
|
||||
{
|
||||
name: "StatefulSets",
|
||||
href: "statefulsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Stateful Sets"
|
||||
},
|
||||
{
|
||||
name: "Jobs",
|
||||
href: "jobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Jobs"
|
||||
},
|
||||
{
|
||||
name: "CronJobs",
|
||||
href: "cronjobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Cron Jobs"
|
||||
} ]
|
||||
name: "Deployments",
|
||||
href: "deployments",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Deployments"
|
||||
},
|
||||
{
|
||||
drawer: "Configuration",
|
||||
drawerId: "config",
|
||||
pages: [ {
|
||||
name: "ConfigMaps",
|
||||
href: "configmaps",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Config Maps"
|
||||
},
|
||||
{
|
||||
name: "Secrets",
|
||||
href: "secrets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Secrets"
|
||||
},
|
||||
{
|
||||
name: "Resource Quotas",
|
||||
href: "resourcequotas",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Resource Quotas"
|
||||
},
|
||||
{
|
||||
name: "HPA",
|
||||
href: "hpa",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Horizontal Pod Autoscalers"
|
||||
},
|
||||
{
|
||||
name: "Pod Disruption Budgets",
|
||||
href: "poddisruptionbudgets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Disruption Budgets"
|
||||
} ]
|
||||
name: "DaemonSets",
|
||||
href: "daemonsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Daemon Sets"
|
||||
},
|
||||
{
|
||||
drawer: "Network",
|
||||
drawerId: "networks",
|
||||
pages: [ {
|
||||
name: "Services",
|
||||
href: "services",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Services"
|
||||
},
|
||||
{
|
||||
name: "Endpoints",
|
||||
href: "endpoints",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Endpoints"
|
||||
},
|
||||
{
|
||||
name: "Ingresses",
|
||||
href: "ingresses",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Ingresses"
|
||||
},
|
||||
{
|
||||
name: "Network Policies",
|
||||
href: "network-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Network Policies"
|
||||
} ]
|
||||
name: "StatefulSets",
|
||||
href: "statefulsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Stateful Sets"
|
||||
},
|
||||
{
|
||||
drawer: "Storage",
|
||||
drawerId: "storage",
|
||||
pages: [ {
|
||||
name: "Persistent Volume Claims",
|
||||
href: "persistent-volume-claims",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volume Claims"
|
||||
},
|
||||
{
|
||||
name: "Persistent Volumes",
|
||||
href: "persistent-volumes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volumes"
|
||||
},
|
||||
{
|
||||
name: "Storage Classes",
|
||||
href: "storage-classes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Storage Classes"
|
||||
} ]
|
||||
name: "Jobs",
|
||||
href: "jobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Jobs"
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [ {
|
||||
name: "Namespaces",
|
||||
href: "namespaces",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Namespaces"
|
||||
}]
|
||||
name: "CronJobs",
|
||||
href: "cronjobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Cron Jobs"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Configuration",
|
||||
drawerId: "config",
|
||||
pages: [{
|
||||
name: "ConfigMaps",
|
||||
href: "configmaps",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Config Maps"
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [ {
|
||||
name: "Events",
|
||||
href: "events",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Events"
|
||||
}]
|
||||
name: "Secrets",
|
||||
href: "secrets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Secrets"
|
||||
},
|
||||
{
|
||||
drawer: "Apps",
|
||||
drawerId: "apps",
|
||||
pages: [ {
|
||||
name: "Charts",
|
||||
href: "apps/charts",
|
||||
expectedSelector: "div.HelmCharts input",
|
||||
expectedText: ""
|
||||
},
|
||||
{
|
||||
name: "Releases",
|
||||
href: "apps/releases",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Releases"
|
||||
} ]
|
||||
name: "Resource Quotas",
|
||||
href: "resourcequotas",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Resource Quotas"
|
||||
},
|
||||
{
|
||||
drawer: "Access Control",
|
||||
drawerId: "users",
|
||||
pages: [ {
|
||||
name: "Service Accounts",
|
||||
href: "service-accounts",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Service Accounts"
|
||||
},
|
||||
{
|
||||
name: "Role Bindings",
|
||||
href: "role-bindings",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Role Bindings"
|
||||
},
|
||||
{
|
||||
name: "Roles",
|
||||
href: "roles",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Roles"
|
||||
},
|
||||
{
|
||||
name: "Pod Security Policies",
|
||||
href: "pod-security-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Security Policies"
|
||||
} ]
|
||||
name: "HPA",
|
||||
href: "hpa",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Horizontal Pod Autoscalers"
|
||||
},
|
||||
{
|
||||
drawer: "Custom Resources",
|
||||
drawerId: "custom-resources",
|
||||
pages: [ {
|
||||
name: "Definitions",
|
||||
href: "crd/definitions",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Custom Resources"
|
||||
} ]
|
||||
name: "Pod Disruption Budgets",
|
||||
href: "poddisruptionbudgets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Disruption Budgets"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Network",
|
||||
drawerId: "networks",
|
||||
pages: [{
|
||||
name: "Services",
|
||||
href: "services",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Services"
|
||||
},
|
||||
];
|
||||
{
|
||||
name: "Endpoints",
|
||||
href: "endpoints",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Endpoints"
|
||||
},
|
||||
{
|
||||
name: "Ingresses",
|
||||
href: "ingresses",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Ingresses"
|
||||
},
|
||||
{
|
||||
name: "Network Policies",
|
||||
href: "network-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Network Policies"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Storage",
|
||||
drawerId: "storage",
|
||||
pages: [{
|
||||
name: "Persistent Volume Claims",
|
||||
href: "persistent-volume-claims",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volume Claims"
|
||||
},
|
||||
{
|
||||
name: "Persistent Volumes",
|
||||
href: "persistent-volumes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volumes"
|
||||
},
|
||||
{
|
||||
name: "Storage Classes",
|
||||
href: "storage-classes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Storage Classes"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Namespaces",
|
||||
href: "namespaces",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Namespaces"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Events",
|
||||
href: "events",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Events"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Apps",
|
||||
drawerId: "apps",
|
||||
pages: [{
|
||||
name: "Charts",
|
||||
href: "apps/charts",
|
||||
expectedSelector: "div.HelmCharts input",
|
||||
expectedText: ""
|
||||
},
|
||||
{
|
||||
name: "Releases",
|
||||
href: "apps/releases",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Releases"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Access Control",
|
||||
drawerId: "users",
|
||||
pages: [{
|
||||
name: "Service Accounts",
|
||||
href: "service-accounts",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Service Accounts"
|
||||
},
|
||||
{
|
||||
name: "Role Bindings",
|
||||
href: "role-bindings",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Role Bindings"
|
||||
},
|
||||
{
|
||||
name: "Roles",
|
||||
href: "roles",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Roles"
|
||||
},
|
||||
{
|
||||
name: "Pod Security Policies",
|
||||
href: "pod-security-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Security Policies"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Custom Resources",
|
||||
drawerId: "custom-resources",
|
||||
pages: [{
|
||||
name: "Definitions",
|
||||
href: "crd/definitions",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Custom Resources"
|
||||
}]
|
||||
}];
|
||||
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
||||
if (drawer !== "") {
|
||||
it(`shows ${drawer} drawer`, async () => {
|
||||
@ -394,7 +392,7 @@ describe("Lens integration tests", () => {
|
||||
await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name)
|
||||
})
|
||||
}
|
||||
pages.forEach(({name, href, expectedSelector, expectedText}) => {
|
||||
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
|
||||
it(`shows ${drawer}->${name} page`, async () => {
|
||||
expect(clusterAdded).toBe(true)
|
||||
await app.client.click(`a[href="/${href}"]`)
|
||||
|
||||
@ -12,7 +12,7 @@ export function setup(): Application {
|
||||
args: [],
|
||||
path: AppPaths[process.platform],
|
||||
startTimeout: 30000,
|
||||
waitTimeout: 30000,
|
||||
waitTimeout: 60000,
|
||||
chromeDriverArgs: ['remote-debugging-port=9222'],
|
||||
env: {
|
||||
CICD: "true"
|
||||
@ -21,10 +21,11 @@ export function setup(): Application {
|
||||
}
|
||||
|
||||
export async function tearDown(app: Application) {
|
||||
const pid = app.mainProcess.pid
|
||||
let mpid: any = app.mainProcess.pid
|
||||
let pid = await mpid()
|
||||
await app.stop()
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
process.kill(pid, "SIGKILL");
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ msgstr "Account Name"
|
||||
msgid "Active"
|
||||
msgstr "Active"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:289
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
|
||||
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
|
||||
msgid "Add Cluster"
|
||||
msgstr "Add Cluster"
|
||||
@ -112,7 +112,7 @@ msgstr "Add bindings to {name}"
|
||||
#~ msgid "Add cluster"
|
||||
#~ msgstr "Add cluster"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:306
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:327
|
||||
msgid "Add cluster(s)"
|
||||
msgstr "Add cluster(s)"
|
||||
|
||||
@ -203,7 +203,7 @@ msgstr "All clusters within workspace will be cleared as well"
|
||||
msgid "All groups"
|
||||
msgstr "All groups"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57
|
||||
#: src/renderer/components/dock/pod-logs.tsx:37
|
||||
msgid "All logs"
|
||||
msgstr "All logs"
|
||||
|
||||
@ -323,7 +323,7 @@ msgstr "Binding targets"
|
||||
msgid "Bindings"
|
||||
msgstr "Bindings"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:236
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
|
||||
msgid "Browse"
|
||||
msgstr "Browse"
|
||||
|
||||
@ -404,7 +404,7 @@ msgstr "CPU:"
|
||||
|
||||
#: src/renderer/components/+workspaces/workspaces.tsx:133
|
||||
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
|
||||
#: src/renderer/components/dock/info-panel.tsx:85
|
||||
#: src/renderer/components/dock/info-panel.tsx:86
|
||||
#: src/renderer/components/wizard/wizard.tsx:130
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
@ -473,7 +473,6 @@ msgstr "Claim"
|
||||
msgid "Claim Name"
|
||||
msgstr "Claim Name"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
|
||||
#: src/renderer/components/dialog/logs-dialog.tsx:39
|
||||
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
|
||||
msgid "Close"
|
||||
@ -566,7 +565,7 @@ msgstr "Configuration"
|
||||
msgid "Connection"
|
||||
msgstr "Connection"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246
|
||||
#: src/renderer/components/dock/pod-logs.tsx:148
|
||||
msgid "Container"
|
||||
msgstr "Container"
|
||||
|
||||
@ -595,8 +594,8 @@ msgid "Container runtime"
|
||||
msgstr "Container runtime"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details.tsx:122
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186
|
||||
#: src/renderer/components/+workloads-pods/pods.tsx:77
|
||||
#: src/renderer/components/dock/pod-logs.tsx:129
|
||||
msgid "Containers"
|
||||
msgstr "Containers"
|
||||
|
||||
@ -691,7 +690,7 @@ msgstr "Create new Secret"
|
||||
msgid "Create new Service Account"
|
||||
msgstr "Create new Service Account"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:93
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
msgid "Create resource"
|
||||
msgstr "Create resource"
|
||||
|
||||
@ -928,7 +927,8 @@ msgstr "Environment"
|
||||
msgid "Error stack"
|
||||
msgstr "Error stack"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:109
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
|
||||
msgid "Error while adding cluster(s): {0}"
|
||||
msgstr "Error while adding cluster(s): {0}"
|
||||
|
||||
@ -948,7 +948,7 @@ msgstr "Everything is fine in the Cluster"
|
||||
#~ msgid "Excluded items with \"system:\" prefix"
|
||||
#~ msgstr "Excluded items with \"system:\" prefix"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:98
|
||||
#: src/renderer/components/dock/dock.tsx:104
|
||||
msgid "Exit full size mode"
|
||||
msgstr "Exit full size mode"
|
||||
|
||||
@ -964,7 +964,7 @@ msgstr "External IP"
|
||||
msgid "External IPs"
|
||||
msgstr "External IPs"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106
|
||||
#: src/renderer/components/dock/pod-logs.store.ts:65
|
||||
msgid "Failed to load logs: {0}"
|
||||
msgstr "Failed to load logs: {0}"
|
||||
|
||||
@ -989,7 +989,7 @@ msgstr "Finalizers"
|
||||
msgid "First seen"
|
||||
msgstr "First seen"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:98
|
||||
#: src/renderer/components/dock/dock.tsx:104
|
||||
msgid "Fit to window"
|
||||
msgstr "Fit to window"
|
||||
|
||||
@ -1006,8 +1006,8 @@ msgid "From"
|
||||
msgstr "From"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212
|
||||
msgid "From <0>{from}</0> to <1>{to}</1>"
|
||||
msgstr "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>"
|
||||
|
||||
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125
|
||||
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"
|
||||
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:93
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19
|
||||
msgid "Hide"
|
||||
msgstr "Hide"
|
||||
@ -1153,7 +1153,7 @@ msgid "Ingresses"
|
||||
msgstr "Ingresses"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details.tsx:118
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192
|
||||
#: src/renderer/components/dock/pod-logs.tsx:135
|
||||
msgid "Init Containers"
|
||||
msgstr "Init Containers"
|
||||
|
||||
@ -1314,7 +1314,7 @@ msgstr "Limited to {0}"
|
||||
msgid "Limits"
|
||||
msgstr "Limits"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248
|
||||
#: src/renderer/components/dock/pod-logs.tsx:150
|
||||
msgid "Lines"
|
||||
msgstr "Lines"
|
||||
|
||||
@ -1338,8 +1338,8 @@ msgstr "Load-Balancer Ingress Points"
|
||||
msgid "Loading"
|
||||
msgstr "Loading"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:90
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:91
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:100
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:101
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
|
||||
@ -1445,7 +1445,7 @@ msgstr "Min Available"
|
||||
msgid "Min Pods"
|
||||
msgstr "Min Pods"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
#: src/renderer/components/dock/dock.tsx:105
|
||||
msgid "Minimize"
|
||||
msgstr "Minimize"
|
||||
|
||||
@ -1600,11 +1600,11 @@ msgstr "Network File System"
|
||||
msgid "Network Policies"
|
||||
msgstr "Network Policies"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231
|
||||
#: src/renderer/components/dock/pod-logs.tsx:178
|
||||
msgid "New logs since opening the dialog"
|
||||
msgstr "New logs since opening the dialog"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:86
|
||||
#: src/renderer/components/dock/dock.tsx:92
|
||||
msgid "New tab"
|
||||
msgstr "New tab"
|
||||
|
||||
@ -1642,7 +1642,7 @@ msgstr "No Nodes Available."
|
||||
#~ msgid "No contexts available or they already added"
|
||||
#~ msgstr "No contexts available or they already added"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:281
|
||||
msgid "No contexts available or they have been added already"
|
||||
msgstr "No contexts available or they have been added already"
|
||||
|
||||
@ -1738,7 +1738,7 @@ msgstr "Ok"
|
||||
msgid "Ok, got it!"
|
||||
msgstr "Ok, got it!"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
#: src/renderer/components/dock/dock.tsx:105
|
||||
msgid "Open"
|
||||
msgstr "Open"
|
||||
|
||||
@ -1774,7 +1774,7 @@ msgstr "Parallelism"
|
||||
msgid "Parameters"
|
||||
msgstr "Parameters"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:230
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
|
||||
msgid "Paste as text"
|
||||
msgstr "Paste as text"
|
||||
|
||||
@ -1798,7 +1798,7 @@ msgstr "Persistent Volume Claims"
|
||||
msgid "Persistent Volumes"
|
||||
msgstr "Persistent Volumes"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:72
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
|
||||
msgid "Please select at least one cluster context"
|
||||
msgstr "Please select at least one cluster context"
|
||||
|
||||
@ -1851,7 +1851,7 @@ msgstr "Pod Selector"
|
||||
msgid "Pod Status"
|
||||
msgstr "Pod Status"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:67
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:77
|
||||
msgid "Pod shell"
|
||||
msgstr "Pod shell"
|
||||
|
||||
@ -1914,7 +1914,7 @@ msgstr "Privileged"
|
||||
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
|
||||
#~ msgstr "Pro-Tip: paste kubeconfig to collect available contexts"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:248
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:269
|
||||
msgid "Pro-Tip: paste kubeconfig to get available contexts"
|
||||
msgstr "Pro-Tip: paste kubeconfig to get available contexts"
|
||||
|
||||
@ -1922,7 +1922,7 @@ msgstr "Pro-Tip: paste kubeconfig to get available contexts"
|
||||
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
|
||||
#~ msgstr "Pro-Tip: paste kubeconfig to parse available contexts"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:239
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
|
||||
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
|
||||
msgstr "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
|
||||
|
||||
@ -1943,7 +1943,7 @@ msgstr "Provisioner"
|
||||
msgid "Proxy is used only for non-cluster communication."
|
||||
msgstr "Proxy is used only for non-cluster communication."
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:294
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:315
|
||||
msgid "Proxy settings"
|
||||
msgstr "Proxy settings"
|
||||
|
||||
@ -1979,7 +1979,7 @@ msgstr "Readiness"
|
||||
msgid "Reason"
|
||||
msgstr "Reason"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107
|
||||
#: src/renderer/components/dock/pod-logs.store.ts:66
|
||||
msgid "Reason: {0} ({1})"
|
||||
msgstr "Reason: {0} ({1})"
|
||||
|
||||
@ -2124,7 +2124,7 @@ msgstr "Required Drop Capabilities"
|
||||
msgid "Required field"
|
||||
msgstr "Required field"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:235
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:256
|
||||
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
|
||||
msgid "Reset"
|
||||
msgstr "Reset"
|
||||
@ -2266,9 +2266,9 @@ msgstr "Runtime Class"
|
||||
#: src/renderer/components/+apps-releases/release-details.tsx:114
|
||||
#: src/renderer/components/+config-maps/config-map-details.tsx:78
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:97
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216
|
||||
#: src/renderer/components/+workspaces/workspaces.tsx:132
|
||||
#: src/renderer/components/dock/edit-resource.tsx:87
|
||||
#: src/renderer/components/dock/pod-logs.tsx:161
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
@ -2360,7 +2360,7 @@ msgstr "Select a quota.."
|
||||
#~ msgid "Select context(s)"
|
||||
#~ msgstr "Select context(s)"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:278
|
||||
msgid "Select contexts"
|
||||
msgstr "Select contexts"
|
||||
|
||||
@ -2373,8 +2373,8 @@ msgstr "Select contexts"
|
||||
#~ msgid "Select custom kube-config file"
|
||||
#~ msgstr "Select custom kube-config file"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
msgid "Select custom kubeconfig file"
|
||||
msgstr "Select custom kubeconfig file"
|
||||
|
||||
@ -2390,7 +2390,7 @@ msgstr "Select custom kubeconfig file"
|
||||
#~ msgid "Select kubeconfig"
|
||||
#~ msgstr "Select kubeconfig"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
|
||||
msgid "Select kubeconfig file"
|
||||
msgstr "Select kubeconfig file"
|
||||
|
||||
@ -2418,7 +2418,7 @@ msgstr "Select service accounts"
|
||||
#~ msgid "Selected contexts ({0}): <0>{1}</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>"
|
||||
msgstr "Selected contexts: <0>{0}</0>"
|
||||
|
||||
@ -2475,13 +2475,13 @@ msgid "Settings"
|
||||
msgstr "Settings"
|
||||
|
||||
#: src/renderer/components/+nodes/node-menu.tsx:48
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:68
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:78
|
||||
msgid "Shell"
|
||||
msgstr "Shell"
|
||||
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:93
|
||||
#: src/renderer/components/+workloads-pods/pod-container-env.tsx:101
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19
|
||||
msgid "Show"
|
||||
msgstr "Show"
|
||||
@ -2490,10 +2490,22 @@ msgstr "Show"
|
||||
msgid "Show Notes"
|
||||
msgstr "Show Notes"
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:160
|
||||
msgid "Show current logs"
|
||||
msgstr "Show current logs"
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:160
|
||||
msgid "Show previous terminated container logs"
|
||||
msgstr "Show previous terminated container logs"
|
||||
|
||||
#: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20
|
||||
msgid "Show value"
|
||||
msgstr "Show value"
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:154
|
||||
msgid "Since"
|
||||
msgstr "Since"
|
||||
|
||||
#: src/renderer/components/+nodes/node-charts.tsx:80
|
||||
#: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49
|
||||
msgid "Size"
|
||||
@ -2588,12 +2600,12 @@ msgstr "Strategy Type"
|
||||
msgid "Sub-object"
|
||||
msgstr "Sub-object"
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:93
|
||||
#: src/renderer/components/dock/info-panel.tsx:95
|
||||
#: src/renderer/components/wizard/wizard.tsx:131
|
||||
msgid "Submit"
|
||||
msgstr "Submit"
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:94
|
||||
#: src/renderer/components/dock/info-panel.tsx:96
|
||||
msgid "Submitting.."
|
||||
msgstr "Submitting.."
|
||||
|
||||
@ -2601,7 +2613,7 @@ msgstr "Submitting.."
|
||||
msgid "Subsets"
|
||||
msgstr "Subsets"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:102
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
|
||||
msgid "Successfully imported <0>{0}</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"
|
||||
msgstr "Terminal"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:89
|
||||
#: src/renderer/components/dock/dock.tsx:95
|
||||
msgid "Terminal session"
|
||||
msgstr "Terminal session"
|
||||
|
||||
@ -2643,7 +2655,7 @@ msgstr "Terminal session"
|
||||
msgid "The path to the kubectl binary on the system."
|
||||
msgstr "The path to the kubectl binary on the system."
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226
|
||||
#: src/renderer/components/dock/pod-logs.tsx:172
|
||||
msgid "There are no logs available for container."
|
||||
msgstr "There are no logs available for container."
|
||||
|
||||
@ -2773,8 +2785,8 @@ msgstr "Upgrade version"
|
||||
msgid "Usage"
|
||||
msgstr "Usage"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
|
||||
msgid "Use configuration"
|
||||
msgstr "Use configuration"
|
||||
|
||||
@ -2961,7 +2973,7 @@ msgstr "sec"
|
||||
msgid "singular"
|
||||
msgstr "singular"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
msgid "timestamps"
|
||||
msgstr "timestamps"
|
||||
|
||||
@ -3006,8 +3018,8 @@ msgid "{metricsRemainCount} more..."
|
||||
msgstr "{metricsRemainCount} more..."
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240
|
||||
msgid "{podName} Logs"
|
||||
msgstr "{podName} Logs"
|
||||
#~ msgid "{podName} Logs"
|
||||
#~ msgstr "{podName} Logs"
|
||||
|
||||
#: src/renderer/components/dock/edit-resource.tsx:56
|
||||
msgid "{resourceType} <0>{resourceName}</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>}}"
|
||||
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"
|
||||
msgstr "{submitLabel} & Close"
|
||||
|
||||
@ -87,7 +87,7 @@ msgstr ""
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:289
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
|
||||
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
|
||||
msgid "Add Cluster"
|
||||
msgstr ""
|
||||
@ -112,7 +112,7 @@ msgstr ""
|
||||
#~ msgid "Add cluster"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:306
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:327
|
||||
msgid "Add cluster(s)"
|
||||
msgstr ""
|
||||
|
||||
@ -203,7 +203,7 @@ msgstr ""
|
||||
msgid "All groups"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57
|
||||
#: src/renderer/components/dock/pod-logs.tsx:37
|
||||
msgid "All logs"
|
||||
msgstr ""
|
||||
|
||||
@ -323,7 +323,7 @@ msgstr ""
|
||||
msgid "Bindings"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:236
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
|
||||
msgid "Browse"
|
||||
msgstr ""
|
||||
|
||||
@ -404,7 +404,7 @@ msgstr ""
|
||||
|
||||
#: src/renderer/components/+workspaces/workspaces.tsx:133
|
||||
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
|
||||
#: src/renderer/components/dock/info-panel.tsx:85
|
||||
#: src/renderer/components/dock/info-panel.tsx:86
|
||||
#: src/renderer/components/wizard/wizard.tsx:130
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@ -469,7 +469,6 @@ msgstr ""
|
||||
msgid "Claim Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
|
||||
#: src/renderer/components/dialog/logs-dialog.tsx:39
|
||||
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
|
||||
msgid "Close"
|
||||
@ -562,7 +561,7 @@ msgstr ""
|
||||
msgid "Connection"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246
|
||||
#: src/renderer/components/dock/pod-logs.tsx:148
|
||||
msgid "Container"
|
||||
msgstr ""
|
||||
|
||||
@ -591,8 +590,8 @@ msgid "Container runtime"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details.tsx:122
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186
|
||||
#: src/renderer/components/+workloads-pods/pods.tsx:77
|
||||
#: src/renderer/components/dock/pod-logs.tsx:129
|
||||
msgid "Containers"
|
||||
msgstr ""
|
||||
|
||||
@ -687,7 +686,7 @@ msgstr ""
|
||||
msgid "Create new Service Account"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:93
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
msgid "Create resource"
|
||||
msgstr ""
|
||||
|
||||
@ -924,7 +923,8 @@ msgstr ""
|
||||
msgid "Error stack"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:109
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
|
||||
msgid "Error while adding cluster(s): {0}"
|
||||
msgstr ""
|
||||
|
||||
@ -939,7 +939,7 @@ msgstr ""
|
||||
msgid "Everything is fine in the Cluster"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:98
|
||||
#: src/renderer/components/dock/dock.tsx:104
|
||||
msgid "Exit full size mode"
|
||||
msgstr ""
|
||||
|
||||
@ -955,7 +955,7 @@ msgstr ""
|
||||
msgid "External IPs"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106
|
||||
#: src/renderer/components/dock/pod-logs.store.ts:65
|
||||
msgid "Failed to load logs: {0}"
|
||||
msgstr ""
|
||||
|
||||
@ -980,7 +980,7 @@ msgstr ""
|
||||
msgid "First seen"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:98
|
||||
#: src/renderer/components/dock/dock.tsx:104
|
||||
msgid "Fit to window"
|
||||
msgstr ""
|
||||
|
||||
@ -997,8 +997,8 @@ msgid "From"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212
|
||||
msgid "From <0>{from}</0> to <1>{to}</1>"
|
||||
msgstr ""
|
||||
#~ msgid "From <0>{from}</0> to <1>{to}</1>"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125
|
||||
msgid "Fs Group"
|
||||
@ -1059,7 +1059,7 @@ msgid "Helm branch <0>{0}</0> already in use"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:93
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19
|
||||
msgid "Hide"
|
||||
msgstr ""
|
||||
@ -1144,7 +1144,7 @@ msgid "Ingresses"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details.tsx:118
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192
|
||||
#: src/renderer/components/dock/pod-logs.tsx:135
|
||||
msgid "Init Containers"
|
||||
msgstr ""
|
||||
|
||||
@ -1305,7 +1305,7 @@ msgstr ""
|
||||
msgid "Limits"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248
|
||||
#: src/renderer/components/dock/pod-logs.tsx:150
|
||||
msgid "Lines"
|
||||
msgstr ""
|
||||
|
||||
@ -1329,8 +1329,8 @@ msgstr ""
|
||||
msgid "Loading"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:90
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:91
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:100
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:101
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
@ -1436,7 +1436,7 @@ msgstr ""
|
||||
msgid "Min Pods"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
#: src/renderer/components/dock/dock.tsx:105
|
||||
msgid "Minimize"
|
||||
msgstr ""
|
||||
|
||||
@ -1591,11 +1591,11 @@ msgstr ""
|
||||
msgid "Network Policies"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231
|
||||
#: src/renderer/components/dock/pod-logs.tsx:178
|
||||
msgid "New logs since opening the dialog"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:86
|
||||
#: src/renderer/components/dock/dock.tsx:92
|
||||
msgid "New tab"
|
||||
msgstr ""
|
||||
|
||||
@ -1625,7 +1625,7 @@ msgstr ""
|
||||
#~ msgid "No contexts available or they already added"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:281
|
||||
msgid "No contexts available or they have been added already"
|
||||
msgstr ""
|
||||
|
||||
@ -1721,7 +1721,7 @@ msgstr ""
|
||||
msgid "Ok, got it!"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
#: src/renderer/components/dock/dock.tsx:105
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
@ -1757,7 +1757,7 @@ msgstr ""
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:230
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
|
||||
msgid "Paste as text"
|
||||
msgstr ""
|
||||
|
||||
@ -1781,7 +1781,7 @@ msgstr ""
|
||||
msgid "Persistent Volumes"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:72
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
|
||||
msgid "Please select at least one cluster context"
|
||||
msgstr ""
|
||||
|
||||
@ -1834,7 +1834,7 @@ msgstr ""
|
||||
msgid "Pod Status"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:67
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:77
|
||||
msgid "Pod shell"
|
||||
msgstr ""
|
||||
|
||||
@ -1897,7 +1897,7 @@ msgstr ""
|
||||
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:248
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:269
|
||||
msgid "Pro-Tip: paste kubeconfig to get available contexts"
|
||||
msgstr ""
|
||||
|
||||
@ -1905,7 +1905,7 @@ msgstr ""
|
||||
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:239
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
|
||||
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
|
||||
msgstr ""
|
||||
|
||||
@ -1926,7 +1926,7 @@ msgstr ""
|
||||
msgid "Proxy is used only for non-cluster communication."
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:294
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:315
|
||||
msgid "Proxy settings"
|
||||
msgstr ""
|
||||
|
||||
@ -1962,7 +1962,7 @@ msgstr ""
|
||||
msgid "Reason"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107
|
||||
#: src/renderer/components/dock/pod-logs.store.ts:66
|
||||
msgid "Reason: {0} ({1})"
|
||||
msgstr ""
|
||||
|
||||
@ -2107,7 +2107,7 @@ msgstr ""
|
||||
msgid "Required field"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:235
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:256
|
||||
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
@ -2249,9 +2249,9 @@ msgstr ""
|
||||
#: src/renderer/components/+apps-releases/release-details.tsx:114
|
||||
#: src/renderer/components/+config-maps/config-map-details.tsx:78
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:97
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216
|
||||
#: src/renderer/components/+workspaces/workspaces.tsx:132
|
||||
#: src/renderer/components/dock/edit-resource.tsx:87
|
||||
#: src/renderer/components/dock/pod-logs.tsx:161
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@ -2343,7 +2343,7 @@ msgstr ""
|
||||
#~ msgid "Select context(s)"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:278
|
||||
msgid "Select contexts"
|
||||
msgstr ""
|
||||
|
||||
@ -2356,8 +2356,8 @@ msgstr ""
|
||||
#~ msgid "Select custom kube-config file"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
msgid "Select custom kubeconfig file"
|
||||
msgstr ""
|
||||
|
||||
@ -2373,7 +2373,7 @@ msgstr ""
|
||||
#~ msgid "Select kubeconfig"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
|
||||
msgid "Select kubeconfig file"
|
||||
msgstr ""
|
||||
|
||||
@ -2401,7 +2401,7 @@ msgstr ""
|
||||
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
|
||||
#~ 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>"
|
||||
msgstr ""
|
||||
|
||||
@ -2458,13 +2458,13 @@ msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+nodes/node-menu.tsx:48
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:68
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:78
|
||||
msgid "Shell"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:93
|
||||
#: src/renderer/components/+workloads-pods/pod-container-env.tsx:101
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19
|
||||
msgid "Show"
|
||||
msgstr ""
|
||||
@ -2473,10 +2473,22 @@ msgstr ""
|
||||
msgid "Show Notes"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:160
|
||||
msgid "Show current logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:160
|
||||
msgid "Show previous terminated container logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20
|
||||
msgid "Show value"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:154
|
||||
msgid "Since"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+nodes/node-charts.tsx:80
|
||||
#: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49
|
||||
msgid "Size"
|
||||
@ -2571,12 +2583,12 @@ msgstr ""
|
||||
msgid "Sub-object"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:93
|
||||
#: src/renderer/components/dock/info-panel.tsx:95
|
||||
#: src/renderer/components/wizard/wizard.tsx:131
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:94
|
||||
#: src/renderer/components/dock/info-panel.tsx:96
|
||||
msgid "Submitting.."
|
||||
msgstr ""
|
||||
|
||||
@ -2584,7 +2596,7 @@ msgstr ""
|
||||
msgid "Subsets"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:102
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
|
||||
msgid "Successfully imported <0>{0}</0> cluster(s)"
|
||||
msgstr ""
|
||||
|
||||
@ -2618,7 +2630,7 @@ msgstr ""
|
||||
msgid "Terminal"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:89
|
||||
#: src/renderer/components/dock/dock.tsx:95
|
||||
msgid "Terminal session"
|
||||
msgstr ""
|
||||
|
||||
@ -2626,7 +2638,7 @@ msgstr ""
|
||||
msgid "The path to the kubectl binary on the system."
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226
|
||||
#: src/renderer/components/dock/pod-logs.tsx:172
|
||||
msgid "There are no logs available for container."
|
||||
msgstr ""
|
||||
|
||||
@ -2756,8 +2768,8 @@ msgstr ""
|
||||
msgid "Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
|
||||
msgid "Use configuration"
|
||||
msgstr ""
|
||||
|
||||
@ -2944,7 +2956,7 @@ msgstr ""
|
||||
msgid "singular"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
msgid "timestamps"
|
||||
msgstr ""
|
||||
|
||||
@ -2989,8 +3001,8 @@ msgid "{metricsRemainCount} more..."
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240
|
||||
msgid "{podName} Logs"
|
||||
msgstr ""
|
||||
#~ msgid "{podName} Logs"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/edit-resource.tsx:56
|
||||
msgid "{resourceType} <0>{resourceName}</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>}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:88
|
||||
#: src/renderer/components/dock/info-panel.tsx:89
|
||||
msgid "{submitLabel} & Close"
|
||||
msgstr ""
|
||||
|
||||
@ -88,7 +88,7 @@ msgstr "Название аккаунта"
|
||||
msgid "Active"
|
||||
msgstr "Активный"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:289
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
|
||||
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
|
||||
msgid "Add Cluster"
|
||||
msgstr ""
|
||||
@ -113,7 +113,7 @@ msgstr "Добавить привязки к {name}"
|
||||
#~ msgid "Add cluster"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:306
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:327
|
||||
msgid "Add cluster(s)"
|
||||
msgstr ""
|
||||
|
||||
@ -204,7 +204,7 @@ msgstr ""
|
||||
msgid "All groups"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:57
|
||||
#: src/renderer/components/dock/pod-logs.tsx:37
|
||||
msgid "All logs"
|
||||
msgstr "Все логи"
|
||||
|
||||
@ -324,7 +324,7 @@ msgstr "Цели привязки"
|
||||
msgid "Bindings"
|
||||
msgstr "Привязки"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:236
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
|
||||
msgid "Browse"
|
||||
msgstr ""
|
||||
|
||||
@ -405,7 +405,7 @@ msgstr "CPU:"
|
||||
|
||||
#: src/renderer/components/+workspaces/workspaces.tsx:133
|
||||
#: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44
|
||||
#: src/renderer/components/dock/info-panel.tsx:85
|
||||
#: src/renderer/components/dock/info-panel.tsx:86
|
||||
#: src/renderer/components/wizard/wizard.tsx:130
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
@ -474,7 +474,6 @@ msgstr "Запрос"
|
||||
msgid "Claim Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
|
||||
#: src/renderer/components/dialog/logs-dialog.tsx:39
|
||||
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
|
||||
msgid "Close"
|
||||
@ -567,7 +566,7 @@ msgstr "Конфигурация"
|
||||
msgid "Connection"
|
||||
msgstr "Соединение"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:246
|
||||
#: src/renderer/components/dock/pod-logs.tsx:148
|
||||
msgid "Container"
|
||||
msgstr "Контейнер"
|
||||
|
||||
@ -596,8 +595,8 @@ msgid "Container runtime"
|
||||
msgstr "Среда контейнеров"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details.tsx:122
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:186
|
||||
#: src/renderer/components/+workloads-pods/pods.tsx:77
|
||||
#: src/renderer/components/dock/pod-logs.tsx:129
|
||||
msgid "Containers"
|
||||
msgstr "Контейнеры"
|
||||
|
||||
@ -692,7 +691,7 @@ msgstr "Создать новый секрет"
|
||||
msgid "Create new Service Account"
|
||||
msgstr "Создать новый Service Account"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:93
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
msgid "Create resource"
|
||||
msgstr "Создать ресурс"
|
||||
|
||||
@ -929,7 +928,8 @@ msgstr "Среда"
|
||||
msgid "Error stack"
|
||||
msgstr "Стэк ошибки"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:109
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
|
||||
msgid "Error while adding cluster(s): {0}"
|
||||
msgstr ""
|
||||
|
||||
@ -949,7 +949,7 @@ msgstr "В кластере все в порядке"
|
||||
#~ msgid "Excluded items with \"system:\" prefix"
|
||||
#~ msgstr "За исключением объектов с префиксом “system:”"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:98
|
||||
#: src/renderer/components/dock/dock.tsx:104
|
||||
msgid "Exit full size mode"
|
||||
msgstr "Выйти из полного размера"
|
||||
|
||||
@ -965,7 +965,7 @@ msgstr "Внешний IP"
|
||||
msgid "External IPs"
|
||||
msgstr "Внешние IP"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:106
|
||||
#: src/renderer/components/dock/pod-logs.store.ts:65
|
||||
msgid "Failed to load logs: {0}"
|
||||
msgstr "Ошибка загрузки логов: {0}"
|
||||
|
||||
@ -990,7 +990,7 @@ msgstr "Финализаторы"
|
||||
msgid "First seen"
|
||||
msgstr "Увиденно впервые"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:98
|
||||
#: src/renderer/components/dock/dock.tsx:104
|
||||
msgid "Fit to window"
|
||||
msgstr "По размеру окна"
|
||||
|
||||
@ -1007,8 +1007,8 @@ msgid "From"
|
||||
msgstr "От"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:212
|
||||
msgid "From <0>{from}</0> to <1>{to}</1>"
|
||||
msgstr "От <0>{from}</0> до <1>{to}</1>"
|
||||
#~ msgid "From <0>{from}</0> to <1>{to}</1>"
|
||||
#~ msgstr "От <0>{from}</0> до <1>{to}</1>"
|
||||
|
||||
#: src/renderer/components/+pod-security-policies/pod-security-policy-details.tsx:125
|
||||
msgid "Fs Group"
|
||||
@ -1069,7 +1069,7 @@ msgid "Helm branch <0>{0}</0> already in use"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:93
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19
|
||||
msgid "Hide"
|
||||
msgstr "Скрыть"
|
||||
@ -1154,7 +1154,7 @@ msgid "Ingresses"
|
||||
msgstr "Ingresses"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details.tsx:118
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:192
|
||||
#: src/renderer/components/dock/pod-logs.tsx:135
|
||||
msgid "Init Containers"
|
||||
msgstr "Контейнеры инициализации"
|
||||
|
||||
@ -1315,7 +1315,7 @@ msgstr ""
|
||||
msgid "Limits"
|
||||
msgstr "Лимиты"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:248
|
||||
#: src/renderer/components/dock/pod-logs.tsx:150
|
||||
msgid "Lines"
|
||||
msgstr "Строки"
|
||||
|
||||
@ -1339,8 +1339,8 @@ msgstr ""
|
||||
msgid "Loading"
|
||||
msgstr "Загрузка"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:90
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:91
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:100
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:101
|
||||
msgid "Logs"
|
||||
msgstr "Логи"
|
||||
|
||||
@ -1446,7 +1446,7 @@ msgstr ""
|
||||
msgid "Min Pods"
|
||||
msgstr "Мин. подов"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
#: src/renderer/components/dock/dock.tsx:105
|
||||
msgid "Minimize"
|
||||
msgstr "Минимизировать"
|
||||
|
||||
@ -1601,11 +1601,11 @@ msgstr "Сетевая файловая система"
|
||||
msgid "Network Policies"
|
||||
msgstr "Network Policies"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:231
|
||||
#: src/renderer/components/dock/pod-logs.tsx:178
|
||||
msgid "New logs since opening the dialog"
|
||||
msgstr "Новые логи с момента открытия диалога"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:86
|
||||
#: src/renderer/components/dock/dock.tsx:92
|
||||
msgid "New tab"
|
||||
msgstr "Новая вкладка"
|
||||
|
||||
@ -1643,7 +1643,7 @@ msgstr "Нет доступных нод."
|
||||
#~ msgid "No contexts available or they already added"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:281
|
||||
msgid "No contexts available or they have been added already"
|
||||
msgstr ""
|
||||
|
||||
@ -1739,7 +1739,7 @@ msgstr "Ок"
|
||||
msgid "Ok, got it!"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:99
|
||||
#: src/renderer/components/dock/dock.tsx:105
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
@ -1775,7 +1775,7 @@ msgstr "Параллелизм"
|
||||
msgid "Parameters"
|
||||
msgstr "Параметры"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:230
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:251
|
||||
msgid "Paste as text"
|
||||
msgstr ""
|
||||
|
||||
@ -1799,7 +1799,7 @@ msgstr "Persistent Volume Claims"
|
||||
msgid "Persistent Volumes"
|
||||
msgstr "Persistent Volumes"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:72
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
|
||||
msgid "Please select at least one cluster context"
|
||||
msgstr ""
|
||||
|
||||
@ -1852,7 +1852,7 @@ msgstr "Селектор подов"
|
||||
msgid "Pod Status"
|
||||
msgstr "Статус подов"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:67
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:77
|
||||
msgid "Pod shell"
|
||||
msgstr "Командная строка пода"
|
||||
|
||||
@ -1915,7 +1915,7 @@ msgstr ""
|
||||
#~ msgid "Pro-Tip: paste kubeconfig to collect available contexts"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:248
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:269
|
||||
msgid "Pro-Tip: paste kubeconfig to get available contexts"
|
||||
msgstr ""
|
||||
|
||||
@ -1923,7 +1923,7 @@ msgstr ""
|
||||
#~ msgid "Pro-Tip: paste kubeconfig to parse available contexts"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:239
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:260
|
||||
msgid "Pro-Tip: you can also drag-n-drop kubeconfig file to this area"
|
||||
msgstr ""
|
||||
|
||||
@ -1944,7 +1944,7 @@ msgstr "Комиссия"
|
||||
msgid "Proxy is used only for non-cluster communication."
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:294
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:315
|
||||
msgid "Proxy settings"
|
||||
msgstr ""
|
||||
|
||||
@ -1980,7 +1980,7 @@ msgstr "Готовность"
|
||||
msgid "Reason"
|
||||
msgstr "Причина"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:107
|
||||
#: src/renderer/components/dock/pod-logs.store.ts:66
|
||||
msgid "Reason: {0} ({1})"
|
||||
msgstr "Причина: {0} ({1})"
|
||||
|
||||
@ -2125,7 +2125,7 @@ msgstr ""
|
||||
msgid "Required field"
|
||||
msgstr "Обязательное поле"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:235
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:256
|
||||
#: src/renderer/components/item-object-list/page-filters-list.tsx:31
|
||||
msgid "Reset"
|
||||
msgstr "Сбросить"
|
||||
@ -2267,9 +2267,9 @@ msgstr ""
|
||||
#: src/renderer/components/+apps-releases/release-details.tsx:114
|
||||
#: src/renderer/components/+config-maps/config-map-details.tsx:78
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:97
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216
|
||||
#: src/renderer/components/+workspaces/workspaces.tsx:132
|
||||
#: src/renderer/components/dock/edit-resource.tsx:87
|
||||
#: src/renderer/components/dock/pod-logs.tsx:161
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
@ -2361,7 +2361,7 @@ msgstr "Выберите квоту..."
|
||||
#~ msgid "Select context(s)"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:257
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:278
|
||||
msgid "Select contexts"
|
||||
msgstr ""
|
||||
|
||||
@ -2374,8 +2374,8 @@ msgstr ""
|
||||
#~ msgid "Select custom kube-config file"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:62
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
msgid "Select custom kubeconfig file"
|
||||
msgstr ""
|
||||
|
||||
@ -2391,7 +2391,7 @@ msgstr ""
|
||||
#~ msgid "Select kubeconfig"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:229
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:250
|
||||
msgid "Select kubeconfig file"
|
||||
msgstr ""
|
||||
|
||||
@ -2419,7 +2419,7 @@ msgstr "Выбрать сервисные аккаунты"
|
||||
#~ msgid "Selected contexts ({0}): <0>{1}</0>"
|
||||
#~ 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>"
|
||||
msgstr ""
|
||||
|
||||
@ -2476,13 +2476,13 @@ msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+nodes/node-menu.tsx:48
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:68
|
||||
#: src/renderer/components/+workloads-pods/pod-menu.tsx:78
|
||||
msgid "Shell"
|
||||
msgstr "Командная строка"
|
||||
|
||||
#: src/renderer/components/+config-secrets/secret-details.tsx:93
|
||||
#: src/renderer/components/+workloads-pods/pod-container-env.tsx:101
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
#: src/renderer/components/drawer/drawer-param-toggler.tsx:19
|
||||
msgid "Show"
|
||||
msgstr "Показать"
|
||||
@ -2491,10 +2491,22 @@ msgstr "Показать"
|
||||
msgid "Show Notes"
|
||||
msgstr "Показать логи"
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:160
|
||||
msgid "Show current logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:160
|
||||
msgid "Show previous terminated container logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+user-management-service-accounts/service-accounts-secret.tsx:20
|
||||
msgid "Show value"
|
||||
msgstr "Показать значение"
|
||||
|
||||
#: src/renderer/components/dock/pod-logs.tsx:154
|
||||
msgid "Since"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+nodes/node-charts.tsx:80
|
||||
#: src/renderer/components/+storage-volume-claims/volume-claims.tsx:49
|
||||
msgid "Size"
|
||||
@ -2589,12 +2601,12 @@ msgstr "Тип стратегии"
|
||||
msgid "Sub-object"
|
||||
msgstr "Суб-объект"
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:93
|
||||
#: src/renderer/components/dock/info-panel.tsx:95
|
||||
#: src/renderer/components/wizard/wizard.tsx:131
|
||||
msgid "Submit"
|
||||
msgstr "Отправить"
|
||||
|
||||
#: src/renderer/components/dock/info-panel.tsx:94
|
||||
#: src/renderer/components/dock/info-panel.tsx:96
|
||||
msgid "Submitting.."
|
||||
msgstr "Применение.."
|
||||
|
||||
@ -2602,7 +2614,7 @@ msgstr "Применение.."
|
||||
msgid "Subsets"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:102
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
|
||||
msgid "Successfully imported <0>{0}</0> cluster(s)"
|
||||
msgstr ""
|
||||
|
||||
@ -2636,7 +2648,7 @@ msgstr ""
|
||||
msgid "Terminal"
|
||||
msgstr "Терминал"
|
||||
|
||||
#: src/renderer/components/dock/dock.tsx:89
|
||||
#: src/renderer/components/dock/dock.tsx:95
|
||||
msgid "Terminal session"
|
||||
msgstr "Сессия терминала"
|
||||
|
||||
@ -2644,7 +2656,7 @@ msgstr "Сессия терминала"
|
||||
msgid "The path to the kubectl binary on the system."
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:226
|
||||
#: src/renderer/components/dock/pod-logs.tsx:172
|
||||
msgid "There are no logs available for container."
|
||||
msgstr "Для контейнера нет логов."
|
||||
|
||||
@ -2774,8 +2786,8 @@ msgstr "Обновить версию"
|
||||
msgid "Usage"
|
||||
msgstr "Использование"
|
||||
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
|
||||
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
|
||||
msgid "Use configuration"
|
||||
msgstr ""
|
||||
|
||||
@ -2962,7 +2974,7 @@ msgstr "сек"
|
||||
msgid "singular"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:215
|
||||
#: src/renderer/components/dock/pod-logs.tsx:159
|
||||
msgid "timestamps"
|
||||
msgstr "временные метки"
|
||||
|
||||
@ -3007,8 +3019,8 @@ msgid "{metricsRemainCount} more..."
|
||||
msgstr "{metricsRemainCount} еще…"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:240
|
||||
msgid "{podName} Logs"
|
||||
msgstr "{podName} логи"
|
||||
#~ msgid "{podName} Logs"
|
||||
#~ msgstr "{podName} логи"
|
||||
|
||||
#: src/renderer/components/dock/edit-resource.tsx:56
|
||||
msgid "{resourceType} <0>{resourceName}</0> updated."
|
||||
@ -3025,6 +3037,6 @@ msgstr ""
|
||||
"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"
|
||||
msgstr "{submitLabel} и закрыть"
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "kontena-lens",
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "3.6.5",
|
||||
"version": "3.6.6",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"license": "MIT",
|
||||
@ -183,6 +183,7 @@
|
||||
"@kubernetes/client-node": "^0.12.0",
|
||||
"array-move": "^3.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"command-exists": "1.2.9",
|
||||
"conf": "^7.0.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"electron-updater": "^4.3.1",
|
||||
@ -311,6 +312,7 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"include-media": "^1.4.9",
|
||||
"jest": "^26.0.1",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"make-plural": "^6.2.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
|
||||
@ -6,14 +6,10 @@ import { tracker } from "./tracker";
|
||||
export const clusterIpc = {
|
||||
activate: createIpcChannel({
|
||||
channel: "cluster:activate",
|
||||
handle: (clusterId: ClusterId, frameId?: number) => {
|
||||
handle: (clusterId: ClusterId, force = false) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
if (frameId) {
|
||||
cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||
}
|
||||
extensionLoader.broadcastExtensions(frameId)
|
||||
return cluster.activate();
|
||||
return cluster.activate(force);
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -24,6 +20,7 @@ export const clusterIpc = {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||
extensionLoader.broadcastExtensions(frameId)
|
||||
return cluster.pushState();
|
||||
}
|
||||
},
|
||||
|
||||
12
src/common/custom-errors.ts
Normal file
12
src/common/custom-errors.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,8 @@ import path from "path"
|
||||
import os from "os"
|
||||
import yaml from "js-yaml"
|
||||
import logger from "../main/logger";
|
||||
import commandExists from "command-exists";
|
||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||
|
||||
export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config');
|
||||
|
||||
@ -140,3 +142,28 @@ export function getNodeWarningConditions(node: V1Node) {
|
||||
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates kubeconfig supplied in the add clusters screen. At present this will just validate
|
||||
* the User struct, specifically the command passed to the exec substructure.
|
||||
*/
|
||||
export function validateKubeConfig (config: KubeConfig) {
|
||||
// we only receive a single context, cluster & user object here so lets validate them as this
|
||||
// will be called when we add a new cluster to Lens
|
||||
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
|
||||
|
||||
// Validate the User Object
|
||||
const user = config.getCurrentUser();
|
||||
if (user.exec) {
|
||||
const execCommand = user.exec["command"];
|
||||
// check if the command is absolute or not
|
||||
const isAbsolute = path.isAbsolute(execCommand);
|
||||
// validate the exec struct in the user object, start with the command field
|
||||
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
|
||||
|
||||
if (!commandExists.sync(execCommand)) {
|
||||
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`);
|
||||
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/common/utils/debouncePromise.ts
Executable file
9
src/common/utils/debouncePromise.ts
Executable 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);
|
||||
});
|
||||
}
|
||||
@ -1,7 +1,14 @@
|
||||
// Common utils (main/renderer)
|
||||
// Common utils (main OR renderer)
|
||||
|
||||
export * from "./app-version"
|
||||
export * from "./autobind"
|
||||
export * from "./base64"
|
||||
export * from "./camelCase"
|
||||
export * from "./splitArray"
|
||||
export * from "./getRandId"
|
||||
export * from "./cloneJson"
|
||||
export * from "./debouncePromise"
|
||||
export * from "./defineGlobal"
|
||||
export * from "./getRandId"
|
||||
export * from "./splitArray"
|
||||
export * from "./saveToAppFiles"
|
||||
export * from "./singleton"
|
||||
export * from "./cloneJson"
|
||||
|
||||
165
src/main/__test__/cluster.test.ts
Normal file
165
src/main/__test__/cluster.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
130
src/main/__test__/kube-auth-proxy.test.ts
Normal file
130
src/main/__test__/kube-auth-proxy.test.ts
Normal 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" }] })
|
||||
})
|
||||
})
|
||||
})
|
||||
112
src/main/__test__/kubeconfig-manager.test.ts
Normal file
112
src/main/__test__/kubeconfig-manager.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@ -46,6 +46,7 @@ export class Cluster implements ClusterModel {
|
||||
public contextHandler: ContextHandler;
|
||||
protected kubeconfigManager: KubeconfigManager;
|
||||
protected eventDisposers: Function[] = [];
|
||||
protected activated = false;
|
||||
|
||||
whenInitialized = when(() => this.initialized);
|
||||
whenReady = when(() => this.ready);
|
||||
@ -93,7 +94,7 @@ export class Cluster implements ClusterModel {
|
||||
async init(port: number) {
|
||||
try {
|
||||
this.contextHandler = new ContextHandler(this);
|
||||
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler, port);
|
||||
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
|
||||
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
||||
this.initialized = true;
|
||||
logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
|
||||
@ -126,7 +127,10 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
|
||||
@action
|
||||
async activate() {
|
||||
async activate(force = false ) {
|
||||
if (this.activated && !force) {
|
||||
return this.pushState();
|
||||
}
|
||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||
await this.whenInitialized;
|
||||
if (!this.eventDisposers.length) {
|
||||
@ -142,6 +146,7 @@ export class Cluster implements ClusterModel {
|
||||
this.kubeCtl = new Kubectl(this.version)
|
||||
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
|
||||
}
|
||||
this.activated = true
|
||||
return this.pushState();
|
||||
}
|
||||
|
||||
@ -162,6 +167,7 @@ export class Cluster implements ClusterModel {
|
||||
this.online = false;
|
||||
this.accessible = false;
|
||||
this.ready = false;
|
||||
this.activated = false;
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
@ -184,6 +190,7 @@ export class Cluster implements ClusterModel {
|
||||
this.refreshEvents(),
|
||||
this.refreshAllowedResources(),
|
||||
]);
|
||||
this.ready = true
|
||||
}
|
||||
this.pushState();
|
||||
}
|
||||
@ -234,7 +241,7 @@ export class Cluster implements ClusterModel {
|
||||
const apiUrl = this.kubeProxyUrl + path;
|
||||
return request(apiUrl, {
|
||||
json: true,
|
||||
timeout: 5000,
|
||||
timeout: 30000,
|
||||
...options,
|
||||
headers: {
|
||||
Host: `${this.id}.${new URL(this.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
|
||||
|
||||
@ -2,7 +2,7 @@ import { ChildProcess, spawn } from "child_process"
|
||||
import { waitUntilUsed } from "tcp-port-used";
|
||||
import { broadcastIpc } from "../common/ipc";
|
||||
import type { Cluster } from "./cluster"
|
||||
import { bundledKubectl, Kubectl } from "./kubectl"
|
||||
import { Kubectl } from "./kubectl"
|
||||
import logger from "./logger"
|
||||
|
||||
export interface KubeAuthProxyLog {
|
||||
@ -23,7 +23,7 @@ export class KubeAuthProxy {
|
||||
this.env = env
|
||||
this.port = port
|
||||
this.cluster = cluster
|
||||
this.kubectl = bundledKubectl
|
||||
this.kubectl = Kubectl.bundled()
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
|
||||
@ -11,8 +11,12 @@ export class KubeconfigManager {
|
||||
protected configDir = app.getPath("temp")
|
||||
protected tempFile: string;
|
||||
|
||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) {
|
||||
this.init();
|
||||
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
|
||||
|
||||
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
|
||||
const kcm = new KubeconfigManager(cluster, contextHandler, port)
|
||||
await kcm.init()
|
||||
return kcm
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
@ -72,8 +76,13 @@ export class KubeconfigManager {
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
unlink() {
|
||||
async unlink() {
|
||||
if (!this.tempFile) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Deleting temporary kubeconfig: ' + this.tempFile)
|
||||
fs.unlinkSync(this.tempFile)
|
||||
await fs.unlink(this.tempFile)
|
||||
this.tempFile = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,15 +36,21 @@ const packageMirrors: Map<string, string> = new Map([
|
||||
let bundledPath: string
|
||||
const initScriptVersionString = "# lens-initscript v3\n"
|
||||
|
||||
if (isDevelopment || isTestEnv) {
|
||||
const platformName = isWindows ? "windows" : process.platform
|
||||
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
|
||||
} else {
|
||||
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
|
||||
}
|
||||
export function bundledKubectlPath(): string {
|
||||
if (bundledPath) { return bundledPath }
|
||||
|
||||
if (isWindows) {
|
||||
bundledPath = `${bundledPath}.exe`
|
||||
if (isDevelopment || isTestEnv) {
|
||||
const platformName = isWindows ? "windows" : process.platform
|
||||
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
|
||||
} else {
|
||||
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
bundledPath = `${bundledPath}.exe`
|
||||
}
|
||||
|
||||
return bundledPath
|
||||
}
|
||||
|
||||
export class Kubectl {
|
||||
@ -58,7 +64,6 @@ export class Kubectl {
|
||||
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
|
||||
}
|
||||
|
||||
public static readonly bundledKubectlPath = bundledPath
|
||||
public static readonly bundledKubectlVersion: string = bundledVersion
|
||||
public static invalidBundle = false
|
||||
private static bundledInstance: Kubectl;
|
||||
@ -102,7 +107,7 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
public getBundledPath() {
|
||||
return Kubectl.bundledKubectlPath
|
||||
return bundledKubectlPath()
|
||||
}
|
||||
|
||||
public getPathFromPreferences() {
|
||||
@ -125,19 +130,19 @@ export class Kubectl {
|
||||
// return binary name if bundled path is not functional
|
||||
if (!await this.checkBinary(this.getBundledPath(), false)) {
|
||||
Kubectl.invalidBundle = true
|
||||
return path.basename(bundledPath)
|
||||
return path.basename(this.getBundledPath())
|
||||
}
|
||||
|
||||
try {
|
||||
if (!await this.ensureKubectl()) {
|
||||
logger.error("Failed to ensure kubectl, fallback to the bundled version")
|
||||
return Kubectl.bundledKubectlPath
|
||||
return this.getBundledPath()
|
||||
}
|
||||
return this.path
|
||||
} catch (err) {
|
||||
logger.error("Failed to ensure kubectl, fallback to the bundled version")
|
||||
logger.error(err)
|
||||
return Kubectl.bundledKubectlPath
|
||||
return this.getBundledPath()
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,7 +188,7 @@ export class Kubectl {
|
||||
try {
|
||||
const exist = await pathExists(this.path)
|
||||
if (!exist) {
|
||||
await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path)
|
||||
await fs.promises.copyFile(this.getBundledPath(), this.path)
|
||||
await fs.promises.chmod(this.path, 0o755)
|
||||
}
|
||||
return true
|
||||
@ -332,6 +337,3 @@ export class Kubectl {
|
||||
return packageMirrors.get("default") // MacOS packages are only available from default
|
||||
}
|
||||
}
|
||||
|
||||
const bundledKubectl = Kubectl.bundled()
|
||||
export { bundledKubectl }
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import packageInfo from "../../package.json"
|
||||
import path from "path"
|
||||
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
|
||||
import { Kubectl } from "../../src/main/kubectl";
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
jest.mock("../common/user-store");
|
||||
|
||||
describe("kubectlVersion", () => {
|
||||
it("returns bundled version if exactly same version used", async () => {
|
||||
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
|
||||
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
|
||||
const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion)
|
||||
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
|
||||
})
|
||||
|
||||
it("returns bundled version if same major.minor version is used", async () => {
|
||||
const { bundledKubectlVersion } = packageInfo.config;
|
||||
const kubectl = new Kubectl(bundledKubectlVersion);
|
||||
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
|
||||
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
|
||||
import { bundledKubectl } from "../kubectl"
|
||||
import { Kubectl } from "../kubectl"
|
||||
import { getFreePort } from "../port"
|
||||
import { shell } from "electron"
|
||||
import * as tcpPortUsed from "tcp-port-used"
|
||||
@ -37,7 +37,7 @@ class PortForward {
|
||||
|
||||
public async start() {
|
||||
this.localPort = await getFreePort()
|
||||
const kubectlBin = await bundledKubectl.getPath()
|
||||
const kubectlBin = await Kubectl.bundled().getPath()
|
||||
const args = [
|
||||
"--kubeconfig", this.kubeConfig,
|
||||
"port-forward",
|
||||
|
||||
@ -38,7 +38,7 @@ export class ShellSession extends EventEmitter {
|
||||
|
||||
public async open() {
|
||||
this.kubectlBinDir = await this.kubectl.binDir()
|
||||
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath
|
||||
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath()
|
||||
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
|
||||
this.helmBinDir = helmCli.getBinaryDir()
|
||||
const env = await this.getCachedShellEnv()
|
||||
|
||||
@ -3,7 +3,7 @@ import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuP
|
||||
import type React from "react";
|
||||
|
||||
import { observable } from "mobx";
|
||||
import { autobind } from "../utils/autobind";
|
||||
import { autobind } from "../utils";
|
||||
import { KubeApi } from "./kube-api";
|
||||
|
||||
export interface ApiComponents {
|
||||
|
||||
@ -42,11 +42,14 @@ export interface IPodMetrics<T = IMetrics> {
|
||||
networkTransmit: T;
|
||||
}
|
||||
|
||||
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
|
||||
export interface IPodLogsQuery {
|
||||
container?: string;
|
||||
tailLines?: number;
|
||||
timestamps?: boolean;
|
||||
sinceTime?: string; // Date.toISOString()-format
|
||||
follow?: boolean;
|
||||
previous?: boolean;
|
||||
}
|
||||
|
||||
export enum PodStatus {
|
||||
|
||||
@ -13,7 +13,7 @@ import { AceEditor } from "../ace-editor";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig } from "../../../common/kube-helpers";
|
||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
||||
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { v4 as uuid } from "uuid"
|
||||
@ -23,6 +23,7 @@ import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { ExecValidationNotFoundError } from "../../../common/custom-errors";
|
||||
|
||||
enum KubeConfigSourceTab {
|
||||
FILE = "file",
|
||||
@ -118,6 +119,9 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
|
||||
addClusters = () => {
|
||||
const configValidationErrors:string[] = [];
|
||||
let newClusters: ClusterModel[] = [];
|
||||
|
||||
try {
|
||||
if (!this.selectedContexts.length) {
|
||||
this.error = <Trans>Please select at least one cluster context</Trans>
|
||||
@ -125,7 +129,22 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
this.error = ""
|
||||
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 kubeConfig = this.kubeContexts.get(context);
|
||||
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE
|
||||
@ -141,7 +160,8 @@ export class AddCluster extends React.Component {
|
||||
httpsProxy: this.proxyServer || undefined,
|
||||
},
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
runInAction(() => {
|
||||
clusterStore.addCluster(...newClusters);
|
||||
if (newClusters.length === 1) {
|
||||
@ -149,9 +169,11 @@ export class AddCluster extends React.Component {
|
||||
clusterStore.setActive(clusterId);
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
} else {
|
||||
Notifications.ok(
|
||||
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
|
||||
);
|
||||
if (newClusters.length > 1) {
|
||||
Notifications.ok(
|
||||
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
this.refreshContexts();
|
||||
|
||||
@ -46,8 +46,9 @@ export class ClusterSettings extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
refreshCluster = () => {
|
||||
refreshCluster = async () => {
|
||||
if(this.cluster) {
|
||||
await clusterIpc.activate.invokeFromRenderer(this.cluster.id);
|
||||
clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { Input } from '../input';
|
||||
import { SubTitle } from '../layout/sub-title';
|
||||
import { UserPreferences, userStore } from '../../../common/user-store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Kubectl } from '../../../main/kubectl';
|
||||
import { bundledKubectlPath } from '../../../main/kubectl';
|
||||
import { SelectOption, Select } from '../select';
|
||||
|
||||
export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => {
|
||||
@ -58,7 +58,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
|
||||
<SubTitle title="Path to Kubectl binary" />
|
||||
<Input
|
||||
theme="round-black"
|
||||
placeholder={Kubectl.bundledKubectlPath}
|
||||
placeholder={bundledKubectlPath()}
|
||||
value={binariesPath}
|
||||
validators={isPath}
|
||||
onChange={setBinariesPath}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -3,15 +3,15 @@ import "./pod-menu.scss";
|
||||
import React from "react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { MenuItem, SubMenu } from "../menu";
|
||||
import { IPodContainer, Pod, nodesApi } from "../../api/endpoints";
|
||||
import { IPodContainer, Pod } from "../../api/endpoints";
|
||||
import { Icon } from "../icon";
|
||||
import { StatusBrick } from "../status-brick";
|
||||
import { PodLogsDialog } from "./pod-logs-dialog";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import { terminalStore, createTerminalTab } from "../dock/terminal.store";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { hideDetails } from "../../navigation";
|
||||
import { createPodLogsTab } from "../dock/pod-logs.store";
|
||||
|
||||
interface Props extends KubeObjectMenuProps<Pod> {
|
||||
}
|
||||
@ -42,7 +42,17 @@ export class PodMenu extends React.Component<Props> {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@ -23,7 +23,6 @@ import { eventRoute } from "./+events";
|
||||
import { Apps, appsRoute } from "./+apps";
|
||||
import { KubeObjectDetails } from "./kube-object/kube-object-details";
|
||||
import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
|
||||
import { PodLogsDialog } from "./+workloads-pods/pod-logs-dialog";
|
||||
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
|
||||
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
|
||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||
@ -90,7 +89,6 @@ export class App extends React.Component {
|
||||
<KubeObjectDetails/>
|
||||
<KubeConfigDialog/>
|
||||
<AddRoleBindingDialog/>
|
||||
<PodLogsDialog/>
|
||||
<DeploymentScaleDialog/>
|
||||
<CronJobTriggerDialog/>
|
||||
</ErrorBoundary>
|
||||
|
||||
@ -47,13 +47,14 @@ export class ClusterStatus extends React.Component<Props> {
|
||||
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
|
||||
}
|
||||
|
||||
activateCluster = async () => {
|
||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
|
||||
activateCluster = async (force = false) => {
|
||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId, force);
|
||||
}
|
||||
|
||||
reconnect = async () => {
|
||||
this.authOutput = []
|
||||
this.isReconnecting = true;
|
||||
await this.activateCluster();
|
||||
await this.activateCluster(true);
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { hasLoadedView } from "./lens-views";
|
||||
export class ClusterView extends React.Component {
|
||||
render() {
|
||||
const cluster = getMatchedCluster();
|
||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id))
|
||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready)
|
||||
return (
|
||||
<div className="ClusterView">
|
||||
{showStatus && (
|
||||
|
||||
@ -11,6 +11,7 @@ export enum TabKind {
|
||||
EDIT_RESOURCE = "edit-resource",
|
||||
INSTALL_CHART = "install-chart",
|
||||
UPGRADE_CHART = "upgrade-chart",
|
||||
POD_LOGS = "pod-logs",
|
||||
}
|
||||
|
||||
export interface IDockTab {
|
||||
|
||||
@ -22,6 +22,8 @@ import { createResourceTab, isCreateResourceTab } from "./create-resource.store"
|
||||
import { isEditResourceTab } from "./edit-resource.store";
|
||||
import { isInstallChartTab } from "./install-chart.store";
|
||||
import { isUpgradeChartTab } from "./upgrade-chart.store";
|
||||
import { PodLogs } from "./pod-logs";
|
||||
import { isPodLogsTab } from "./pod-logs.store";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -59,6 +61,9 @@ export class Dock extends React.Component<Props> {
|
||||
if (isInstallChartTab(tab) || isUpgradeChartTab(tab)) {
|
||||
return <DockTab value={tab} icon={<Icon svg="install" />} />
|
||||
}
|
||||
if (isPodLogsTab(tab)) {
|
||||
return <DockTab value={tab} icon="subject" />
|
||||
}
|
||||
}
|
||||
|
||||
renderTabContent() {
|
||||
@ -71,6 +76,7 @@ export class Dock extends React.Component<Props> {
|
||||
{isInstallChartTab(tab) && <InstallChart tab={tab} />}
|
||||
{isUpgradeChartTab(tab) && <UpgradeChart tab={tab} />}
|
||||
{isTerminalTab(tab) && <TerminalWindow tab={tab} />}
|
||||
{isPodLogsTab(tab) && <PodLogs tab={tab} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { Notifications } from "../notifications";
|
||||
|
||||
interface Props extends OptionalProps {
|
||||
tabId: TabId;
|
||||
submit: () => Promise<ReactNode | string>;
|
||||
submit?: () => Promise<ReactNode | string>;
|
||||
}
|
||||
|
||||
interface OptionalProps {
|
||||
@ -23,6 +23,7 @@ interface OptionalProps {
|
||||
submitLabel?: ReactNode;
|
||||
submittingMessage?: ReactNode;
|
||||
disableSubmit?: boolean;
|
||||
showButtons?: boolean
|
||||
showSubmitClose?: boolean;
|
||||
showInlineInfo?: boolean;
|
||||
showNotifications?: boolean;
|
||||
@ -33,6 +34,7 @@ export class InfoPanel extends Component<Props> {
|
||||
static defaultProps: OptionalProps = {
|
||||
submitLabel: <Trans>Submit</Trans>,
|
||||
submittingMessage: <Trans>Submitting..</Trans>,
|
||||
showButtons: true,
|
||||
showSubmitClose: true,
|
||||
showInlineInfo: true,
|
||||
showNotifications: true,
|
||||
@ -87,7 +89,7 @@ export class InfoPanel extends Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showSubmitClose } = this.props;
|
||||
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props;
|
||||
const { submit, close, submitAndClose, waiting } = this;
|
||||
const isDisabled = !!(disableSubmit || waiting || error);
|
||||
return (
|
||||
@ -98,22 +100,26 @@ export class InfoPanel extends Component<Props> {
|
||||
<div className="info flex gaps align-center">
|
||||
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
|
||||
</div>
|
||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
||||
<Button
|
||||
active
|
||||
outlined={showSubmitClose}
|
||||
primary={!showSubmitClose}// one button always should be primary (blue)
|
||||
label={submitLabel}
|
||||
onClick={submit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
{showSubmitClose && (
|
||||
<Button
|
||||
primary active
|
||||
label={<Trans>{submitLabel} & Close</Trans>}
|
||||
onClick={submitAndClose}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
{showButtons && (
|
||||
<>
|
||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
||||
<Button
|
||||
active
|
||||
outlined={showSubmitClose}
|
||||
primary={!showSubmitClose}// one button always should be primary (blue)
|
||||
label={submitLabel}
|
||||
onClick={submit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
{showSubmitClose && (
|
||||
<Button
|
||||
primary active
|
||||
label={<Trans>{submitLabel} & Close</Trans>}
|
||||
onClick={submitAndClose}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
43
src/renderer/components/dock/pod-logs.scss
Normal file
43
src/renderer/components/dock/pod-logs.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/renderer/components/dock/pod-logs.store.ts
Normal file
128
src/renderer/components/dock/pod-logs.store.ts
Normal 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;
|
||||
}
|
||||
246
src/renderer/components/dock/pod-logs.tsx
Normal file
246
src/renderer/components/dock/pod-logs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { FitAddon } from "xterm-addon-fit";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
import { TerminalApi } from "../../api/terminal-api";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { autobind } from "../../utils/autobind";
|
||||
import { autobind } from "../../utils";
|
||||
|
||||
export class Terminal {
|
||||
static spawningPool: HTMLElement;
|
||||
|
||||
@ -39,7 +39,7 @@ export const isNumber: Validator = {
|
||||
export const isUrl: Validator = {
|
||||
condition: ({ type }) => type === "url",
|
||||
message: () => _i18n._(t`Wrong url format`),
|
||||
validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
|
||||
validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
|
||||
};
|
||||
|
||||
export const isPath: Validator = {
|
||||
|
||||
@ -7,7 +7,11 @@
|
||||
> label {
|
||||
color: inherit;
|
||||
border-radius: $radius;
|
||||
padding: $padding / 1.33 $padding * 1.25;
|
||||
padding: 6px 6px 6px 10px;
|
||||
|
||||
.Icon {
|
||||
height: $margin * 2;
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
|
||||
@ -15,6 +15,14 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.NamespaceSelect {
|
||||
.Select {
|
||||
&__value-container {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
label {
|
||||
background: none;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
.MainLayout {
|
||||
--sidebar-max-size: 200px;
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"aside header"
|
||||
@ -35,7 +33,7 @@
|
||||
transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.pinned {
|
||||
width: var(--sidebar-max-size);
|
||||
width: var(--sidebar-width);
|
||||
}
|
||||
|
||||
&:not(.pinned) {
|
||||
@ -45,7 +43,7 @@
|
||||
overflow: hidden;
|
||||
|
||||
&.accessible:hover {
|
||||
width: var(--sidebar-max-size);
|
||||
width: var(--sidebar-width);
|
||||
transition-delay: 750ms;
|
||||
box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35);
|
||||
z-index: $zIndex-sidebar-hover;
|
||||
|
||||
@ -3,11 +3,12 @@ import "./main-layout.scss";
|
||||
import React from "react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { createStorage, cssNames } from "../../utils";
|
||||
import { autobind, createStorage, cssNames } from "../../utils";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { ErrorBoundary } from "../error-boundary";
|
||||
import { Dock } from "../dock";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
|
||||
|
||||
export interface MainLayoutProps {
|
||||
className?: any;
|
||||
@ -18,22 +19,42 @@ export interface MainLayoutProps {
|
||||
|
||||
@observer
|
||||
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 isAccessible = true;
|
||||
@observable sidebarWidth = this.storage.get().sidebarWidth
|
||||
|
||||
@disposeOnUnmount syncPinnedStateWithStorage = reaction(
|
||||
() => this.isPinned,
|
||||
(isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
|
||||
);
|
||||
|
||||
@disposeOnUnmount syncWidthStateWithStorage = reaction(
|
||||
() => this.sidebarWidth,
|
||||
(sidebarWidth) => this.storage.merge({ sidebarWidth })
|
||||
);
|
||||
|
||||
toggleSidebar = () => {
|
||||
this.isPinned = !this.isPinned;
|
||||
this.isAccessible = false;
|
||||
setTimeout(() => (this.isAccessible = true), 250);
|
||||
};
|
||||
|
||||
getSidebarSize = () => {
|
||||
return {
|
||||
"--sidebar-width": `${this.sidebarWidth}px`,
|
||||
}
|
||||
}
|
||||
|
||||
@autobind()
|
||||
adjustWidth(newWidth: number): void {
|
||||
this.sidebarWidth = newWidth
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, headerClass, footer, footerClass, children } = this.props;
|
||||
const cluster = getHostedCluster();
|
||||
@ -41,20 +62,31 @@ export class MainLayout extends React.Component<MainLayoutProps> {
|
||||
return null; // fix: skip render when removing active (visible) cluster
|
||||
}
|
||||
return (
|
||||
<div className={cssNames("MainLayout", className)}>
|
||||
<div className={cssNames("MainLayout", className)} style={this.getSidebarSize() as any}>
|
||||
<header className={cssNames("flex gaps align-center", headerClass)}>
|
||||
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
|
||||
</header>
|
||||
|
||||
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
||||
<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>
|
||||
|
||||
<main>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</main>
|
||||
|
||||
<footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer>
|
||||
<footer className={footerClass}>{footer ?? <Dock />}</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { autobind } from "./utils/autobind";
|
||||
import { autobind } from "./utils";
|
||||
import { userStore } from "../common/user-store";
|
||||
import logger from "../main/logger";
|
||||
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
"dockHeadBackground": "#2e3136",
|
||||
"dockInfoBackground": "#1e2125",
|
||||
"dockInfoBorderColor": "#303136",
|
||||
"logsBackground": "#000000",
|
||||
"terminalBackground": "#000000",
|
||||
"terminalForeground": "#ffffff",
|
||||
"terminalCursor": "#ffffff",
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
"dockHeadBackground": "#e8e8e8",
|
||||
"dockInfoBackground": "#e8e8e8",
|
||||
"dockInfoBorderColor": "#c9cfd3",
|
||||
"logsBackground": "#ffffff",
|
||||
"terminalBackground": "#ffffff",
|
||||
"terminalForeground": "#2d2d2d",
|
||||
"terminalCursor": "#2d2d2d",
|
||||
|
||||
@ -91,6 +91,9 @@ $terminalBrightMagenta: var(--terminalBrightMagenta);
|
||||
$terminalBrightCyan: var(--terminalBrightCyan);
|
||||
$terminalBrightWhite: var(--terminalBrightWhite);
|
||||
|
||||
// Logs
|
||||
$logsBackground: var(--logsBackground);
|
||||
|
||||
// Dialogs
|
||||
$dialogTextColor: var(--dialogTextColor);
|
||||
$dialogBackground: var(--dialogBackground);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
@ -3,21 +3,18 @@
|
||||
export const noop: any = Function();
|
||||
export const isElectron = !!navigator.userAgent.match(/Electron/);
|
||||
|
||||
export * from '../../common/utils/camelCase'
|
||||
export * from '../../common/utils/base64'
|
||||
export * from "../../common/utils"
|
||||
|
||||
export * from './autobind'
|
||||
export * from './cssVar'
|
||||
export * from './cssNames'
|
||||
export * from './eventEmitter'
|
||||
export * from './downloadFile'
|
||||
export * from './prevDefault'
|
||||
export * from './createStorage'
|
||||
export * from './interval'
|
||||
export * from './debouncePromise'
|
||||
export * from './copyToClipboard'
|
||||
export * from './formatDuration'
|
||||
export * from './isReactNode'
|
||||
export * from './convertMemory'
|
||||
export * from './convertCpu'
|
||||
export * from './metricUnitsToNumber'
|
||||
export * from "./cssVar"
|
||||
export * from "./cssNames"
|
||||
export * from "./eventEmitter"
|
||||
export * from "./downloadFile"
|
||||
export * from "./prevDefault"
|
||||
export * from "./createStorage"
|
||||
export * from "./interval"
|
||||
export * from "./copyToClipboard"
|
||||
export * from "./formatDuration"
|
||||
export * from "./isReactNode"
|
||||
export * from "./convertMemory"
|
||||
export * from "./convertCpu"
|
||||
export * from "./metricUnitsToNumber"
|
||||
|
||||
@ -2,7 +2,12 @@
|
||||
|
||||
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
||||
|
||||
## 3.6.5 (current version)
|
||||
## 3.6.6 (current version)
|
||||
- Fix labels' word boundary to cover only drawer badges
|
||||
- Fix cluster dashboard opening not to start authentication proxy twice
|
||||
- Fix: Refresh cluster connection status also when connection is disconnected
|
||||
|
||||
## 3.6.5
|
||||
- Prevent drawer close when revealing secret value
|
||||
- Fix app crash when CRD conditions were not present
|
||||
- Add support for Stacklight prometheus metrics
|
||||
|
||||
16
types/command-exists.d.ts
vendored
Normal file
16
types/command-exists.d.ts
vendored
Normal 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;
|
||||
}
|
||||
17
yarn.lock
17
yarn.lock
@ -4122,6 +4122,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
command-exists@1.2.9:
|
||||
version "1.2.9"
|
||||
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
|
||||
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
|
||||
|
||||
commander@*, commander@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
||||
@ -7599,6 +7604,13 @@ jest-message-util@^26.0.1:
|
||||
slash "^3.0.0"
|
||||
stack-utils "^2.0.2"
|
||||
|
||||
jest-mock-extended@^1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.10.tgz#a4b1f5b0bb1121acf7c58cd5423d04c473532702"
|
||||
integrity sha512-R2wKiOgEUPoHZ2kLsAQeQP2IfVEgo3oQqWLSXKdMXK06t3UHkQirA2Xnsdqg/pX6KPWTsdnrzE2ig6nqNjdgVw==
|
||||
dependencies:
|
||||
ts-essentials "^4.0.0"
|
||||
|
||||
jest-mock@^26.0.1:
|
||||
version "26.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.0.1.tgz#7fd1517ed4955397cf1620a771dc2d61fad8fd40"
|
||||
@ -12692,6 +12704,11 @@ truncate-utf8-bytes@^1.0.0:
|
||||
dependencies:
|
||||
utf8-byte-length "^1.0.1"
|
||||
|
||||
ts-essentials@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab"
|
||||
integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==
|
||||
|
||||
ts-jest@^26.1.0:
|
||||
version "26.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.3.0.tgz#6b2845045347dce394f069bb59358253bc1338a9"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user