mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extensions-api
This commit is contained in:
commit
de3849d22c
53 files changed
+1273
-584
No files matched your search
@@ -93,6 +93,10 @@ export class BaseStore<T = any> extends Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
ipcRenderer.removeAllListeners(this.syncChannel)
|
||||
}
|
||||
|
||||
disableSync() {
|
||||
this.syncDisposers.forEach(dispose => dispose());
|
||||
this.syncDisposers.length = 0;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const clusterIpc = {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||
return cluster.activate(true);
|
||||
return cluster.activate();
|
||||
}
|
||||
},
|
||||
}),
|
||||
@@ -58,4 +58,4 @@ export const clusterIpc = {
|
||||
return clusterStore.getById(clusterId)?.upgradeFeature(feature, config)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -1,6 +1,6 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote } from "electron";
|
||||
import { app, ipcRenderer, remote, webFrame, webContents } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
@@ -73,20 +73,27 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
migrations: migrations,
|
||||
});
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
||||
this.applyWithoutSync(() => {
|
||||
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host}`, model);
|
||||
this.getById(model.id)?.updateModel(model);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||
|
||||
registerIpcListener() {
|
||||
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
|
||||
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
||||
this.applyWithoutSync(() => {
|
||||
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model);
|
||||
this.getById(model.id)?.updateModel(model);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
super.unregisterIpcListener()
|
||||
ipcRenderer.removeAllListeners("cluster:state")
|
||||
}
|
||||
|
||||
@computed get activeCluster(): Cluster | null {
|
||||
return this.getById(this.activeClusterId);
|
||||
}
|
||||
@@ -122,10 +129,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.clusters.size > 0;
|
||||
}
|
||||
|
||||
hasContext(name: string) {
|
||||
return this.clustersList.some(cluster => cluster.contextName === name);
|
||||
}
|
||||
|
||||
getById(id: ClusterId): Cluster {
|
||||
return this.clusters.get(id);
|
||||
}
|
||||
|
||||
@@ -146,14 +146,22 @@ describe("config with existing clusters", () => {
|
||||
id: 'cluster1',
|
||||
kubeConfig: 'foo',
|
||||
contextName: 'foo',
|
||||
preferences: { terminalCWD: '/foo' }
|
||||
preferences: { terminalCWD: '/foo' },
|
||||
workspace: 'default'
|
||||
},
|
||||
{
|
||||
id: 'cluster2',
|
||||
kubeConfig: 'foo2',
|
||||
contextName: 'foo2',
|
||||
preferences: { terminalCWD: '/foo2' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cluster3',
|
||||
kubeConfig: 'foo',
|
||||
contextName: 'foo',
|
||||
preferences: { terminalCWD: '/foo' },
|
||||
workspace: 'foo'
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -183,10 +191,12 @@ describe("config with existing clusters", () => {
|
||||
|
||||
it("allows getting all of the clusters", async () => {
|
||||
const storedClusters = clusterStore.clustersList;
|
||||
expect(storedClusters.length).toBe(3)
|
||||
expect(storedClusters[0].id).toBe('cluster1')
|
||||
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
|
||||
expect(storedClusters[1].id).toBe('cluster2')
|
||||
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
|
||||
expect(storedClusters[2].id).toBe('cluster3')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PrometheusLens } from "../main/prometheus/lens";
|
||||
import { PrometheusHelm } from "../main/prometheus/helm";
|
||||
import { PrometheusOperator } from "../main/prometheus/operator";
|
||||
import { PrometheusStacklight } from "../main/prometheus/stacklight";
|
||||
import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry";
|
||||
|
||||
[PrometheusLens, PrometheusHelm, PrometheusOperator].forEach(providerClass => {
|
||||
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
|
||||
const provider = new providerClass()
|
||||
PrometheusProviderRegistry.registerProvider(provider.id, provider)
|
||||
});
|
||||
|
||||
@@ -59,8 +59,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
colorTheme: UserStore.defaultTheme,
|
||||
downloadMirror: "default",
|
||||
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
|
||||
downloadBinariesPath: this.getDefaultKubectlPath(),
|
||||
kubectlBinariesPath: ""
|
||||
};
|
||||
|
||||
get isNewVersion() {
|
||||
|
||||
+8
-6
@@ -56,10 +56,10 @@ export class Cluster implements ClusterModel {
|
||||
@observable kubeConfigPath: string;
|
||||
@observable apiUrl: string; // cluster server url
|
||||
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
||||
@observable online: boolean;
|
||||
@observable accessible: boolean;
|
||||
@observable ready: boolean;
|
||||
@observable disconnected: boolean;
|
||||
@observable online = false;
|
||||
@observable accessible = false;
|
||||
@observable ready = false;
|
||||
@observable disconnected = true;
|
||||
@observable failureReason: string;
|
||||
@observable nodes = 0;
|
||||
@observable version: string;
|
||||
@@ -124,13 +124,14 @@ export class Cluster implements ClusterModel {
|
||||
this.eventDisposers.length = 0;
|
||||
}
|
||||
|
||||
async activate(init = false) {
|
||||
@action
|
||||
async activate() {
|
||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||
await this.whenInitialized;
|
||||
if (!this.eventDisposers.length) {
|
||||
this.bindEvents();
|
||||
}
|
||||
if (this.disconnected || (!init && !this.accessible)) {
|
||||
if (this.disconnected || !this.accessible) {
|
||||
await this.reconnect();
|
||||
}
|
||||
await this.refreshConnectionStatus()
|
||||
@@ -143,6 +144,7 @@ export class Cluster implements ClusterModel {
|
||||
return this.pushState();
|
||||
}
|
||||
|
||||
@action
|
||||
async reconnect() {
|
||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||
this.contextHandler.stopServer();
|
||||
|
||||
+18
-3
@@ -36,7 +36,7 @@ const packageMirrors: Map<string, string> = new Map([
|
||||
let bundledPath: string
|
||||
const initScriptVersionString = "# lens-initscript v3\n"
|
||||
|
||||
if (isDevelopment || isTestEnv) {
|
||||
if (isDevelopment || isTestEnv) {
|
||||
const platformName = isWindows ? "windows" : process.platform
|
||||
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
|
||||
} else {
|
||||
@@ -110,7 +110,11 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
protected getDownloadDir() {
|
||||
return userStore.preferences?.downloadBinariesPath || Kubectl.kubectlDir
|
||||
if (userStore.preferences?.downloadBinariesPath) {
|
||||
return path.join(userStore.preferences.downloadBinariesPath, "kubectl")
|
||||
}
|
||||
|
||||
return Kubectl.kubectlDir
|
||||
}
|
||||
|
||||
public async getPath(bundled = false): Promise<string> {
|
||||
@@ -214,7 +218,7 @@ export class Kubectl {
|
||||
});
|
||||
isValid = !await this.checkBinary(this.path, false)
|
||||
}
|
||||
if(!isValid) {
|
||||
if (!isValid) {
|
||||
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
|
||||
release()
|
||||
return false
|
||||
@@ -279,6 +283,12 @@ export class Kubectl {
|
||||
bashScript += "fi\n"
|
||||
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`
|
||||
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
|
||||
|
||||
bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
|
||||
bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
|
||||
bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
|
||||
bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
|
||||
bashScript += "export NO_PROXY\n"
|
||||
bashScript += "unset tempkubeconfig\n"
|
||||
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 })
|
||||
|
||||
@@ -304,6 +314,11 @@ export class Kubectl {
|
||||
zshScript += "d=${d/#:/}\n"
|
||||
zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n"
|
||||
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
|
||||
zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
|
||||
zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
|
||||
zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
|
||||
zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
|
||||
zshScript += "export NO_PROXY\n"
|
||||
zshScript += "unset tempkubeconfig\n"
|
||||
zshScript += "unset OLD_ZDOTDIR\n"
|
||||
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 })
|
||||
|
||||
+37
-16
@@ -44,9 +44,7 @@ export class LensProxy {
|
||||
const spdyProxy = spdy.createServer({
|
||||
spdy: {
|
||||
plain: true,
|
||||
connection: {
|
||||
autoSpdy31: true
|
||||
}
|
||||
protocols: ["http/1.1", "spdy/3.1"]
|
||||
}
|
||||
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res)
|
||||
@@ -55,11 +53,7 @@ export class LensProxy {
|
||||
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
} else {
|
||||
if (req.headers.upgrade?.startsWith("SPDY")) {
|
||||
this.handleSpdyProxy(proxy, req, socket, head)
|
||||
} else {
|
||||
socket.end()
|
||||
}
|
||||
this.handleProxyUpgrade(proxy, req, socket, head)
|
||||
}
|
||||
})
|
||||
spdyProxy.on("error", (err) => {
|
||||
@@ -68,17 +62,45 @@ export class LensProxy {
|
||||
return spdyProxy
|
||||
}
|
||||
|
||||
protected async handleSpdyProxy(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
if (cluster) {
|
||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "")
|
||||
const apiUrl = url.parse(cluster.apiUrl)
|
||||
const res = new http.ServerResponse(req)
|
||||
res.assignSocket(socket)
|
||||
res.setHeader("Location", proxyUrl)
|
||||
res.setHeader("Host", apiUrl.hostname)
|
||||
res.statusCode = 302
|
||||
res.end()
|
||||
const pUrl = url.parse(proxyUrl)
|
||||
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }
|
||||
const proxySocket = new net.Socket()
|
||||
proxySocket.connect(connectOpts, () => {
|
||||
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`)
|
||||
proxySocket.write(`Host: ${apiUrl.host}\r\n`)
|
||||
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
||||
const key = req.rawHeaders[i]
|
||||
if (key !== "Host" && key !== "Authorization") {
|
||||
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`)
|
||||
}
|
||||
}
|
||||
proxySocket.write("\r\n")
|
||||
proxySocket.write(head)
|
||||
})
|
||||
proxySocket.on('data', function (chunk) {
|
||||
socket.write(chunk)
|
||||
})
|
||||
proxySocket.on('end', function () {
|
||||
socket.end()
|
||||
})
|
||||
proxySocket.on('error', function (err) {
|
||||
socket.write("HTTP/" + req.httpVersion + " 500 Connection error\r\n\r\n");
|
||||
socket.end()
|
||||
})
|
||||
socket.on('data', function (chunk) {
|
||||
proxySocket.write(chunk)
|
||||
})
|
||||
socket.on('end', function () {
|
||||
proxySocket.end()
|
||||
})
|
||||
socket.on('error', function () {
|
||||
proxySocket.end()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +156,6 @@ export class LensProxy {
|
||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
if (cluster) {
|
||||
await cluster.contextHandler.ensureServer();
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler)
|
||||
if (proxyTarget) {
|
||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||
|
||||
@@ -37,19 +37,19 @@ export class PrometheusOperator implements PrometheusProvider {
|
||||
memoryUsage: `
|
||||
sum(
|
||||
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
|
||||
) by (node)
|
||||
)
|
||||
`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`),
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`,
|
||||
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`,
|
||||
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
|
||||
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`,
|
||||
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`,
|
||||
podUsage: `sum(kubelet_running_pod_count{node=~"${opts.nodes}"})`,
|
||||
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}) by (node)`
|
||||
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`
|
||||
}
|
||||
case 'nodes':
|
||||
return {
|
||||
@@ -62,10 +62,10 @@ export class PrometheusOperator implements PrometheusProvider {
|
||||
}
|
||||
case 'pods':
|
||||
return {
|
||||
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import logger from "../logger"
|
||||
|
||||
export class PrometheusStacklight implements PrometheusProvider {
|
||||
id = "stacklight"
|
||||
name = "Stacklight"
|
||||
rateAccuracy = "1m"
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||
try {
|
||||
const resp = await client.readNamespacedService("prometheus-server", "stacklight")
|
||||
const service = resp.body
|
||||
return {
|
||||
id: this.id,
|
||||
namespace: service.metadata.namespace,
|
||||
service: service.metadata.name,
|
||||
port: service.spec.ports[0].port
|
||||
}
|
||||
} catch(error) {
|
||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
|
||||
switch(opts.category) {
|
||||
case 'cluster':
|
||||
return {
|
||||
memoryUsage: `
|
||||
sum(
|
||||
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
|
||||
) by (kubernetes_name)
|
||||
`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`),
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
cpuUsage: `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
|
||||
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
podUsage: `sum(kubelet_running_pod_count{instance=~"${opts.nodes}"})`,
|
||||
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`
|
||||
}
|
||||
case 'nodes':
|
||||
return {
|
||||
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
|
||||
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`,
|
||||
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`
|
||||
}
|
||||
case 'pods':
|
||||
return {
|
||||
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
|
||||
}
|
||||
case 'pvc':
|
||||
return {
|
||||
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
|
||||
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
|
||||
}
|
||||
case 'ingress':
|
||||
const bytesSent = (ingress: string, statuses: string) =>
|
||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
|
||||
return {
|
||||
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
||||
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
||||
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
|
||||
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,8 @@ export class ShellSession extends EventEmitter {
|
||||
if (this.preferences.httpsProxy) {
|
||||
env["HTTPS_PROXY"] = this.preferences.httpsProxy
|
||||
}
|
||||
const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]]
|
||||
env["NO_PROXY"] = no_proxy.filter(address => !!address).join()
|
||||
if (env.DEBUG) { // do not pass debug option to bash
|
||||
delete env["DEBUG"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import version260Beta3 from "./2.6.0-beta.3"
|
||||
import version270Beta0 from "./2.7.0-beta.0"
|
||||
import version270Beta1 from "./2.7.0-beta.1"
|
||||
import version360Beta1 from "./3.6.0-beta.1"
|
||||
import snap from "./snap"
|
||||
|
||||
export default {
|
||||
...version200Beta2,
|
||||
@@ -16,4 +17,5 @@ export default {
|
||||
...version270Beta0,
|
||||
...version270Beta1,
|
||||
...version360Beta1,
|
||||
...snap
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Fix embedded kubeconfig paths under snap config
|
||||
|
||||
import { migration } from "../migration-wrapper";
|
||||
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
|
||||
import { getAppVersion } from "../../common/utils/app-version";
|
||||
import fs from "fs"
|
||||
|
||||
export default migration({
|
||||
version: getAppVersion(), // Run always after upgrade
|
||||
run(store, printLog) {
|
||||
if (!process.env["SNAP"]) return;
|
||||
|
||||
printLog("Migrating embedded kubeconfig paths")
|
||||
const storedClusters: ClusterModel[] = store.get("clusters") || [];
|
||||
if (!storedClusters.length) return;
|
||||
|
||||
printLog("Number of clusters to migrate: ", storedClusters.length)
|
||||
const migratedClusters = storedClusters
|
||||
.map(cluster => {
|
||||
/**
|
||||
* replace snap version with 'current' in kubeconfig path
|
||||
*/
|
||||
if (!fs.existsSync(cluster.kubeConfigPath)) {
|
||||
const kubeconfigPath = cluster.kubeConfigPath.replace(/\/snap\/kontena-lens\/[0-9]*\//, "/snap/kontena-lens/current/")
|
||||
cluster.kubeConfigPath = kubeconfigPath
|
||||
}
|
||||
return cluster;
|
||||
})
|
||||
|
||||
|
||||
store.set("clusters", migratedClusters)
|
||||
}
|
||||
})
|
||||
@@ -223,6 +223,7 @@ export class Issuer extends KubeObject {
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
if (!this.status?.conditions) return [];
|
||||
const { conditions = [] } = this.status;
|
||||
return conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
|
||||
@@ -9,12 +9,12 @@ type AdditionalPrinterColumnsCommon = {
|
||||
description: string;
|
||||
}
|
||||
|
||||
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
|
||||
jsonPath: string;
|
||||
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
|
||||
jsonPath: string;
|
||||
}
|
||||
|
||||
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
JSONPath: string;
|
||||
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
JSONPath: string;
|
||||
}
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject {
|
||||
@@ -50,7 +50,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type: string;
|
||||
type?: string;
|
||||
}[];
|
||||
acceptedNames: {
|
||||
plural: string;
|
||||
@@ -132,7 +132,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
if (!this.status.conditions) return [];
|
||||
if (!this.status?.conditions) return [];
|
||||
return this.status.conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
return {
|
||||
|
||||
@@ -115,12 +115,12 @@ export class KubeObject implements ItemObject {
|
||||
return KubeObject.stringifyLabels(this.metadata.labels);
|
||||
}
|
||||
|
||||
getAnnotations(): string[] {
|
||||
getAnnotations(filter = false): string[] {
|
||||
const labels = KubeObject.stringifyLabels(this.metadata.annotations);
|
||||
return labels.filter(label => {
|
||||
return filter ? labels.filter(label => {
|
||||
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
|
||||
return !skip;
|
||||
})
|
||||
}) : labels;
|
||||
}
|
||||
|
||||
getOwnerRefs() {
|
||||
@@ -138,7 +138,7 @@ export class KubeObject implements ItemObject {
|
||||
getNs(),
|
||||
getId(),
|
||||
...getLabels(),
|
||||
...getAnnotations(),
|
||||
...getAnnotations(true),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./components/app.scss"
|
||||
import React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import { isMac } from "../common/vars";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
@@ -30,12 +30,27 @@ export async function bootstrap(App: AppComponent) {
|
||||
themeStore.init(),
|
||||
]);
|
||||
|
||||
// Register additional store listeners
|
||||
clusterStore.registerIpcListener();
|
||||
|
||||
// init app's dependencies if any
|
||||
if (App.init) {
|
||||
await App.init();
|
||||
extensionStore.autoEnableOnLoad(getLensRuntime);
|
||||
}
|
||||
render(<App/>, rootElem);
|
||||
window.addEventListener("message", (ev: MessageEvent) => {
|
||||
if (ev.data === "teardown") {
|
||||
userStore.unregisterIpcListener()
|
||||
workspaceStore.unregisterIpcListener()
|
||||
clusterStore.unregisterIpcListener()
|
||||
unmountComponentAtNode(rootElem)
|
||||
window.location.href = "about:blank"
|
||||
}
|
||||
})
|
||||
render(<>
|
||||
{isMac && <div id="draggable-top" />}
|
||||
<App />
|
||||
</>, rootElem);
|
||||
}
|
||||
|
||||
// run
|
||||
|
||||
@@ -99,10 +99,7 @@ export class AddCluster extends React.Component {
|
||||
getContexts(config: KubeConfig): Map<string, KubeConfig> {
|
||||
const contexts = new Map();
|
||||
splitConfig(config).forEach(config => {
|
||||
const isExists = clusterStore.hasContext(config.currentContext);
|
||||
if (!isExists) {
|
||||
contexts.set(config.currentContext, config);
|
||||
}
|
||||
contexts.set(config.currentContext, config);
|
||||
})
|
||||
return contexts
|
||||
}
|
||||
|
||||
@@ -49,4 +49,4 @@
|
||||
.chart-description {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import "./helm-chart-details.scss";
|
||||
import React, { Component } from "react";
|
||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { autorun, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, toJS } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Drawer, DrawerItem } from "../drawer";
|
||||
import { autobind, stopPropagation } from "../../utils";
|
||||
import { MarkdownViewer } from "../markdown-viewer";
|
||||
@@ -25,39 +25,41 @@ interface Props {
|
||||
export class HelmChartDetails extends Component<Props> {
|
||||
@observable chartVersions: HelmChart[];
|
||||
@observable selectedChart: HelmChart;
|
||||
@observable description: string = null;
|
||||
@observable readme: string = null;
|
||||
@observable error: string = null;
|
||||
|
||||
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
|
||||
|
||||
@disposeOnUnmount
|
||||
chartSelector = autorun(async () => {
|
||||
if (!this.props.chart) return;
|
||||
this.chartVersions = null;
|
||||
this.selectedChart = null;
|
||||
this.description = null;
|
||||
this.loadChartData();
|
||||
this.chartPromise.then(data => {
|
||||
this.description = data.readme;
|
||||
this.chartVersions = data.versions;
|
||||
this.selectedChart = data.versions[0];
|
||||
});
|
||||
});
|
||||
async componentDidMount() {
|
||||
const { chart: { name, repo, version } } = this.props
|
||||
|
||||
loadChartData(version?: string) {
|
||||
const { chart: { name, repo } } = this.props;
|
||||
if (this.chartPromise) this.chartPromise.cancel();
|
||||
this.chartPromise = helmChartsApi.get(repo, name, version);
|
||||
try {
|
||||
const { readme, versions } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
|
||||
this.readme = readme
|
||||
this.chartVersions = versions
|
||||
this.selectedChart = versions[0]
|
||||
} catch (error) {
|
||||
this.error = error
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.chartPromise?.cancel();
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onVersionChange(opt: SelectOption) {
|
||||
const version = opt.value;
|
||||
async onVersionChange({ value: version }: SelectOption) {
|
||||
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
||||
this.description = null;
|
||||
this.loadChartData(version);
|
||||
this.chartPromise.then(data => {
|
||||
this.description = data.readme
|
||||
});
|
||||
this.readme = null;
|
||||
|
||||
try {
|
||||
this.chartPromise?.cancel();
|
||||
const { chart: { name, repo } } = this.props;
|
||||
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
|
||||
this.readme = readme;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind()
|
||||
@@ -79,7 +81,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
<div className="intro-contents box grow">
|
||||
<div className="description flex align-center justify-space-between">
|
||||
{selectedChart.getDescription()}
|
||||
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/>
|
||||
<Button primary label={_i18n._(t`Install`)} onClick={this.install} />
|
||||
</div>
|
||||
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
|
||||
<Select
|
||||
@@ -95,12 +97,12 @@ export class HelmChartDetails extends Component<Props> {
|
||||
</DrawerItem>
|
||||
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
|
||||
{selectedChart.getMaintainers().map(({ name, email, url }) =>
|
||||
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a>
|
||||
<a key={name} href={url || `mailto:${email}`} target="_blank">{name}</a>
|
||||
)}
|
||||
</DrawerItem>
|
||||
{selectedChart.getKeywords().length > 0 && (
|
||||
<DrawerItem name={_i18n._(t`Keywords`)} labelsOnly>
|
||||
{selectedChart.getKeywords().map(key => <Badge key={key} label={key}/>)}
|
||||
{selectedChart.getKeywords().map(key => <Badge key={key} label={key} />)}
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
@@ -108,14 +110,35 @@ export class HelmChartDetails extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
renderReadme() {
|
||||
if (this.readme === null) {
|
||||
return <Spinner center />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart-description">
|
||||
<MarkdownViewer markdown={this.readme} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.selectedChart === null || this.description === null) return <Spinner center/>;
|
||||
if (!this.selectedChart) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return (
|
||||
<div className="box grow">
|
||||
<p className="error">{this.error}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="box grow">
|
||||
{this.renderIntroduction()}
|
||||
<div className="chart-description">
|
||||
<MarkdownViewer markdown={this.description}/>
|
||||
</div>
|
||||
{this.renderReadme()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -135,4 +158,4 @@ export class HelmChartDetails extends Component<Props> {
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
|
||||
(items: HelmChart[]) => items.filter(item => !item.deprecated)
|
||||
]}
|
||||
customizeHeader={() => (
|
||||
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
|
||||
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
|
||||
)}
|
||||
renderTableHeader={[
|
||||
{ className: "icon" },
|
||||
@@ -99,11 +99,13 @@ export class HelmCharts extends Component<Props> {
|
||||
detailsItem={this.selectedChart}
|
||||
onDetails={this.showDetails}
|
||||
/>
|
||||
<HelmChartDetails
|
||||
chart={this.selectedChart}
|
||||
hideDetails={this.hideDetails}
|
||||
/>
|
||||
{this.selectedChart && (
|
||||
<HelmChartDetails
|
||||
chart={this.selectedChart}
|
||||
hideDetails={this.hideDetails}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@ import { apiManager } from "../../api/api-manager";
|
||||
import { crdStore } from "./crd.store";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Input } from "../input";
|
||||
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps {
|
||||
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
|
||||
}
|
||||
|
||||
function CrdColumnValue({ value }: { value: any[] | {} | string }) {
|
||||
@@ -66,12 +67,14 @@ export class CrdResourceDetails extends React.Component<Props> {
|
||||
})}
|
||||
{showStatus && (
|
||||
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly>
|
||||
{object.status.conditions.map((condition: { type: string; message: string; status: string }) => {
|
||||
const { type, message, status } = condition;
|
||||
{object.status.conditions.map((condition, index) => {
|
||||
const { type, reason, message, status } = condition;
|
||||
const kind = type || reason;
|
||||
if (!kind) return null;
|
||||
return (
|
||||
<Badge
|
||||
key={type} label={type}
|
||||
className={cssNames({ disabled: status === "False" }, type.toLowerCase())}
|
||||
key={kind + index} label={kind}
|
||||
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
|
||||
tooltip={message}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
const { value, label } = option;
|
||||
return label || (
|
||||
<>
|
||||
{showIcons && <Icon small material="layers"/>}
|
||||
{showIcons && <Icon small material="layers" />}
|
||||
{value}
|
||||
</>
|
||||
);
|
||||
@@ -91,14 +91,15 @@ export class NamespaceSelectFilter extends React.Component {
|
||||
closeMenuOnSelect={false}
|
||||
isOptionSelected={() => false}
|
||||
controlShouldRenderValue={false}
|
||||
onChange={({ value: namespace }: SelectOption) => toggleContext(namespace)}
|
||||
isMulti
|
||||
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
|
||||
formatOptionLabel={({ value: namespace }: SelectOption) => {
|
||||
const isSelected = hasContext(namespace);
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<FilterIcon type={FilterType.NAMESPACE}/>
|
||||
<FilterIcon type={FilterType.NAMESPACE} />
|
||||
<span>{namespace}</span>
|
||||
{isSelected && <Icon small material="check" className="box right"/>}
|
||||
{isSelected && <Icon small material="check" className="box right" />}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -18,44 +18,22 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
|
||||
{ value: "china", label: "China (Azure)" },
|
||||
]
|
||||
|
||||
|
||||
const save = () => {
|
||||
preferences.downloadBinariesPath = downloadPath;
|
||||
preferences.kubectlBinariesPath = binariesPath;
|
||||
}
|
||||
|
||||
const renderPath = () => {
|
||||
if (preferences.downloadKubectlBinaries) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Path to Kubectl binary"/>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={binariesPath}
|
||||
validators={isPath}
|
||||
onChange={setBinariesPath}
|
||||
onBlur={save}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Default:</Trans>{" "}{Kubectl.bundledKubectlPath}
|
||||
</small>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2><Trans>Kubectl Binary</Trans></h2>
|
||||
<small className="hint">
|
||||
<Trans>Download kubectl binaries matching to Kubernetes cluster verison.</Trans>
|
||||
</small>
|
||||
<Checkbox
|
||||
label={<Trans>Download kubectl binaries</Trans>}
|
||||
value={preferences.downloadKubectlBinaries}
|
||||
onChange={downloadKubectlBinaries => preferences.downloadKubectlBinaries = downloadKubectlBinaries}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Download kubectl binaries matching to Kubernetes cluster version.</Trans>
|
||||
</small>
|
||||
<SubTitle title="Download mirror" />
|
||||
<Select
|
||||
placeholder={<Trans>Download mirror for kubectl</Trans>}
|
||||
@@ -64,20 +42,32 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
|
||||
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
|
||||
disabled={!preferences.downloadKubectlBinaries}
|
||||
/>
|
||||
<SubTitle title="Directory for binaries"/>
|
||||
<SubTitle title="Directory for binaries" />
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={downloadPath}
|
||||
placeholder={`Directory to download binaries into`}
|
||||
placeholder={userStore.getDefaultKubectlPath()}
|
||||
validators={isPath}
|
||||
onChange={setDownloadPath}
|
||||
onBlur={save}
|
||||
disabled={!preferences.downloadKubectlBinaries}
|
||||
/>
|
||||
<small>
|
||||
Default: {userStore.getDefaultKubectlPath()}
|
||||
<small className="hint">
|
||||
The directory to download binaries into.
|
||||
</small>
|
||||
<SubTitle title="Path to Kubectl binary" />
|
||||
<Input
|
||||
theme="round-black"
|
||||
placeholder={Kubectl.bundledKubectlPath}
|
||||
value={binariesPath}
|
||||
validators={isPath}
|
||||
onChange={setBinariesPath}
|
||||
onBlur={save}
|
||||
disabled={preferences.downloadKubectlBinaries}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>The path to the kubectl binary on the system.</Trans>
|
||||
</small>
|
||||
{renderPath()}
|
||||
</>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -11,11 +11,9 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
|
||||
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
loadMetrics(daemonSet: DaemonSet) {
|
||||
async loadMetrics(daemonSet: DaemonSet) {
|
||||
const pods = this.getChildPods(daemonSet);
|
||||
return podsApi.getMetrics(pods, daemonSet.getNs(), "").then(metrics =>
|
||||
this.metrics = metrics
|
||||
);
|
||||
this.metrics = await podsApi.getMetrics(pods, daemonSet.getNs(), "");
|
||||
}
|
||||
|
||||
getChildPods(daemonSet: DaemonSet): Pod[] {
|
||||
|
||||
@@ -16,11 +16,9 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
|
||||
], "desc");
|
||||
}
|
||||
|
||||
loadMetrics(deployment: Deployment) {
|
||||
async loadMetrics(deployment: Deployment) {
|
||||
const pods = this.getChildPods(deployment);
|
||||
return podsApi.getMetrics(pods, deployment.getNs(), "").then(metrics =>
|
||||
this.metrics = metrics
|
||||
);
|
||||
this.metrics = await podsApi.getMetrics(pods, deployment.getNs(), "");
|
||||
}
|
||||
|
||||
getStatuses(deployments?: Deployment[]) {
|
||||
|
||||
@@ -5,72 +5,54 @@ import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { OverviewWorkloadStatus } from "./overview-workload-status";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cronJobsURL, daemonSetsURL, deploymentsURL, jobsURL, podsURL, statefulSetsURL } from "../+workloads";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { deploymentStore } from "../+workloads-deployments/deployments.store";
|
||||
import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store";
|
||||
import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
|
||||
import { jobStore } from "../+workloads-jobs/job.store";
|
||||
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
|
||||
import { workloadURL, workloadStores } from "../+workloads";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { PageFiltersList } from "../item-object-list/page-filters-list";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { isAllowedResource, KubeResource } from "../../../common/rbac";
|
||||
import { ResourceNames } from "../../../renderer/utils/rbac";
|
||||
import { autobind } from "../../utils";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
const resources: KubeResource[] = [
|
||||
"pods",
|
||||
"deployments",
|
||||
"statefulsets",
|
||||
"daemonsets",
|
||||
"jobs",
|
||||
"cronjobs",
|
||||
]
|
||||
|
||||
@observer
|
||||
export class OverviewStatuses extends React.Component {
|
||||
@autobind()
|
||||
renderWorkload(resource: KubeResource): React.ReactElement {
|
||||
const store = workloadStores[resource];
|
||||
const items = store.getAllByNs(namespaceStore.contextNs)
|
||||
return (
|
||||
<div className="workload" key={resource}>
|
||||
<div className="title">
|
||||
<Link to={workloadURL[resource]()}>{ResourceNames[resource]} ({items.length})</Link>
|
||||
</div>
|
||||
<OverviewWorkloadStatus status={store.getStatuses(items)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { contextNs } = namespaceStore;
|
||||
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
|
||||
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
|
||||
const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs(contextNs) : [];
|
||||
const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs(contextNs) : [];
|
||||
const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs(contextNs) : [];
|
||||
const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs(contextNs) : [];
|
||||
const workloads = resources
|
||||
.filter(isAllowedResource)
|
||||
.map(this.renderWorkload);
|
||||
|
||||
return (
|
||||
<div className="OverviewStatuses">
|
||||
<div className="header flex gaps align-center">
|
||||
<h5 className="box grow"><Trans>Overview</Trans></h5>
|
||||
<NamespaceSelectFilter/>
|
||||
<NamespaceSelectFilter />
|
||||
</div>
|
||||
<PageFiltersList/>
|
||||
<PageFiltersList />
|
||||
<div className="workloads">
|
||||
{isAllowedResource("pods") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
|
||||
</div>
|
||||
}
|
||||
{isAllowedResource("deployments") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
|
||||
</div>
|
||||
}
|
||||
{isAllowedResource("statefulsets") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
|
||||
</div>
|
||||
}
|
||||
{isAllowedResource("daemonsets") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
|
||||
</div>
|
||||
}
|
||||
{isAllowedResource("jobs") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
|
||||
</div>
|
||||
}
|
||||
{isAllowedResource("cronjobs") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>
|
||||
</div>
|
||||
}
|
||||
{workloads}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -113,7 +113,8 @@ const SecretKey = (props: SecretKeyProps) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [secret, setSecret] = useState<Secret>()
|
||||
|
||||
const showKey = async () => {
|
||||
const showKey = async (evt: React.MouseEvent) => {
|
||||
evt.preventDefault()
|
||||
setLoading(true)
|
||||
const secret = await secretsStore.load({ name, namespace });
|
||||
setLoading(false)
|
||||
@@ -126,7 +127,7 @@ const SecretKey = (props: SecretKeyProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
secretKeyRef({name}.{key})
|
||||
secretKeyRef({name}.{key})
|
||||
<Icon
|
||||
className={cssNames("secret-button", { loading })}
|
||||
material="visibility"
|
||||
|
||||
@@ -10,11 +10,9 @@ export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
|
||||
api = replicaSetApi
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
loadMetrics(replicaSet: ReplicaSet) {
|
||||
async loadMetrics(replicaSet: ReplicaSet) {
|
||||
const pods = this.getChildPods(replicaSet);
|
||||
return podsApi.getMetrics(pods, replicaSet.getNs(), "").then(metrics =>
|
||||
this.metrics = metrics
|
||||
);
|
||||
this.metrics = await podsApi.getMetrics(pods, replicaSet.getNs(), "");
|
||||
}
|
||||
|
||||
getChildPods(replicaSet: ReplicaSet) {
|
||||
|
||||
@@ -10,11 +10,9 @@ export class StatefulSetStore extends KubeObjectStore<StatefulSet> {
|
||||
api = statefulSetApi
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
loadMetrics(statefulSet: StatefulSet) {
|
||||
async loadMetrics(statefulSet: StatefulSet) {
|
||||
const pods = this.getChildPods(statefulSet);
|
||||
return podsApi.getMetrics(pods, statefulSet.getNs(), "").then(metrics =>
|
||||
this.metrics = metrics
|
||||
);
|
||||
this.metrics = await podsApi.getMetrics(pods, statefulSet.getNs(), "");
|
||||
}
|
||||
|
||||
getChildPods(statefulSet: StatefulSet) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./workloads.route"
|
||||
export * from "./workloads"
|
||||
|
||||
export * from "./workloads.stores"
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { Workloads } from "./workloads";
|
||||
import { buildURL, IURLParams } from "../../navigation";
|
||||
import { KubeResource } from "../../../common/rbac";
|
||||
|
||||
export const workloadsRoute: RouteProps = {
|
||||
get path() {
|
||||
@@ -62,3 +63,12 @@ export const daemonSetsURL = buildURL<IDaemonSetsRouteParams>(daemonSetsRoute.pa
|
||||
export const statefulSetsURL = buildURL<IStatefulSetsRouteParams>(statefulSetsRoute.path)
|
||||
export const jobsURL = buildURL<IJobsRouteParams>(jobsRoute.path)
|
||||
export const cronJobsURL = buildURL<ICronJobsRouteParams>(cronJobsRoute.path)
|
||||
|
||||
export const workloadURL: Partial<Record<KubeResource, ReturnType<typeof buildURL>>> = {
|
||||
"pods": podsURL,
|
||||
"deployments": deploymentsURL,
|
||||
"daemonsets": daemonSetsURL,
|
||||
"statefulsets": statefulSetsURL,
|
||||
"jobs": jobsURL,
|
||||
"cronjobs": cronJobsURL,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { deploymentStore } from "../+workloads-deployments/deployments.store";
|
||||
import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store";
|
||||
import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
|
||||
import { jobStore } from "../+workloads-jobs/job.store";
|
||||
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
|
||||
import { KubeResource } from "../../../common/rbac";
|
||||
|
||||
export const workloadStores: Partial<Record<KubeResource, KubeObjectStore>> = {
|
||||
"pods": podsStore,
|
||||
"deployments": deploymentStore,
|
||||
"daemonsets": daemonSetStore,
|
||||
"statefulsets": statefulSetStore,
|
||||
"jobs": jobStore,
|
||||
"cronjobs": cronJobStore,
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
:root {
|
||||
--mainBackground: #1e2124;
|
||||
--main-layout-header: 40px;
|
||||
--drag-region-height: 22px
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -47,7 +48,7 @@ html, body {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: var(--main-layout-header);
|
||||
height: var(--drag-region-height);
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export const memoryOptions: ChartOptions = {
|
||||
}
|
||||
return bytesToUnits(parseInt(value));
|
||||
}
|
||||
return `${value}`;
|
||||
return bytesToUnits(value);
|
||||
},
|
||||
stepSize: 1
|
||||
}
|
||||
|
||||
@@ -55,25 +55,24 @@ export class ClusterManager extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="ClusterManager">
|
||||
<div id="draggable-top"/>
|
||||
<main>
|
||||
<div id="lens-views"/>
|
||||
<div id="lens-views" />
|
||||
<Switch>
|
||||
<Route component={LandingPage} {...landingRoute}/>
|
||||
<Route component={Preferences} {...preferencesRoute}/>
|
||||
<Route component={Workspaces} {...workspacesRoute}/>
|
||||
<Route component={AddCluster} {...addClusterRoute}/>
|
||||
<Route component={ClusterView} {...clusterViewRoute}/>
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
||||
<Route component={LandingPage} {...landingRoute} />
|
||||
<Route component={Preferences} {...preferencesRoute} />
|
||||
<Route component={Workspaces} {...workspacesRoute} />
|
||||
<Route component={AddCluster} {...addClusterRoute} />
|
||||
<Route component={ClusterView} {...clusterViewRoute} />
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
||||
<Route component={Extensions} {...extensionsRoute}/>
|
||||
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
|
||||
return <Route key={path} path={path} component={Page}/>
|
||||
})}
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
<Redirect exact to={this.startUrl} />
|
||||
</Switch>
|
||||
</main>
|
||||
<ClustersMenu/>
|
||||
<BottomBar/>
|
||||
<ClustersMenu />
|
||||
<BottomBar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ export async function initView(clusterId: ClusterId) {
|
||||
if (!clusterId || lensViews.has(clusterId)) {
|
||||
return;
|
||||
}
|
||||
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
|
||||
const parentElem = document.getElementById("lens-views");
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.name = cluster.contextName;
|
||||
@@ -42,9 +45,9 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
|
||||
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
|
||||
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
|
||||
// Issue: https://github.com/lensapp/lens/issues/811
|
||||
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
|
||||
iframe.removeAttribute("src")
|
||||
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`
|
||||
iframe.removeAttribute("name")
|
||||
iframe.contentWindow.postMessage("teardown", "*")
|
||||
}
|
||||
|
||||
export function refreshViews() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { t } from "@lingui/macro";
|
||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||
import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { Notifications } from "../notifications";
|
||||
|
||||
export interface IChartInstallData {
|
||||
name: string;
|
||||
@@ -27,45 +28,50 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
});
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
if (!isInstallChartTab(selectedTab)) return;
|
||||
if (isOpen) {
|
||||
this.loadData();
|
||||
if (isInstallChartTab(selectedTab) && isOpen) {
|
||||
this.loadData()
|
||||
.catch(err => Notifications.error(String(err)))
|
||||
}
|
||||
}, { delay: 250 })
|
||||
}
|
||||
|
||||
@action
|
||||
async loadData(tabId = dockStore.selectedTabId) {
|
||||
const { values } = this.getData(tabId);
|
||||
const versions = this.versions.getData(tabId);
|
||||
return Promise.all([
|
||||
!versions && this.loadVersions(tabId),
|
||||
!values && this.loadValues(tabId),
|
||||
])
|
||||
const promises = []
|
||||
|
||||
if (!this.getData(tabId).values) {
|
||||
promises.push(this.loadValues(tabId))
|
||||
}
|
||||
|
||||
if (!this.versions.getData(tabId)) {
|
||||
promises.push(this.loadVersions(tabId))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@action
|
||||
async loadVersions(tabId: TabId) {
|
||||
const { repo, name } = this.getData(tabId);
|
||||
const { repo, name, version } = this.getData(tabId);
|
||||
this.versions.clearData(tabId); // reset
|
||||
const charts = await helmChartsApi.get(repo, name);
|
||||
const charts = await helmChartsApi.get(repo, name, version);
|
||||
const versions = charts.versions.map(chartVersion => chartVersion.version);
|
||||
this.versions.setData(tabId, versions);
|
||||
}
|
||||
|
||||
@action
|
||||
async loadValues(tabId: TabId) {
|
||||
const data = this.getData(tabId);
|
||||
const { repo, name, version } = data;
|
||||
let values = "";
|
||||
const fetchValues = async (retry = 1, maxRetries = 3) => {
|
||||
values = await helmChartsApi.getValues(repo, name, version);
|
||||
if (values || retry == maxRetries) return;
|
||||
await fetchValues(retry + 1);
|
||||
};
|
||||
this.setData(tabId, { ...data, values: undefined }); // reset
|
||||
await fetchValues();
|
||||
this.setData(tabId, { ...data, values });
|
||||
const data = this.getData(tabId)
|
||||
const { repo, name, version } = data
|
||||
|
||||
// This loop is for "retrying" the "getValues" call
|
||||
for (const _ of Array(3)) {
|
||||
const values = await helmChartsApi.getValues(repo, name, version)
|
||||
if (values) {
|
||||
this.setData(tabId, { ...data, values })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,15 +107,15 @@ export class InstallChart extends Component<Props> {
|
||||
|
||||
render() {
|
||||
const { tabId, chartData, values, versions, install } = this;
|
||||
if (!chartData || chartData.values === undefined || !versions) {
|
||||
return <Spinner center/>;
|
||||
if (chartData?.values === undefined || !versions) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
if (this.releaseDetails) {
|
||||
return (
|
||||
<div className="InstallChartDone flex column gaps align-center justify-center">
|
||||
<p>
|
||||
<Icon material="check" big sticker/>
|
||||
<Icon material="check" big sticker />
|
||||
</p>
|
||||
<p><Trans>Installation complete!</Trans></p>
|
||||
<div className="flex gaps align-center">
|
||||
@@ -144,7 +144,7 @@ export class InstallChart extends Component<Props> {
|
||||
const panelControls = (
|
||||
<div className="install-controls flex gaps align-center">
|
||||
<span><Trans>Chart</Trans></span>
|
||||
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)}/>
|
||||
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)} />
|
||||
<span><Trans>Version</Trans></span>
|
||||
<Select
|
||||
className="chart-version"
|
||||
@@ -191,4 +191,4 @@ export class InstallChart extends Component<Props> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,7 +282,6 @@ export class Input extends React.Component<InputProps, State> {
|
||||
onKeyDown: this.onKeyDown,
|
||||
rows: multiLine ? (rows || 1) : null,
|
||||
ref: this.bindRef,
|
||||
type: "text",
|
||||
spellCheck: "false",
|
||||
});
|
||||
|
||||
|
||||
@@ -116,6 +116,6 @@ export function openServiceAccountKubeConfig(account: ServiceAccount) {
|
||||
const namespace = account.getNs()
|
||||
KubeConfigDialog.open({
|
||||
title: <Trans>{accountName} kubeconfig</Trans>,
|
||||
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`)
|
||||
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${accountName}`)
|
||||
})
|
||||
}
|
||||
@@ -8,10 +8,16 @@ import { JsonApiErrorParsed } from "../../api/json-api";
|
||||
export type IMessageId = string | number;
|
||||
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
|
||||
|
||||
export enum NotificationStatus {
|
||||
OK = "ok",
|
||||
ERROR = "error",
|
||||
INFO = "info",
|
||||
}
|
||||
|
||||
export interface INotification {
|
||||
id?: IMessageId;
|
||||
message: IMessage;
|
||||
status?: "ok" | "error" | "info";
|
||||
status?: NotificationStatus;
|
||||
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react"
|
||||
import { JsonApiErrorParsed } from "../../api/json-api";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import { IMessage, INotification, notificationsStore } from "./notifications.store";
|
||||
import { IMessage, INotification, notificationsStore, NotificationStatus } from "./notifications.store";
|
||||
import { Animate } from "../animate";
|
||||
import { Icon } from "../icon"
|
||||
|
||||
@@ -17,7 +17,7 @@ export class Notifications extends React.Component {
|
||||
notificationsStore.add({
|
||||
message: message,
|
||||
timeout: 2500,
|
||||
status: "ok"
|
||||
status: NotificationStatus.OK
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ export class Notifications extends React.Component {
|
||||
notificationsStore.add({
|
||||
message: message,
|
||||
timeout: 10000,
|
||||
status: "error"
|
||||
status: NotificationStatus.ERROR
|
||||
});
|
||||
}
|
||||
|
||||
static info(message: IMessage, customOpts: Partial<INotification> = {}) {
|
||||
return notificationsStore.add({
|
||||
status: "info",
|
||||
status: NotificationStatus.INFO,
|
||||
timeout: 0,
|
||||
message: message,
|
||||
...customOpts,
|
||||
@@ -78,7 +78,7 @@ export class Notifications extends React.Component {
|
||||
onMouseLeave={() => addAutoHideTimer(notification)}
|
||||
onMouseEnter={() => removeAutoHideTimer(notification)}>
|
||||
<div className="box center">
|
||||
<Icon material="info_outline"/>
|
||||
<Icon material="info_outline" />
|
||||
</div>
|
||||
<div className="message box grow">{msgText}</div>
|
||||
<div className="box center">
|
||||
|
||||
@@ -32,6 +32,7 @@ html {
|
||||
background: transparent;
|
||||
min-height: 0;
|
||||
box-shadow: 0 0 0 1px $halfGray;
|
||||
cursor: pointer;
|
||||
|
||||
&--is-focused {
|
||||
box-shadow: 0 0 0 2px $primary;
|
||||
@@ -88,6 +89,7 @@ html {
|
||||
|
||||
&__option {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background: $primary;
|
||||
|
||||
@@ -19,6 +19,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
kubeWatchApi.addListener(this, this.onWatchApiEvent);
|
||||
}
|
||||
|
||||
getStatuses?(items: T[]): Record<string, number>;
|
||||
|
||||
getAllByNs(namespace: string | string[], strict = false): T[] {
|
||||
const namespaces: string[] = [].concat(namespace);
|
||||
if (namespaces.length) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { KubeResource } from "../../common/rbac";
|
||||
import { _i18n } from "../i18n";
|
||||
|
||||
export const ResourceNames: Record<KubeResource, string> = {
|
||||
"namespaces": _i18n._("Namespaces"),
|
||||
"nodes": _i18n._("Nodes"),
|
||||
"events": _i18n._("Events"),
|
||||
"resourcequotas": _i18n._("Resource Quotas"),
|
||||
"services": _i18n._("Services"),
|
||||
"secrets": _i18n._("Secrets"),
|
||||
"configmaps": _i18n._("Config Maps"),
|
||||
"ingresses": _i18n._("Ingresses"),
|
||||
"networkpolicies": _i18n._("Network Policies"),
|
||||
"persistentvolumes": _i18n._("Persistent Volumes"),
|
||||
"storageclasses": _i18n._("Storage Classes"),
|
||||
"pods": _i18n._("Pods"),
|
||||
"daemonsets": _i18n._("Daemon Sets"),
|
||||
"deployments": _i18n._("Deployments"),
|
||||
"statefulsets": _i18n._("Stateful Sets"),
|
||||
"replicasets": _i18n._("Replica Sets"),
|
||||
"jobs": _i18n._("Jobs"),
|
||||
"cronjobs": _i18n._("Cron Jobs"),
|
||||
"endpoints": _i18n._("Endpoints"),
|
||||
"customresourcedefinitions": _i18n._("Custom Resource Definitions"),
|
||||
"horizontalpodautoscalers": _i18n._("Horizontal Pod Autoscalers"),
|
||||
"podsecuritypolicies": _i18n._("Pod Security Policies"),
|
||||
"poddisruptionbudgets": _i18n._("Pod Disruption Budgets"),
|
||||
}
|
||||
Reference in new issue
Block a user