mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extensions-api
# Conflicts: # src/renderer/components/app.tsx # src/renderer/components/cluster-manager/clusters-menu.scss # src/renderer/components/cluster-manager/clusters-menu.tsx # src/renderer/components/layout/main-layout.tsx # src/renderer/components/layout/sidebar.tsx
This commit is contained in:
commit
91b4823ec6
12
build/installer.nsh
Normal file
12
build/installer.nsh
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
!macro customInit
|
||||||
|
; Workaround for installer handing when the app directory is removed manually
|
||||||
|
${ifNot} ${FileExists} "$INSTDIR"
|
||||||
|
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{${UNINSTALL_APP_KEY}}"
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
; Workaround for the old-format uninstall registry key (some people report it causes hangups, too)
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_APP_KEY}" "QuietUninstallString"
|
||||||
|
StrCmp $0 "" proceed 0
|
||||||
|
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_APP_KEY}"
|
||||||
|
proceed:
|
||||||
|
!macroend
|
||||||
@ -17,15 +17,12 @@ describe("app start", () => {
|
|||||||
const addMinikubeCluster = async (app: Application) => {
|
const addMinikubeCluster = async (app: Application) => {
|
||||||
await app.client.click("div.add-cluster")
|
await app.client.click("div.add-cluster")
|
||||||
await app.client.waitUntilTextExists("div", "Select kubeconfig file")
|
await app.client.waitUntilTextExists("div", "Select kubeconfig file")
|
||||||
await app.client.click("div.Select__control")
|
|
||||||
await app.client.waitUntilTextExists("div", "minikube")
|
|
||||||
await app.client.click("div.minikube")
|
|
||||||
await app.client.click("button.primary")
|
await app.client.click("button.primary")
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitForMinikubeDashboard = async (app: Application) => {
|
const waitForMinikubeDashboard = async (app: Application) => {
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started")
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started")
|
||||||
let windowCount = await app.client.getWindowCount()
|
await app.client.getWindowCount()
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`)
|
await app.client.waitForExist(`iframe[name="minikube"]`)
|
||||||
await app.client.frame("minikube")
|
await app.client.frame("minikube")
|
||||||
await app.client.waitUntilTextExists("span.link-text", "Cluster")
|
await app.client.waitUntilTextExists("span.link-text", "Cluster")
|
||||||
@ -35,10 +32,8 @@ describe("app start", () => {
|
|||||||
app = util.setup()
|
app = util.setup()
|
||||||
await app.start()
|
await app.start()
|
||||||
await app.client.waitUntilWindowLoaded()
|
await app.client.waitUntilWindowLoaded()
|
||||||
let windowCount = await app.client.getWindowCount()
|
// Wait for splash screen to be closed
|
||||||
while (windowCount > 1) { // Wait for splash screen to be closed
|
while (await app.client.getWindowCount() > 1);
|
||||||
windowCount = await app.client.getWindowCount()
|
|
||||||
}
|
|
||||||
await app.client.windowByIndex(0)
|
await app.client.windowByIndex(0)
|
||||||
await app.client.waitUntilWindowLoaded()
|
await app.client.waitUntilWindowLoaded()
|
||||||
}, 20000)
|
}, 20000)
|
||||||
@ -48,7 +43,7 @@ describe("app start", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('allows to add a cluster', async () => {
|
it('allows to add a cluster', async () => {
|
||||||
const status = spawnSync("minikube status", {shell: true})
|
const status = spawnSync("minikube status", { shell: true })
|
||||||
if (status.status !== 0) {
|
if (status.status !== 0) {
|
||||||
console.warn("minikube not running, skipping test")
|
console.warn("minikube not running, skipping test")
|
||||||
return
|
return
|
||||||
@ -61,7 +56,7 @@ describe("app start", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('allows to create a pod', async () => {
|
it('allows to create a pod', async () => {
|
||||||
const status = spawnSync("minikube status", {shell: true})
|
const status = spawnSync("minikube status", { shell: true })
|
||||||
if (status.status !== 0) {
|
if (status.status !== 0) {
|
||||||
console.warn("minikube not running, skipping test")
|
console.warn("minikube not running, skipping test")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "3.6.0-rc.2",
|
"version": "3.6.4",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -151,6 +151,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"nsis": {
|
||||||
|
"include": "build/installer.nsh"
|
||||||
|
},
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
@ -214,6 +217,7 @@
|
|||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"serializr": "^2.0.3",
|
"serializr": "^2.0.3",
|
||||||
"shell-env": "^3.0.0",
|
"shell-env": "^3.0.0",
|
||||||
|
"spdy": "^4.0.2",
|
||||||
"tar": "^6.0.2",
|
"tar": "^6.0.2",
|
||||||
"tcp-port-used": "^1.0.1",
|
"tcp-port-used": "^1.0.1",
|
||||||
"tempy": "^0.5.0",
|
"tempy": "^0.5.0",
|
||||||
@ -258,6 +262,7 @@
|
|||||||
"@types/request-promise-native": "^1.0.17",
|
"@types/request-promise-native": "^1.0.17",
|
||||||
"@types/semver": "^7.2.0",
|
"@types/semver": "^7.2.0",
|
||||||
"@types/shelljs": "^0.8.8",
|
"@types/shelljs": "^0.8.8",
|
||||||
|
"@types/spdy": "^3.4.4",
|
||||||
"@types/tcp-port-used": "^1.0.0",
|
"@types/tcp-port-used": "^1.0.0",
|
||||||
"@types/tempy": "^0.3.0",
|
"@types/tempy": "^0.3.0",
|
||||||
"@types/terser-webpack-plugin": "^3.0.0",
|
"@types/terser-webpack-plugin": "^3.0.0",
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
);
|
);
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
const callback = (event: IpcMainEvent, model: T) => {
|
const callback = (event: IpcMainEvent, model: T) => {
|
||||||
logger.debug(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||||
this.onSync(model);
|
this.onSync(model);
|
||||||
};
|
};
|
||||||
ipcMain.on(this.syncChannel, callback);
|
ipcMain.on(this.syncChannel, callback);
|
||||||
@ -85,7 +85,7 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
}
|
}
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
const callback = (event: IpcRendererEvent, model: T) => {
|
const callback = (event: IpcRendererEvent, model: T) => {
|
||||||
logger.debug(`[STORE]: SYNC ${this.name} from main`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||||
this.onSync(model);
|
this.onSync(model);
|
||||||
};
|
};
|
||||||
ipcRenderer.on(this.syncChannel, callback);
|
ipcRenderer.on(this.syncChannel, callback);
|
||||||
|
|||||||
@ -14,6 +14,14 @@ export const clusterIpc = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
refresh: createIpcChannel({
|
||||||
|
channel: "cluster:refresh",
|
||||||
|
handle: (clusterId: ClusterId) => {
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
if (cluster) return cluster.refresh();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
disconnect: createIpcChannel({
|
disconnect: createIpcChannel({
|
||||||
channel: "cluster:disconnect",
|
channel: "cluster:disconnect",
|
||||||
handle: (clusterId: ClusterId) => {
|
handle: (clusterId: ClusterId) => {
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
||||||
this.applyWithoutSync(() => {
|
this.applyWithoutSync(() => {
|
||||||
logger.debug(`[CLUSTER-STORE]: received push-state at ${location.host}`, model);
|
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host}`, model);
|
||||||
this.getById(model.id)?.updateModel(model);
|
this.getById(model.id)?.updateModel(model);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export function broadcastIpc({ channel, frameId, frameOnly, webContentId, filter
|
|||||||
}
|
}
|
||||||
views.forEach(webContent => {
|
views.forEach(webContent => {
|
||||||
const type = webContent.getType();
|
const type = webContent.getType();
|
||||||
logger.debug(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
||||||
if (!frameOnly) {
|
if (!frameOnly) {
|
||||||
webContent.send(channel, ...args);
|
webContent.send(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export class ClusterManager {
|
|||||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||||
const clusterId = req.url.split("/")[1]
|
const clusterId = req.url.split("/")[1]
|
||||||
const cluster = clusterStore.getById(clusterId)
|
cluster = clusterStore.getById(clusterId)
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
// we need to swap path prefix so that request is proxied to kube api
|
||||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
|
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
|
||||||
|
|||||||
@ -92,7 +92,7 @@ export class Cluster implements ClusterModel {
|
|||||||
async init(port: number) {
|
async init(port: number) {
|
||||||
try {
|
try {
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
|
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler, port);
|
||||||
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
|
logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
|
||||||
@ -111,12 +111,10 @@ export class Cluster implements ClusterModel {
|
|||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||||
const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s
|
const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s
|
||||||
const refreshEventsTimer = setInterval(() => this.online && this.refreshEvents(), 3000); // every 3s
|
|
||||||
|
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
reaction(this.getState, this.pushState),
|
reaction(this.getState, this.pushState),
|
||||||
() => clearInterval(refreshTimer),
|
() => clearInterval(refreshTimer),
|
||||||
() => clearInterval(refreshEventsTimer),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +133,13 @@ export class Cluster implements ClusterModel {
|
|||||||
if (this.disconnected || (!init && !this.accessible)) {
|
if (this.disconnected || (!init && !this.accessible)) {
|
||||||
await this.reconnect();
|
await this.reconnect();
|
||||||
}
|
}
|
||||||
await this.refresh();
|
await this.refreshConnectionStatus()
|
||||||
|
if (this.accessible) {
|
||||||
|
await this.refreshAllowedResources()
|
||||||
|
this.ready = true
|
||||||
|
this.kubeCtl = new Kubectl(this.version)
|
||||||
|
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
|
||||||
|
}
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,15 +165,14 @@ export class Cluster implements ClusterModel {
|
|||||||
@action
|
@action
|
||||||
async refresh() {
|
async refresh() {
|
||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.refreshConnectionStatus(); // refresh "version", "online", etc.
|
await this.whenInitialized;
|
||||||
|
await this.refreshConnectionStatus();
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.kubeCtl = new Kubectl(this.version)
|
|
||||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||||
const [features, isAdmin, nodesCount] = await Promise.all([
|
const [features, isAdmin, nodesCount] = await Promise.all([
|
||||||
getFeatures(this),
|
getFeatures(this),
|
||||||
this.isClusterAdmin(),
|
this.isClusterAdmin(),
|
||||||
this.getNodeCount(),
|
this.getNodeCount(),
|
||||||
this.kubeCtl.ensureKubectl()
|
|
||||||
]);
|
]);
|
||||||
this.features = features;
|
this.features = features;
|
||||||
this.isAdmin = isAdmin;
|
this.isAdmin = isAdmin;
|
||||||
@ -178,8 +181,8 @@ export class Cluster implements ClusterModel {
|
|||||||
this.refreshEvents(),
|
this.refreshEvents(),
|
||||||
this.refreshAllowedResources(),
|
this.refreshAllowedResources(),
|
||||||
]);
|
]);
|
||||||
this.ready = true
|
|
||||||
}
|
}
|
||||||
|
this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -396,7 +399,7 @@ export class Cluster implements ClusterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pushState = (state = this.getState()): ClusterState => {
|
pushState = (state = this.getState()): ClusterState => {
|
||||||
logger.debug(`[CLUSTER]: push-state`, state);
|
logger.silly(`[CLUSTER]: push-state`, state);
|
||||||
broadcastIpc({
|
broadcastIpc({
|
||||||
channel: "cluster:state",
|
channel: "cluster:state",
|
||||||
frameId: this.frameId,
|
frameId: this.frameId,
|
||||||
|
|||||||
@ -70,7 +70,8 @@ export class ContextHandler {
|
|||||||
|
|
||||||
async resolveAuthProxyUrl() {
|
async resolveAuthProxyUrl() {
|
||||||
const proxyPort = await this.ensurePort();
|
const proxyPort = await this.ensurePort();
|
||||||
return `http://127.0.0.1:${proxyPort}`;
|
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""
|
||||||
|
return `http://127.0.0.1:${proxyPort}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
|
async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
|
||||||
@ -88,7 +89,7 @@ export class ContextHandler {
|
|||||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||||
const proxyUrl = await this.resolveAuthProxyUrl();
|
const proxyUrl = await this.resolveAuthProxyUrl();
|
||||||
return {
|
return {
|
||||||
target: proxyUrl + this.clusterUrl.path,
|
target: proxyUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export class KubeconfigManager {
|
|||||||
protected configDir = app.getPath("temp")
|
protected configDir = app.getPath("temp")
|
||||||
protected tempFile: string;
|
protected tempFile: string;
|
||||||
|
|
||||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) {
|
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +28,10 @@ export class KubeconfigManager {
|
|||||||
return this.tempFile;
|
return this.tempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected resolveProxyUrl() {
|
||||||
|
return `http://127.0.0.1:${this.port}/${this.cluster.id}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
||||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||||
@ -42,7 +46,7 @@ export class KubeconfigManager {
|
|||||||
clusters: [
|
clusters: [
|
||||||
{
|
{
|
||||||
name: contextName,
|
name: contextName,
|
||||||
server: await contextHandler.resolveAuthProxyUrl(),
|
server: this.resolveProxyUrl(),
|
||||||
skipTLSVerify: undefined,
|
skipTLSVerify: undefined,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -62,7 +66,7 @@ export class KubeconfigManager {
|
|||||||
// write
|
// write
|
||||||
const configYaml = dumpConfigYaml(proxyConfig);
|
const configYaml = dumpConfigYaml(proxyConfig);
|
||||||
fs.ensureDir(path.dirname(tempFile));
|
fs.ensureDir(path.dirname(tempFile));
|
||||||
fs.writeFileSync(tempFile, configYaml);
|
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
|
||||||
this.tempFile = tempFile;
|
this.tempFile = tempFile;
|
||||||
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
||||||
return tempFile;
|
return tempFile;
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import net from "net";
|
import net from "net";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
|
import spdy from "spdy";
|
||||||
import httpProxy from "http-proxy";
|
import httpProxy from "http-proxy";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import * as WebSocket from "ws"
|
import * as WebSocket from "ws"
|
||||||
|
import { apiPrefix, apiKubePrefix } from "../common/vars"
|
||||||
import { openShell } from "./node-shell-session";
|
import { openShell } from "./node-shell-session";
|
||||||
import { Router } from "./router"
|
import { Router } from "./router"
|
||||||
import { ClusterManager } from "./cluster-manager"
|
import { ClusterManager } from "./cluster-manager"
|
||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
|
||||||
export class LensProxy {
|
export class LensProxy {
|
||||||
@ -40,37 +41,49 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected buildCustomProxy(): http.Server {
|
protected buildCustomProxy(): http.Server {
|
||||||
const proxy = this.createProxy();
|
const proxy = this.createProxy();
|
||||||
const customProxy = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
const spdyProxy = spdy.createServer({
|
||||||
this.handleRequest(proxy, req, res);
|
spdy: {
|
||||||
});
|
plain: true,
|
||||||
customProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
connection: {
|
||||||
|
autoSpdy31: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
|
this.handleRequest(proxy, req, res)
|
||||||
|
})
|
||||||
|
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||||
|
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||||
this.handleWsUpgrade(req, socket, head)
|
this.handleWsUpgrade(req, socket, head)
|
||||||
});
|
} else {
|
||||||
customProxy.on("error", (err) => {
|
if (req.headers.upgrade?.startsWith("SPDY")) {
|
||||||
|
this.handleSpdyProxy(proxy, req, socket, head)
|
||||||
|
} else {
|
||||||
|
socket.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
spdyProxy.on("error", (err) => {
|
||||||
logger.error("proxy error", err)
|
logger.error("proxy error", err)
|
||||||
});
|
})
|
||||||
return customProxy;
|
return spdyProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleSpdyProxy(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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createProxy(): httpProxy {
|
protected createProxy(): httpProxy {
|
||||||
const proxy = httpProxy.createProxyServer();
|
const proxy = httpProxy.createProxyServer();
|
||||||
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
||||||
if (req.method !== "GET") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (proxyRes.statusCode === 502) {
|
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
|
||||||
const proxyError = cluster?.contextHandler.proxyLastError;
|
|
||||||
if (proxyError) {
|
|
||||||
return res.writeHead(502).end(proxyError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const reqId = this.getRequestId(req);
|
|
||||||
if (this.retryCounters.has(reqId)) {
|
|
||||||
logger.debug(`Resetting proxy retry cache for url: ${reqId}`);
|
|
||||||
this.retryCounters.delete(reqId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
proxy.on("error", (error, req, res, target) => {
|
proxy.on("error", (error, req, res, target) => {
|
||||||
if (this.closed) {
|
if (this.closed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -2,14 +2,16 @@ import { app, remote } from "electron";
|
|||||||
import winston from "winston"
|
import winston from "winston"
|
||||||
import { isDebugging } from "../common/vars";
|
import { isDebugging } from "../common/vars";
|
||||||
|
|
||||||
|
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info"
|
||||||
|
|
||||||
const consoleOptions: winston.transports.ConsoleTransportOptions = {
|
const consoleOptions: winston.transports.ConsoleTransportOptions = {
|
||||||
handleExceptions: false,
|
handleExceptions: false,
|
||||||
level: isDebugging ? "debug" : "info",
|
level: logLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileOptions: winston.transports.FileTransportOptions = {
|
const fileOptions: winston.transports.FileTransportOptions = {
|
||||||
handleExceptions: false,
|
handleExceptions: false,
|
||||||
level: isDebugging ? "debug" : "info",
|
level: logLevel,
|
||||||
filename: "lens.log",
|
filename: "lens.log",
|
||||||
dirname: (app ?? remote?.app)?.getPath("logs"),
|
dirname: (app ?? remote?.app)?.getPath("logs"),
|
||||||
maxsize: 16 * 1024,
|
maxsize: 16 * 1024,
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export class ShellSession extends EventEmitter {
|
|||||||
public async open() {
|
public async open() {
|
||||||
this.kubectlBinDir = await this.kubectl.binDir()
|
this.kubectlBinDir = await this.kubectl.binDir()
|
||||||
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath
|
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath
|
||||||
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? await this.kubectl.binDir() : path.dirname(pathFromPreferences)
|
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
|
||||||
this.helmBinDir = helmCli.getBinaryDir()
|
this.helmBinDir = helmCli.getBinaryDir()
|
||||||
const env = await this.getCachedShellEnv()
|
const env = await this.getCachedShellEnv()
|
||||||
const shell = env.PTYSHELL
|
const shell = env.PTYSHELL
|
||||||
|
|||||||
@ -23,8 +23,8 @@ export class WindowManager {
|
|||||||
this.mainView = new BrowserWindow({
|
this.mainView = new BrowserWindow({
|
||||||
x, y, width, height,
|
x, y, width, height,
|
||||||
show: false,
|
show: false,
|
||||||
minWidth: 900,
|
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||||
minHeight: 760,
|
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hidden",
|
||||||
backgroundColor: "#1e2124",
|
backgroundColor: "#1e2124",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
|
|
||||||
export class NotFound extends React.Component {
|
export class NotFound extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MainLayout className="NotFound" contentClass="flex" footer={null}>
|
<TabLayout className="NotFound" contentClass="flex">
|
||||||
<p className="box center">
|
<p className="box center">
|
||||||
<Trans>Page not found</Trans>
|
<Trans>Page not found</Trans>
|
||||||
</p>
|
</p>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +80,6 @@ export class AddCluster extends React.Component {
|
|||||||
const contexts = this.getContexts(this.kubeConfigLocal);
|
const contexts = this.getContexts(this.kubeConfigLocal);
|
||||||
this.kubeContexts.replace(contexts);
|
this.kubeContexts.replace(contexts);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KubeConfigSourceTab.TEXT:
|
case KubeConfigSourceTab.TEXT:
|
||||||
try {
|
try {
|
||||||
this.error = ""
|
this.error = ""
|
||||||
@ -91,6 +90,10 @@ export class AddCluster extends React.Component {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.kubeContexts.size === 1) {
|
||||||
|
this.selectedContexts.push(this.kubeContexts.keys().next().value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getContexts(config: KubeConfig): Map<string, KubeConfig> {
|
getContexts(config: KubeConfig): Map<string, KubeConfig> {
|
||||||
@ -206,7 +209,7 @@ export class AddCluster extends React.Component {
|
|||||||
<Tab
|
<Tab
|
||||||
value={KubeConfigSourceTab.FILE}
|
value={KubeConfigSourceTab.FILE}
|
||||||
label={<Trans>Select kubeconfig file</Trans>}
|
label={<Trans>Select kubeconfig file</Trans>}
|
||||||
active={this.sourceTab == KubeConfigSourceTab.FILE}/>
|
active={this.sourceTab == KubeConfigSourceTab.FILE} />
|
||||||
<Tab
|
<Tab
|
||||||
value={KubeConfigSourceTab.TEXT}
|
value={KubeConfigSourceTab.TEXT}
|
||||||
label={<Trans>Paste as text</Trans>}
|
label={<Trans>Paste as text</Trans>}
|
||||||
@ -320,13 +323,15 @@ export class AddCluster extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className={cssNames("kube-context flex gaps align-center", context)}>
|
<div className={cssNames("kube-context flex gaps align-center", context)}>
|
||||||
<span>{context}</span>
|
<span>{context}</span>
|
||||||
{isNew && <Icon small material="fiber_new"/>}
|
{isNew && <Icon small material="fiber_new" />}
|
||||||
{isSelected && <Icon small material="check" className="box right"/>}
|
{isSelected && <Icon small material="check" className="box right" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const addDisabled = this.selectedContexts.length === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WizardLayout
|
<WizardLayout
|
||||||
className="AddCluster"
|
className="AddCluster"
|
||||||
@ -374,9 +379,12 @@ export class AddCluster extends React.Component {
|
|||||||
<div className="actions-panel">
|
<div className="actions-panel">
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
|
disabled={addDisabled}
|
||||||
label={<Trans>Add cluster(s)</Trans>}
|
label={<Trans>Add cluster(s)</Trans>}
|
||||||
onClick={this.addClusters}
|
onClick={this.addClusters}
|
||||||
waiting={this.isWaiting}
|
waiting={this.isWaiting}
|
||||||
|
tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
|
||||||
|
tooltipOverrideDisabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
|
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
|
||||||
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
|
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
@ -30,12 +30,12 @@ export class Apps extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const tabRoutes = Apps.tabRoutes;
|
const tabRoutes = Apps.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Apps" tabs={tabRoutes}>
|
<TabLayout className="Apps" tabs={tabRoutes}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||||
<Redirect to={tabRoutes[0].url}/>
|
<Redirect to={tabRoutes[0].url}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,38 @@
|
|||||||
import "./cluster-settings.scss";
|
import "./cluster-settings.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer, disposeOnUnmount } from "mobx-react";
|
||||||
import { Features } from "./features";
|
import { Features } from "./features";
|
||||||
import { Removal } from "./removal";
|
import { Removal } from "./removal";
|
||||||
import { Status } from "./status";
|
import { Status } from "./status";
|
||||||
import { General } from "./general";
|
import { General } from "./general";
|
||||||
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { WizardLayout } from "../layout/wizard-layout";
|
import { WizardLayout } from "../layout/wizard-layout";
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
import { ClusterIcon } from "../cluster-icon";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { getMatchedCluster } from "../cluster-manager/cluster-view.route";
|
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
|
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||||
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
|
import { RouteComponentProps } from "react-router";
|
||||||
|
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||||
|
import { autorun } from "mobx";
|
||||||
|
|
||||||
|
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterSettings extends React.Component {
|
export class ClusterSettings extends React.Component<Props> {
|
||||||
|
get cluster(): Cluster {
|
||||||
|
return clusterStore.getById(this.props.match.params.clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
window.addEventListener('keydown', this.onEscapeKey);
|
window.addEventListener('keydown', this.onEscapeKey);
|
||||||
|
disposeOnUnmount(this,
|
||||||
|
autorun(() => {
|
||||||
|
this.refreshCluster();
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -29,12 +46,18 @@ export class ClusterSettings extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshCluster = () => {
|
||||||
|
if(this.cluster) {
|
||||||
|
clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const cluster = getMatchedCluster();
|
const cluster = this.cluster
|
||||||
if (!cluster) return null;
|
if (!cluster) return null;
|
||||||
const header = (
|
const header = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import "./cluster.scss"
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, reaction } from "mobx";
|
import { computed, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { ClusterIssues } from "./cluster-issues";
|
import { ClusterIssues } from "./cluster-issues";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { cssNames, interval, isElectron } from "../../utils";
|
import { cssNames, interval, isElectron } from "../../utils";
|
||||||
@ -54,7 +54,7 @@ export class Cluster extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { isLoaded } = this;
|
const { isLoaded } = this;
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<TabLayout>
|
||||||
<div className="Cluster">
|
<div className="Cluster">
|
||||||
{!isLoaded && <Spinner center/>}
|
{!isLoaded && <Spinner center/>}
|
||||||
{isLoaded && (
|
{isLoaded && (
|
||||||
@ -65,7 +65,7 @@ export class Cluster extends React.Component {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
|
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
|
||||||
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
|
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
@ -68,12 +68,12 @@ export class Config extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const tabRoutes = Config.tabRoutes;
|
const tabRoutes = Config.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Config" tabs={tabRoutes}>
|
<TabLayout className="Config" tabs={tabRoutes}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||||
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
|
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { crdResourcesRoute, crdRoute, crdURL, crdDefinitionsRoute } from "./crd.route";
|
import { crdResourcesRoute, crdRoute, crdURL, crdDefinitionsRoute } from "./crd.route";
|
||||||
import { CrdList } from "./crd-list";
|
import { CrdList } from "./crd-list";
|
||||||
import { CrdResources } from "./crd-resources";
|
import { CrdResources } from "./crd-resources";
|
||||||
@ -25,13 +25,13 @@ export class CustomResources extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<TabLayout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={CrdList} {...crdDefinitionsRoute} exact/>
|
<Route component={CrdList} {...crdDefinitionsRoute} exact/>
|
||||||
<Route component={CrdResources} {...crdResourcesRoute}/>
|
<Route component={CrdResources} {...crdResourcesRoute}/>
|
||||||
<Redirect to={crdURL()}/>
|
<Redirect to={crdURL()}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@ import "./events.scss";
|
|||||||
|
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
@ -118,9 +118,9 @@ export class Events extends React.Component<Props> {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<TabLayout>
|
||||||
{events}
|
{events}
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
super.reset();
|
super.reset();
|
||||||
this.contextNs.clear();
|
this.contextNs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async remove(item: Namespace) {
|
||||||
|
await super.remove(item);
|
||||||
|
this.contextNs.remove(item.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const namespaceStore = new NamespaceStore();
|
export const namespaceStore = new NamespaceStore();
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { Namespace, namespacesApi, NamespaceStatus } from "../../api/endpoints";
|
import { Namespace, namespacesApi, NamespaceStatus } from "../../api/endpoints";
|
||||||
import { AddNamespaceDialog } from "./add-namespace-dialog";
|
import { AddNamespaceDialog } from "./add-namespace-dialog";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||||
@ -26,7 +26,7 @@ interface Props extends RouteComponentProps<INamespacesRouteParams> {
|
|||||||
export class Namespaces extends React.Component<Props> {
|
export class Namespaces extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
isClusterScoped
|
isClusterScoped
|
||||||
className="Namespaces" store={namespaceStore}
|
className="Namespaces" store={namespaceStore}
|
||||||
@ -65,7 +65,7 @@ export class Namespaces extends React.Component<Props> {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<AddNamespaceDialog/>
|
<AddNamespaceDialog/>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export class IngressDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
|
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
|
||||||
if (ingressPoints.length === 0) return null
|
if (!ingressPoints || ingressPoints.length === 0) return null
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Table className="ingress-points">
|
<Table className="ingress-points">
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { Services, servicesRoute, servicesURL } from "../+network-services";
|
import { Services, servicesRoute, servicesURL } from "../+network-services";
|
||||||
import { Endpoints, endpointRoute, endpointURL } from "../+network-endpoints";
|
import { Endpoints, endpointRoute, endpointURL } from "../+network-endpoints";
|
||||||
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
||||||
@ -60,12 +60,12 @@ export class Network extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const tabRoutes = Network.tabRoutes;
|
const tabRoutes = Network.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Network" tabs={tabRoutes}>
|
<TabLayout className="Network" tabs={tabRoutes}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||||
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/>
|
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
|||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { cssNames, interval } from "../../utils";
|
import { cssNames, interval } from "../../utils";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { nodesStore } from "./nodes.store";
|
import { nodesStore } from "./nodes.store";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectListLayout } from "../kube-object";
|
||||||
@ -123,7 +123,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Nodes"
|
className="Nodes"
|
||||||
store={nodesStore} isClusterScoped
|
store={nodesStore} isClusterScoped
|
||||||
@ -182,7 +182,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
return <NodeMenu object={item}/>
|
return <NodeMenu object={item}/>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
||||||
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
||||||
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
||||||
@ -52,12 +52,12 @@ export class Storage extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const tabRoutes = Storage.tabRoutes;
|
const tabRoutes = Storage.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Storage" tabs={tabRoutes}>
|
<TabLayout className="Storage" tabs={tabRoutes}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||||
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/>
|
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { getDetailsUrl } from "../../navigation";
|
|||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<ServiceAccount> {
|
interface Props extends KubeObjectDetailsProps<ServiceAccount> {
|
||||||
}
|
}
|
||||||
@ -22,21 +23,25 @@ interface Props extends KubeObjectDetailsProps<ServiceAccount> {
|
|||||||
@observer
|
@observer
|
||||||
export class ServiceAccountsDetails extends React.Component<Props> {
|
export class ServiceAccountsDetails extends React.Component<Props> {
|
||||||
@observable secrets: Secret[];
|
@observable secrets: Secret[];
|
||||||
|
@observable imagePullSecrets: Secret[];
|
||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
loadSecrets = autorun(async () => {
|
loadSecrets = autorun(async () => {
|
||||||
this.secrets = null;
|
this.secrets = null;
|
||||||
|
this.imagePullSecrets = null;
|
||||||
const { object: serviceAccount } = this.props;
|
const { object: serviceAccount } = this.props;
|
||||||
if (!serviceAccount) {
|
if (!serviceAccount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const namespace = serviceAccount.getNs();
|
const namespace = serviceAccount.getNs();
|
||||||
const secrets = serviceAccount.getSecrets().map(({ name }) => {
|
const secrets = serviceAccount.getSecrets().map(({ name }) => {
|
||||||
const secret = secretsStore.getByName(name, namespace);
|
return secretsStore.load({ name, namespace });
|
||||||
if (!secret) return secretsStore.load({ name, namespace });
|
|
||||||
return secret;
|
|
||||||
});
|
});
|
||||||
this.secrets = await Promise.all(secrets);
|
this.secrets = await Promise.all(secrets);
|
||||||
|
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async({ name }) => {
|
||||||
|
return secretsStore.load({ name, namespace }).catch(_err => { return this.generateDummySecretObject(name) });
|
||||||
|
});
|
||||||
|
this.imagePullSecrets = await Promise.all(imagePullSecrets)
|
||||||
})
|
})
|
||||||
|
|
||||||
renderSecrets() {
|
renderSecrets() {
|
||||||
@ -49,15 +54,46 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderImagePullSecrets() {
|
||||||
|
const { imagePullSecrets } = this;
|
||||||
|
if (!imagePullSecrets) {
|
||||||
|
return <Spinner center/>
|
||||||
|
}
|
||||||
|
return this.renderSecretLinks(imagePullSecrets)
|
||||||
|
}
|
||||||
|
|
||||||
renderSecretLinks(secrets: Secret[]) {
|
renderSecretLinks(secrets: Secret[]) {
|
||||||
return secrets.map(secret => {
|
return secrets.map((secret) => {
|
||||||
|
if (secret.getId() === null) {
|
||||||
|
return (
|
||||||
|
<div key={secret.getName()}>
|
||||||
|
{secret.getName()}
|
||||||
|
<Icon
|
||||||
|
small material="warning"
|
||||||
|
tooltip={<Trans>Secret is not found</Trans>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
||||||
{secret.getName()}
|
{secret.getName()}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
generateDummySecretObject(name: string) {
|
||||||
|
return new Secret({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Secret",
|
||||||
|
metadata: {
|
||||||
|
name: name,
|
||||||
|
uid: null,
|
||||||
|
selfLink: null,
|
||||||
|
resourceVersion: null
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -69,9 +105,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
|||||||
secret.getNs() == serviceAccount.getNs() &&
|
secret.getNs() == serviceAccount.getNs() &&
|
||||||
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`)
|
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`)
|
||||||
)
|
)
|
||||||
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(({ name }) =>
|
const imagePullSecrets = serviceAccount.getImagePullSecrets()
|
||||||
secretsStore.getByName(name, serviceAccount.getNs())
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<div className="ServiceAccountsDetails">
|
<div className="ServiceAccountsDetails">
|
||||||
<KubeObjectMeta object={serviceAccount}/>
|
<KubeObjectMeta object={serviceAccount}/>
|
||||||
@ -83,7 +117,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
{imagePullSecrets.length > 0 &&
|
{imagePullSecrets.length > 0 &&
|
||||||
<DrawerItem name={<Trans>ImagePullSecrets</Trans>} className="links">
|
<DrawerItem name={<Trans>ImagePullSecrets</Trans>} className="links">
|
||||||
{this.renderSecretLinks(imagePullSecrets)}
|
{this.renderImagePullSecrets()}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { Roles } from "../+user-management-roles";
|
import { Roles } from "../+user-management-roles";
|
||||||
import { RoleBindings } from "../+user-management-roles-bindings";
|
import { RoleBindings } from "../+user-management-roles-bindings";
|
||||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||||
@ -55,12 +55,12 @@ export class UserManagement extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const tabRoutes = UserManagement.tabRoutes;
|
const tabRoutes = UserManagement.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="UserManagement" tabs={tabRoutes}>
|
<TabLayout className="UserManagement" tabs={tabRoutes}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||||
<Redirect to={usersManagementURL({ query: namespaceStore.getContextParams() })}/>
|
<Redirect to={usersManagementURL({ query: namespaceStore.getContextParams() })}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,24 @@
|
|||||||
.WhatsNew {
|
.WhatsNew {
|
||||||
$spacing: $padding * 2;
|
$spacing: $padding * 2;
|
||||||
|
|
||||||
background: $mainBackground url(../../components/icon/crane.svg) no-repeat;
|
&::after {
|
||||||
|
content: "";
|
||||||
|
background: url(../../components/icon/crane.svg) no-repeat;
|
||||||
background-position: 0 35%;
|
background-position: 0 35%;
|
||||||
background-size: 85%;
|
background-size: 85%;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
|
opacity: .75;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
.theme-light & {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { MainLayout, TabRoute } from "../layout/main-layout";
|
import { TabLayout, TabRoute } from "../layout/tab-layout";
|
||||||
import { WorkloadsOverview } from "../+workloads-overview/overview";
|
import { WorkloadsOverview } from "../+workloads-overview/overview";
|
||||||
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL, workloadsURL } from "./workloads.route";
|
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL, workloadsURL } from "./workloads.route";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
@ -86,12 +86,12 @@ export class Workloads extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const tabRoutes = Workloads.tabRoutes;
|
const tabRoutes = Workloads.tabRoutes;
|
||||||
return (
|
return (
|
||||||
<MainLayout className="Workloads" tabs={tabRoutes}>
|
<TabLayout className="Workloads" tabs={tabRoutes}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||||
<Redirect to={workloadsURL({ query: namespaceStore.getContextParams() })}/>
|
<Redirect to={workloadsURL({ query: namespaceStore.getContextParams() })}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dial
|
|||||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||||
import { crdRoute } from "./+custom-resources";
|
import { crdRoute } from "./+custom-resources";
|
||||||
import { isAllowedResource } from "../../common/rbac";
|
import { isAllowedResource } from "../../common/rbac";
|
||||||
|
import { MainLayout } from "./layout/main-layout";
|
||||||
import { ErrorBoundary } from "./error-boundary";
|
import { ErrorBoundary } from "./error-boundary";
|
||||||
import { Terminal } from "./dock/terminal";
|
import { Terminal } from "./dock/terminal";
|
||||||
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
||||||
@ -60,6 +61,7 @@ export class App extends React.Component {
|
|||||||
<I18nProvider i18n={_i18n}>
|
<I18nProvider i18n={_i18n}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
<MainLayout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={Cluster} {...clusterRoute}/>
|
<Route component={Cluster} {...clusterRoute}/>
|
||||||
<Route component={Nodes} {...nodesRoute}/>
|
<Route component={Nodes} {...nodesRoute}/>
|
||||||
@ -77,7 +79,7 @@ export class App extends React.Component {
|
|||||||
})}
|
})}
|
||||||
<Redirect exact from="/" to={this.startURL}/>
|
<Redirect exact from="/" to={this.startURL}/>
|
||||||
<Route component={NotFound}/>
|
<Route component={NotFound}/>
|
||||||
</Switch>
|
</Switch></MainLayout>
|
||||||
<Notifications/>
|
<Notifications/>
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
<KubeObjectDetails/>
|
<KubeObjectDetails/>
|
||||||
|
|||||||
@ -13,11 +13,18 @@ interface Props extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
|||||||
export class Badge extends React.Component<Props> {
|
export class Badge extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { className, label, small, children, ...elemProps } = this.props;
|
const { className, label, small, children, ...elemProps } = this.props;
|
||||||
return (
|
return <>
|
||||||
<span className={cssNames("Badge", { small }, className)} {...elemProps}>
|
<span className={cssNames("Badge", { small }, className)} {...elemProps}>
|
||||||
{label}
|
{label}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
{ /**
|
||||||
|
* This is a zero-width-space. It makes there be a word seperation
|
||||||
|
* between adjacent Badge's because <span>'s are ignored for browers'
|
||||||
|
* word detection algorithmns use for determining the extent of the
|
||||||
|
* text to highlight on multi-click sequences.
|
||||||
|
*/}
|
||||||
|
​
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,8 +54,7 @@
|
|||||||
|
|
||||||
form:not([novalidate]):invalid &[type=submit]:not(.active),
|
form:not([novalidate]):invalid &[type=submit]:not(.active),
|
||||||
&:disabled {
|
&:disabled {
|
||||||
color: silver;
|
opacity: 50%;
|
||||||
background: $buttonDisabledBackground;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
if (this.cluster.disconnected) {
|
if (this.cluster.disconnected) {
|
||||||
await this.refreshCluster();
|
await this.activateCluster();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,13 +47,13 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
|
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCluster = async () => {
|
activateCluster = async () => {
|
||||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
|
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect = async () => {
|
reconnect = async () => {
|
||||||
this.isReconnecting = true;
|
this.isReconnecting = true;
|
||||||
await this.refreshCluster();
|
await this.activateCluster();
|
||||||
this.isReconnecting = false;
|
this.isReconnecting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { ipcRenderer } from "electron";
|
|||||||
import { matchPath, RouteProps } from "react-router";
|
import { matchPath, RouteProps } from "react-router";
|
||||||
import { buildURL, navigation } from "../../navigation";
|
import { buildURL, navigation } from "../../navigation";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route";
|
|
||||||
|
|
||||||
export interface IClusterViewRouteParams {
|
export interface IClusterViewRouteParams {
|
||||||
clusterId: string;
|
clusterId: string;
|
||||||
@ -19,10 +18,7 @@ export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute
|
|||||||
export function getMatchedClusterId(): string {
|
export function getMatchedClusterId(): string {
|
||||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||||
exact: true,
|
exact: true,
|
||||||
path: [
|
path: clusterViewRoute.path
|
||||||
clusterViewRoute.path,
|
|
||||||
clusterSettingsRoute.path,
|
|
||||||
].flat(),
|
|
||||||
})
|
})
|
||||||
if (matched) {
|
if (matched) {
|
||||||
return matched.params.clusterId;
|
return matched.params.clusterId;
|
||||||
|
|||||||
@ -49,12 +49,11 @@
|
|||||||
|
|
||||||
.Badge {
|
.Badge {
|
||||||
$boxSize: 17px;
|
$boxSize: 17px;
|
||||||
$offset: -7px;
|
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
transform: translateX(-50%) translateY(50%);
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
right: $offset;
|
|
||||||
bottom: $offset;
|
|
||||||
line-height: $boxSize;
|
line-height: $boxSize;
|
||||||
min-width: $boxSize;
|
min-width: $boxSize;
|
||||||
min-height: $boxSize;
|
min-height: $boxSize;
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
|
import type { Cluster } from "../../../main/cluster";
|
||||||
import "./clusters-menu.scss"
|
import "./clusters-menu.scss"
|
||||||
|
|
||||||
import { remote } from "electron"
|
import { remote } from "electron"
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
|
||||||
import { userStore } from "../../../common/user-store";
|
import { userStore } from "../../../common/user-store";
|
||||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
import { workspaceStore } from "../../../common/workspace-store";
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
import { ClusterIcon } from "../cluster-icon";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { autobind, cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName, autobind } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { addClusterURL } from "../+add-cluster";
|
import { addClusterURL } from "../+add-cluster";
|
||||||
@ -19,8 +20,8 @@ import { landingURL } from "../+landing-page";
|
|||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||||
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route";
|
import { clusterViewURL } from "./cluster-view.route";
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd";
|
||||||
import { dynamicPages } from "../../../extensions/register-page";
|
import { dynamicPages } from "../../../extensions/register-page";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -36,6 +37,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
|
|
||||||
addCluster = () => {
|
addCluster = () => {
|
||||||
navigate(addClusterURL());
|
navigate(addClusterURL());
|
||||||
|
clusterStore.setActive(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
showContextMenu = (cluster: Cluster) => {
|
showContextMenu = (cluster: Cluster) => {
|
||||||
@ -45,6 +47,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: _i18n._(t`Settings`),
|
label: _i18n._(t`Settings`),
|
||||||
click: () => {
|
click: () => {
|
||||||
|
clusterStore.setActive(cluster.id);
|
||||||
navigate(clusterSettingsURL({
|
navigate(clusterSettingsURL({
|
||||||
params: {
|
params: {
|
||||||
clusterId: cluster.id
|
clusterId: cluster.id
|
||||||
@ -58,6 +61,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
click: async () => {
|
click: async () => {
|
||||||
if (clusterStore.isActive(cluster.id)) {
|
if (clusterStore.isActive(cluster.id)) {
|
||||||
navigate(landingURL());
|
navigate(landingURL());
|
||||||
|
clusterStore.setActive(null);
|
||||||
}
|
}
|
||||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
||||||
}
|
}
|
||||||
@ -113,7 +117,9 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{clusters.map((cluster, index) => (
|
{clusters.map((cluster, index) => {
|
||||||
|
const isActive = cluster.id === clusterStore.activeClusterId;
|
||||||
|
return (
|
||||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
||||||
{(provided: DraggableProvided) => (
|
{(provided: DraggableProvided) => (
|
||||||
<div
|
<div
|
||||||
@ -125,14 +131,15 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
key={cluster.id}
|
key={cluster.id}
|
||||||
showErrors={true}
|
showErrors={true}
|
||||||
cluster={cluster}
|
cluster={cluster}
|
||||||
isActive={cluster.id === getMatchedClusterId()}
|
isActive={isActive}
|
||||||
onClick={() => this.showCluster(cluster.id)}
|
onClick={() => this.showCluster(cluster.id)}
|
||||||
onContextMenu={() => this.showContextMenu(cluster)}
|
onContextMenu={() => this.showContextMenu(cluster)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
))}
|
)}
|
||||||
|
)}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -143,9 +150,9 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
<Tooltip targetId="add-cluster-icon">
|
<Tooltip targetId="add-cluster-icon">
|
||||||
<Trans>Add Cluster</Trans>
|
<Trans>Add Cluster</Trans>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Icon big material="add" id="add-cluster-icon"/>
|
<Icon big material="add" id="add-cluster-icon" />
|
||||||
{newContexts.size > 0 && (
|
{newContexts.size > 0 && (
|
||||||
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
|
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="dynamic-pages">
|
<div className="dynamic-pages">
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export class EditorPanel extends React.Component<Props> {
|
|||||||
|
|
||||||
onResize = () => {
|
onResize = () => {
|
||||||
this.editor.resize();
|
this.editor.resize();
|
||||||
|
this.editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onCursorPosChange = (pos: Ace.Point) => {
|
onCursorPosChange = (pos: Ace.Point) => {
|
||||||
|
|||||||
@ -62,14 +62,13 @@ export class InfoPanel extends Component<Props> {
|
|||||||
this.error = "";
|
this.error = "";
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
try {
|
try {
|
||||||
this.result = await this.props.submit().finally(() => {
|
this.result = await this.props.submit()
|
||||||
this.waiting = false;
|
|
||||||
});
|
|
||||||
if (showNotifications) Notifications.ok(this.result);
|
if (showNotifications) Notifications.ok(this.result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error.toString();
|
this.error = error.toString();
|
||||||
if (showNotifications) Notifications.error(this.error);
|
if (showNotifications) Notifications.error(this.error);
|
||||||
throw error;
|
} finally {
|
||||||
|
this.waiting = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,12 +90,13 @@ export class InfoPanel extends Component<Props> {
|
|||||||
<>
|
<>
|
||||||
{result && (
|
{result && (
|
||||||
<div className="success flex align-center">
|
<div className="success flex align-center">
|
||||||
<Icon material="done"/> <span>{result}</span>
|
<Icon material="done" />
|
||||||
|
<span>{result}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{errorInfo && (
|
{errorInfo && (
|
||||||
<div className="error flex align-center">
|
<div className="error flex align-center">
|
||||||
<Icon material="error_outline"/>
|
<Icon material="error_outline" />
|
||||||
<span>{errorInfo}</span>
|
<span>{errorInfo}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -114,9 +114,9 @@ export class InfoPanel extends Component<Props> {
|
|||||||
{controls}
|
{controls}
|
||||||
</div>
|
</div>
|
||||||
<div className="info flex gaps align-center">
|
<div className="info flex gaps align-center">
|
||||||
{waiting ? <><Spinner/> {submittingMessage}</> : this.renderInfo()}
|
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderInfo()}
|
||||||
</div>
|
</div>
|
||||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close}/>
|
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
||||||
<Button
|
<Button
|
||||||
primary active
|
primary active
|
||||||
label={submitLabel}
|
label={submitLabel}
|
||||||
|
|||||||
@ -152,6 +152,7 @@ export class Terminal {
|
|||||||
onResize = () => {
|
onResize = () => {
|
||||||
if (!this.isActive) return;
|
if (!this.isActive) return;
|
||||||
this.fitLazy();
|
this.fitLazy();
|
||||||
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivate = () => {
|
onActivate = () => {
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export const isUrl: Validator = {
|
|||||||
|
|
||||||
export const isPath: Validator = {
|
export const isPath: Validator = {
|
||||||
condition: ({ type }) => type === "text",
|
condition: ({ type }) => type === "text",
|
||||||
message: () => _i18n._(t`This field must be a path to an existing file`),
|
message: () => _i18n._(t`This field must be a valid path`),
|
||||||
validate: value => !value || fse.pathExistsSync(value),
|
validate: value => !value || fse.pathExistsSync(value),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
|
|
||||||
.MainLayout {
|
.MainLayout {
|
||||||
--sidebar-max-size: 200px;
|
--sidebar-max-size: 200px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "aside header" "aside tabs" "aside main" "aside footer";
|
grid-template-areas:
|
||||||
|
"aside header"
|
||||||
|
"aside tabs"
|
||||||
|
"aside main"
|
||||||
|
"aside footer";
|
||||||
grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto;
|
grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto;
|
||||||
grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
|
grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
> .Tabs {
|
> header {
|
||||||
grid-area: tabs;
|
|
||||||
background: $layoutTabsBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
background: $layoutBackground;
|
background: $layoutBackground;
|
||||||
padding: $padding $padding * 2;
|
padding: $padding $padding * 2;
|
||||||
@ -28,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
> aside {
|
||||||
grid-area: aside;
|
grid-area: aside;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $sidebarBackground;
|
background: $sidebarBackground;
|
||||||
@ -48,25 +47,14 @@
|
|||||||
&.accessible:hover {
|
&.accessible:hover {
|
||||||
width: var(--sidebar-max-size);
|
width: var(--sidebar-max-size);
|
||||||
transition-delay: 750ms;
|
transition-delay: 750ms;
|
||||||
box-shadow: 3px 3px 16px rgba(0, 0, 0, .35);
|
box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35);
|
||||||
z-index: $zIndex-sidebar-hover;
|
z-index: $zIndex-sidebar-hover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
> main {
|
||||||
@include custom-scrollbar;
|
display: contents;
|
||||||
$spacing: $margin * 2;
|
|
||||||
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
grid-area: main;
|
|
||||||
overflow-y: scroll; // always reserve space for scrollbar (17px)
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: $spacing;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
|||||||
@ -3,26 +3,16 @@ import "./main-layout.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable, reaction } from "mobx";
|
import { observable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { matchPath, RouteProps } from "react-router-dom";
|
|
||||||
import { createStorage, cssNames } from "../../utils";
|
import { createStorage, cssNames } from "../../utils";
|
||||||
import { Tab, Tabs } from "../tabs";
|
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { ErrorBoundary } from "../error-boundary";
|
import { ErrorBoundary } from "../error-boundary";
|
||||||
import { Dock } from "../dock";
|
import { Dock } from "../dock";
|
||||||
import { navigate, navigation } from "../../navigation";
|
|
||||||
import { getHostedCluster } from "../../../common/cluster-store";
|
import { getHostedCluster } from "../../../common/cluster-store";
|
||||||
|
|
||||||
export interface TabRoute extends RouteProps {
|
|
||||||
title: React.ReactNode;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainLayoutProps {
|
export interface MainLayoutProps {
|
||||||
className?: any;
|
className?: any;
|
||||||
tabs?: TabRoute[];
|
|
||||||
footer?: React.ReactNode;
|
footer?: React.ReactNode;
|
||||||
headerClass?: string;
|
headerClass?: string;
|
||||||
contentClass?: string;
|
|
||||||
footerClass?: string;
|
footerClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,18 +25,17 @@ export class MainLayout extends React.Component<MainLayoutProps> {
|
|||||||
|
|
||||||
@disposeOnUnmount syncPinnedStateWithStorage = reaction(
|
@disposeOnUnmount syncPinnedStateWithStorage = reaction(
|
||||||
() => this.isPinned,
|
() => this.isPinned,
|
||||||
isPinned => this.storage.merge({ pinnedSidebar: isPinned })
|
(isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
|
||||||
);
|
);
|
||||||
|
|
||||||
toggleSidebar = () => {
|
toggleSidebar = () => {
|
||||||
this.isPinned = !this.isPinned;
|
this.isPinned = !this.isPinned;
|
||||||
this.isAccessible = false;
|
this.isAccessible = false;
|
||||||
setTimeout(() => this.isAccessible = true, 250);
|
setTimeout(() => (this.isAccessible = true), 250);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
|
const { className, headerClass, footer, footerClass, children } = this.props;
|
||||||
const routePath = navigation.location.pathname;
|
|
||||||
const cluster = getHostedCluster();
|
const cluster = getHostedCluster();
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
return null; // fix: skip render when removing active (visible) cluster
|
return null; // fix: skip render when removing active (visible) cluster
|
||||||
@ -54,37 +43,18 @@ export class MainLayout extends React.Component<MainLayoutProps> {
|
|||||||
return (
|
return (
|
||||||
<div className={cssNames("MainLayout", className)}>
|
<div className={cssNames("MainLayout", className)}>
|
||||||
<header className={cssNames("flex gaps align-center", headerClass)}>
|
<header className={cssNames("flex gaps align-center", headerClass)}>
|
||||||
<span className="cluster">
|
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
|
||||||
{cluster.preferences.clusterName || cluster.contextName}
|
|
||||||
</span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
||||||
<Sidebar
|
<Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} />
|
||||||
className="box grow"
|
|
||||||
isPinned={this.isPinned}
|
|
||||||
toggle={this.toggleSidebar}
|
|
||||||
/>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{tabs && (
|
<main>
|
||||||
<Tabs center onChange={url => navigate(url)}>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
{tabs.map(({ title, path, url, ...routeProps }) => {
|
|
||||||
const isActive = !!matchPath(routePath, { path, ...routeProps });
|
|
||||||
return <Tab key={url} label={title} value={url} active={isActive}/>
|
|
||||||
})}
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<main className={contentClass}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className={footerClass}>
|
<footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer>
|
||||||
{footer === undefined ? <Dock/> : footer}
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { TabRoute } from "./main-layout";
|
import type { TabRoute } from "./tab-layout";
|
||||||
import "./sidebar.scss";
|
import "./sidebar.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -44,21 +44,21 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class Sidebar extends React.Component<Props> {
|
export class Sidebar extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) {
|
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
|
||||||
crdStore.loadAll()
|
crdStore.loadAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCustomResources() {
|
renderCustomResources() {
|
||||||
return Object.entries(crdStore.groups).map(([group, crds]) => {
|
return Object.entries(crdStore.groups).map(([group, crds]) => {
|
||||||
const submenus = crds.map(crd => {
|
const submenus = crds.map((crd) => {
|
||||||
return {
|
return {
|
||||||
title: crd.getResourceKind(),
|
title: crd.getResourceKind(),
|
||||||
component: CrdList,
|
component: CrdList,
|
||||||
url: crd.getResourceUrl(),
|
url: crd.getResourceUrl(),
|
||||||
path: crdResourcesRoute.path,
|
path: crdResourcesRoute.path,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
return (
|
return (
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
key={group}
|
key={group}
|
||||||
@ -68,8 +68,8 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
subMenus={submenus}
|
subMenus={submenus}
|
||||||
text={group}
|
text={group}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -80,7 +80,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
|
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
|
||||||
<div className="header flex align-center">
|
<div className="header flex align-center">
|
||||||
<NavLink exact to="/" className="box grow">
|
<NavLink exact to="/" className="box grow">
|
||||||
<Icon svg="logo-full" className="logo-icon"/>
|
<Icon svg="logo-full" className="logo-icon" />
|
||||||
<div className="logo-text">Lens</div>
|
<div className="logo-text">Lens</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<Icon
|
<Icon
|
||||||
@ -94,17 +94,17 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
<div className="sidebar-nav flex column box grow-fixed">
|
<div className="sidebar-nav flex column box grow-fixed">
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="cluster"
|
id="cluster"
|
||||||
isHidden={!isAllowedResource('nodes')}
|
isHidden={!isAllowedResource("nodes")}
|
||||||
url={clusterURL()}
|
url={clusterURL()}
|
||||||
text={<Trans>Cluster</Trans>}
|
text={<Trans>Cluster</Trans>}
|
||||||
icon={<Icon svg="kube"/>}
|
icon={<Icon svg="kube" />}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="nodes"
|
id="nodes"
|
||||||
isHidden={!isAllowedResource('nodes')}
|
isHidden={!isAllowedResource("nodes")}
|
||||||
url={nodesURL()}
|
url={nodesURL()}
|
||||||
text={<Trans>Nodes</Trans>}
|
text={<Trans>Nodes</Trans>}
|
||||||
icon={<Icon svg="nodes"/>}
|
icon={<Icon svg="nodes" />}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="workloads"
|
id="workloads"
|
||||||
@ -113,7 +113,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
routePath={workloadsRoute.path}
|
routePath={workloadsRoute.path}
|
||||||
subMenus={Workloads.tabRoutes}
|
subMenus={Workloads.tabRoutes}
|
||||||
text={<Trans>Workloads</Trans>}
|
text={<Trans>Workloads</Trans>}
|
||||||
icon={<Icon svg="workloads"/>}
|
icon={<Icon svg="workloads" />}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="config"
|
id="config"
|
||||||
@ -122,7 +122,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
routePath={configRoute.path}
|
routePath={configRoute.path}
|
||||||
subMenus={Config.tabRoutes}
|
subMenus={Config.tabRoutes}
|
||||||
text={<Trans>Configuration</Trans>}
|
text={<Trans>Configuration</Trans>}
|
||||||
icon={<Icon material="list"/>}
|
icon={<Icon material="list" />}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="networks"
|
id="networks"
|
||||||
@ -131,7 +131,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
routePath={networkRoute.path}
|
routePath={networkRoute.path}
|
||||||
subMenus={Network.tabRoutes}
|
subMenus={Network.tabRoutes}
|
||||||
text={<Trans>Network</Trans>}
|
text={<Trans>Network</Trans>}
|
||||||
icon={<Icon material="device_hub"/>}
|
icon={<Icon material="device_hub" />}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="storage"
|
id="storage"
|
||||||
@ -139,22 +139,22 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
url={storageURL({ query })}
|
url={storageURL({ query })}
|
||||||
routePath={storageRoute.path}
|
routePath={storageRoute.path}
|
||||||
subMenus={Storage.tabRoutes}
|
subMenus={Storage.tabRoutes}
|
||||||
icon={<Icon svg="storage"/>}
|
icon={<Icon svg="storage" />}
|
||||||
text={<Trans>Storage</Trans>}
|
text={<Trans>Storage</Trans>}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="namespaces"
|
id="namespaces"
|
||||||
isHidden={!isAllowedResource('namespaces')}
|
isHidden={!isAllowedResource("namespaces")}
|
||||||
url={namespacesURL()}
|
url={namespacesURL()}
|
||||||
icon={<Icon material="layers"/>}
|
icon={<Icon material="layers" />}
|
||||||
text={<Trans>Namespaces</Trans>}
|
text={<Trans>Namespaces</Trans>}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="events"
|
id="events"
|
||||||
isHidden={!isAllowedResource('events')}
|
isHidden={!isAllowedResource("events")}
|
||||||
url={eventsURL({ query })}
|
url={eventsURL({ query })}
|
||||||
routePath={eventRoute.path}
|
routePath={eventRoute.path}
|
||||||
icon={<Icon material="access_time"/>}
|
icon={<Icon material="access_time" />}
|
||||||
text={<Trans>Events</Trans>}
|
text={<Trans>Events</Trans>}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
@ -162,7 +162,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
url={appsURL({ query })}
|
url={appsURL({ query })}
|
||||||
subMenus={Apps.tabRoutes}
|
subMenus={Apps.tabRoutes}
|
||||||
routePath={appsRoute.path}
|
routePath={appsRoute.path}
|
||||||
icon={<Icon material="apps"/>}
|
icon={<Icon material="apps" />}
|
||||||
text={<Trans>Apps</Trans>}
|
text={<Trans>Apps</Trans>}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
@ -170,16 +170,16 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
url={usersManagementURL({ query })}
|
url={usersManagementURL({ query })}
|
||||||
routePath={usersManagementRoute.path}
|
routePath={usersManagementRoute.path}
|
||||||
subMenus={UserManagement.tabRoutes}
|
subMenus={UserManagement.tabRoutes}
|
||||||
icon={<Icon material="security"/>}
|
icon={<Icon material="security" />}
|
||||||
text={<Trans>Access Control</Trans>}
|
text={<Trans>Access Control</Trans>}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
id="custom-resources"
|
id="custom-resources"
|
||||||
isHidden={!isAllowedResource('customresourcedefinitions')}
|
isHidden={!isAllowedResource("customresourcedefinitions")}
|
||||||
url={crdURL()}
|
url={crdURL()}
|
||||||
subMenus={CustomResources.tabRoutes}
|
subMenus={CustomResources.tabRoutes}
|
||||||
routePath={crdRoute.path}
|
routePath={crdRoute.path}
|
||||||
icon={<Icon material="extension"/>}
|
icon={<Icon material="extension" />}
|
||||||
text={<Trans>Custom Resources</Trans>}
|
text={<Trans>Custom Resources</Trans>}
|
||||||
>
|
>
|
||||||
{this.renderCustomResources()}
|
{this.renderCustomResources()}
|
||||||
@ -199,7 +199,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +216,10 @@ interface SidebarNavItemProps {
|
|||||||
|
|
||||||
const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []);
|
const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []);
|
||||||
const navItemState = observable.map<string, boolean>(navItemStorage.get());
|
const navItemState = observable.map<string, boolean>(navItemStorage.get());
|
||||||
reaction(() => [...navItemState], value => navItemStorage.set(value));
|
reaction(
|
||||||
|
() => [...navItemState],
|
||||||
|
(value) => navItemStorage.set(value)
|
||||||
|
);
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
||||||
@ -229,15 +232,15 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
|||||||
|
|
||||||
toggleSubMenu = () => {
|
toggleSubMenu = () => {
|
||||||
navItemState.set(this.props.id, !this.isExpanded);
|
navItemState.set(this.props.id, !this.isExpanded);
|
||||||
}
|
};
|
||||||
|
|
||||||
isActive = () => {
|
isActive = () => {
|
||||||
const { routePath, url } = this.props;
|
const { routePath, url } = this.props;
|
||||||
const { pathname } = navigation.location;
|
const { pathname } = navigation.location;
|
||||||
return !!matchPath(pathname, {
|
return !!matchPath(pathname, {
|
||||||
path: routePath || url
|
path: routePath || url,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props;
|
const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props;
|
||||||
@ -252,10 +255,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
|||||||
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
|
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
|
||||||
{icon}
|
{icon}
|
||||||
<span className="link-text">{text}</span>
|
<span className="link-text">{text}</span>
|
||||||
<Icon
|
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
|
||||||
className="expand-icon"
|
|
||||||
material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ul className={cssNames("sub-menu", { active: isActive })}>
|
<ul className={cssNames("sub-menu", { active: isActive })}>
|
||||||
{subMenus.map(({ title, url }) => (
|
{subMenus.map(({ title, url }) => (
|
||||||
@ -265,18 +265,18 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
|||||||
))}
|
))}
|
||||||
{React.Children.toArray(children).map((child: React.ReactElement<any>) => {
|
{React.Children.toArray(children).map((child: React.ReactElement<any>) => {
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
className: cssNames(child.props.className, { visible: this.isExpanded })
|
className: cssNames(child.props.className, { visible: this.isExpanded }),
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={this.isActive}>
|
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={this.isActive}>
|
||||||
{icon}
|
{icon}
|
||||||
<span className="link-text">{text}</span>
|
<span className="link-text">{text}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/renderer/components/layout/tab-layout.scss
Executable file
25
src/renderer/components/layout/tab-layout.scss
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
.TabLayout {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
> .Tabs {
|
||||||
|
grid-area: tabs;
|
||||||
|
background: $layoutTabsBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main {
|
||||||
|
@include custom-scrollbar;
|
||||||
|
$spacing: $margin * 2;
|
||||||
|
|
||||||
|
.theme-light & {
|
||||||
|
@include custom-scrollbar(dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
grid-area: main;
|
||||||
|
overflow-y: scroll; // always reserve space for scrollbar (17px)
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: $spacing;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/renderer/components/layout/tab-layout.tsx
Normal file
45
src/renderer/components/layout/tab-layout.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import "./tab-layout.scss";
|
||||||
|
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { matchPath, RouteProps } from "react-router-dom";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { Tab, Tabs } from "../tabs";
|
||||||
|
import { ErrorBoundary } from "../error-boundary";
|
||||||
|
import { navigate, navigation } from "../../navigation";
|
||||||
|
import { getHostedCluster } from "../../../common/cluster-store";
|
||||||
|
|
||||||
|
export interface TabRoute extends RouteProps {
|
||||||
|
title: React.ReactNode;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: any;
|
||||||
|
tabs?: TabRoute[];
|
||||||
|
contentClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabLayout = observer(({ className, contentClass, tabs, children }: Props) => {
|
||||||
|
const routePath = navigation.location.pathname;
|
||||||
|
const cluster = getHostedCluster();
|
||||||
|
if (!cluster) {
|
||||||
|
return null; // fix: skip render when removing active (visible) cluster
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssNames("TabLayout", className)}>
|
||||||
|
{tabs && (
|
||||||
|
<Tabs center onChange={(url) => navigate(url)}>
|
||||||
|
{tabs.map(({ title, path, url, ...routeProps }) => {
|
||||||
|
const isActive = !!matchPath(routePath, { path, ...routeProps });
|
||||||
|
return <Tab key={url} label={title} value={url} active={isActive} />;
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
<main className={contentClass}>
|
||||||
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -3,10 +3,11 @@
|
|||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
padding: $padding * 2;
|
padding: $padding * 2;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
z-index: 100000;
|
z-index: 100000;
|
||||||
|
height: min-content!important;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export enum TooltipPosition {
|
|||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
targetId: string; // html-id of target element to bind for
|
targetId: string; // html-id of target element to bind for
|
||||||
|
tooltipOnParentHover?: boolean; // detect hover on parent of target
|
||||||
visible?: boolean; // initial visibility
|
visible?: boolean; // initial visibility
|
||||||
offset?: number; // offset from target element in pixels (all sides)
|
offset?: number; // offset from target element in pixels (all sides)
|
||||||
usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true
|
usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true
|
||||||
@ -50,14 +51,22 @@ export class Tooltip extends React.Component<TooltipProps> {
|
|||||||
return document.getElementById(this.props.targetId)
|
return document.getElementById(this.props.targetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hoverTarget(): HTMLElement {
|
||||||
|
if (this.props.tooltipOnParentHover) {
|
||||||
|
return this.targetElem.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.targetElem
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.targetElem.addEventListener("mouseenter", this.onEnterTarget)
|
this.hoverTarget.addEventListener("mouseenter", this.onEnterTarget)
|
||||||
this.targetElem.addEventListener("mouseleave", this.onLeaveTarget)
|
this.hoverTarget.addEventListener("mouseleave", this.onLeaveTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.targetElem.removeEventListener("mouseenter", this.onEnterTarget)
|
this.hoverTarget.removeEventListener("mouseenter", this.onEnterTarget)
|
||||||
this.targetElem.removeEventListener("mouseleave", this.onLeaveTarget)
|
this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
|
|||||||
@ -8,6 +8,11 @@ import uniqueId from "lodash/uniqueId"
|
|||||||
|
|
||||||
export interface TooltipDecoratorProps {
|
export interface TooltipDecoratorProps {
|
||||||
tooltip?: ReactNode | Omit<TooltipProps, "targetId">;
|
tooltip?: ReactNode | Omit<TooltipProps, "targetId">;
|
||||||
|
/**
|
||||||
|
* forces tooltip to detect the target's parent for mouse events. This is
|
||||||
|
* useful for displaying tooltips even when the target is "disabled"
|
||||||
|
*/
|
||||||
|
tooltipOverrideDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
||||||
@ -17,22 +22,25 @@ export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
|||||||
protected tooltipId = uniqueId("tooltip_target_");
|
protected tooltipId = uniqueId("tooltip_target_");
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tooltip, ...targetProps } = this.props;
|
const { tooltip, tooltipOverrideDisabled, ...targetProps } = this.props;
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
const tooltipId = targetProps.id || this.tooltipId;
|
const tooltipId = targetProps.id || this.tooltipId;
|
||||||
const tooltipProps: TooltipProps = {
|
const tooltipProps: TooltipProps = {
|
||||||
targetId: tooltipId,
|
targetId: tooltipId,
|
||||||
|
tooltipOnParentHover: tooltipOverrideDisabled,
|
||||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||||
};
|
};
|
||||||
targetProps.id = tooltipId;
|
targetProps.id = tooltipId;
|
||||||
targetProps.children = (
|
targetProps.children = (
|
||||||
<>
|
<>
|
||||||
|
<div>
|
||||||
{targetProps.children}
|
{targetProps.children}
|
||||||
<Tooltip {...tooltipProps}/>
|
</div>
|
||||||
|
<Tooltip {...tooltipProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <Target {...targetProps as any}/>;
|
return <Target {...targetProps as any} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,37 +2,47 @@
|
|||||||
|
|
||||||
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
||||||
|
|
||||||
## 3.6.0-rc.2 (current version)
|
## 3.6.4 (current version)
|
||||||
- Refresh input values on cluster change
|
- Fix: deleted namespace does not get auto unselected
|
||||||
- Update logo
|
- Get focus to dock tab (terminal & resource editor) content after resize
|
||||||
- Fix margins in cluster menu
|
- Downloading kubectl binary does not block dashboard opening anymore
|
||||||
|
- Fix background image of What's New page on white theme
|
||||||
|
|
||||||
## 3.6.0-rc.1
|
## 3.6.3
|
||||||
|
- Fix app crash on certain situations when opening ingress details
|
||||||
|
- Reduce app minimum size to support >= 800 x 600 resolution displays
|
||||||
|
- Fix app crash when service account has imagePullSecrets defined but the actual secret is missing
|
||||||
|
- Fix words in labels to be selectable either by hovering or double-clicking
|
||||||
|
|
||||||
|
**Known issues**
|
||||||
|
|
||||||
|
- Kubectl exec command does not work in terminal against clusters that are behind a load balancer and require Host header in request, for example Rancher clusters.
|
||||||
|
|
||||||
|
## 3.6.2
|
||||||
|
- Fix terminal connection opening
|
||||||
|
|
||||||
|
**Known issues**
|
||||||
|
|
||||||
|
- Kubectl exec command does not work in terminal against clusters that are behind a load balancer and require Host header in request, for example Rancher clusters.
|
||||||
|
|
||||||
|
## 3.6.1
|
||||||
|
- Inject Host header to k8s client requests
|
||||||
|
- Remove extra refreshEvents polling
|
||||||
|
- Fix windows installer when app directory removed manually
|
||||||
|
|
||||||
|
**Known issues**
|
||||||
|
|
||||||
|
- Kubectl exec command does not work in terminal against clusters that are behind a load balancer and require Host header in request, for example Rancher clusters.
|
||||||
|
|
||||||
|
## 3.6.0
|
||||||
- Allow user to configure directory where Kubectl binaries are downloaded
|
- Allow user to configure directory where Kubectl binaries are downloaded
|
||||||
- Allow user to configure path to Kubectl binary, instead of using bundled Kubectl
|
- Allow user to configure path to Kubectl binary, instead of using bundled Kubectl
|
||||||
- Log application logs also to log file
|
- Allow user to select Kubeconfig from filesystem
|
||||||
- Restrict file permissions to only the user for pasted kubeconfigs
|
- Show the path of the cluster's Kubeconfig in cluster settings
|
||||||
- Close Preferences and Cluster Setting on Esc keypress
|
- Store reference to added Kubeconfig files
|
||||||
|
- Update logo
|
||||||
- Update Kubectl versions used with Lens
|
- Update Kubectl versions used with Lens
|
||||||
- Update Helm binary version
|
- Update Helm binary version
|
||||||
- Fix: Update CRD api to use preferred version and implement v1 differences
|
|
||||||
- Fix: Allow to drag and drop cluster icons
|
|
||||||
- Fix: Wider version select box for Helm chart installation
|
|
||||||
- Fix: Reload only active dashboard view, not the whole app window
|
|
||||||
- Fix cluster icon margins
|
|
||||||
- Fix: Reconnect non-accessible clusters on reconnect
|
|
||||||
- Fix: Bundle Kubectl and Helm binaries
|
|
||||||
- Fix: Remove double copyright
|
|
||||||
|
|
||||||
## 3.6.0-beta.2
|
|
||||||
- Fix: too narrow sidebar without clusters
|
|
||||||
- Fix app crash when iterating Events without 'kind' property defined
|
|
||||||
- Detect non-functional bundled kubectl
|
|
||||||
|
|
||||||
## 3.6.0-beta.1
|
|
||||||
- Allow user to select Kubeconfig from filesystem
|
|
||||||
- Store reference to added Kubeconfig files
|
|
||||||
- Show the path of the cluster's Kubeconfig in cluster settings
|
|
||||||
- Add support for PodDisruptionBudgets
|
- Add support for PodDisruptionBudgets
|
||||||
- Add port-forwarding for containers in pod
|
- Add port-forwarding for containers in pod
|
||||||
- Add shortcut keys to menu items
|
- Add shortcut keys to menu items
|
||||||
@ -42,6 +52,30 @@ Here you can find description of changes we've built into each release. While we
|
|||||||
- Allow to trigger cronjobs
|
- Allow to trigger cronjobs
|
||||||
- Show devtools in menu
|
- Show devtools in menu
|
||||||
- Open last active cluster as default
|
- Open last active cluster as default
|
||||||
|
- Log application logs also to log file
|
||||||
|
- Fix Dialog Esc keypress behavior
|
||||||
|
- Set new workspace name restrictions
|
||||||
|
- Fix cluster's apiUrl
|
||||||
|
- Fix: Cluster dashboard not rendered
|
||||||
|
- Fix app reload in cluster settings
|
||||||
|
- Fix proxy kubeconfig file permissions
|
||||||
|
- Move verbose log lines to silly level
|
||||||
|
- Add path to auth proxy url if present in cluster url
|
||||||
|
- Fix path validation message
|
||||||
|
- Fix: Refresh input values on cluster change
|
||||||
|
- Fix margins in cluster menu
|
||||||
|
- Restrict file permissions to only the user for pasted kubeconfigs
|
||||||
|
- Close Preferences and Cluster Setting on Esc keypress
|
||||||
|
- Fix: Update CRD api to use preferred version and implement v1 differences
|
||||||
|
- Fix: Allow to drag and drop cluster icons
|
||||||
|
- Fix: Wider version select box for Helm chart installation
|
||||||
|
- Fix: Reload only active dashboard view, not the whole app window
|
||||||
|
- Fix cluster icon margins
|
||||||
|
- Fix: Reconnect non-accessible clusters on reconnect
|
||||||
|
- Fix: Remove double copyright
|
||||||
|
- Fix: too narrow sidebar without clusters
|
||||||
|
- Fix app crash when iterating Events without 'kind' property defined
|
||||||
|
- Detect non-functional bundled kubectl
|
||||||
- Fix format duration rounding days error
|
- Fix format duration rounding days error
|
||||||
- Handle unsupported resources properly after they've been created from editor
|
- Handle unsupported resources properly after they've been created from editor
|
||||||
- Fix CRD api parsing
|
- Fix CRD api parsing
|
||||||
@ -62,7 +96,7 @@ Here you can find description of changes we've built into each release. While we
|
|||||||
## 3.5.2
|
## 3.5.2
|
||||||
- Fix application not opening properly in some cases by catching and logging error from shell sync.
|
- Fix application not opening properly in some cases by catching and logging error from shell sync.
|
||||||
|
|
||||||
## 3.5.1 (current version)
|
## 3.5.1
|
||||||
- Fix kubernetes api requests to work with non-"namespaces" pathnames
|
- Fix kubernetes api requests to work with non-"namespaces" pathnames
|
||||||
- Fix: Handle invalid metrics responses properly
|
- Fix: Handle invalid metrics responses properly
|
||||||
- Fix: Display namespace defined in kubeconfig always in the namespace selector
|
- Fix: Display namespace defined in kubeconfig always in the namespace selector
|
||||||
|
|||||||
69
yarn.lock
69
yarn.lock
@ -2161,6 +2161,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
|
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
|
||||||
integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
|
integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
|
||||||
|
|
||||||
|
"@types/spdy@^3.4.4":
|
||||||
|
version "3.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/spdy/-/spdy-3.4.4.tgz#3282fd4ad8c4603aa49f7017dd520a08a345b2bc"
|
||||||
|
integrity sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/stack-utils@^1.0.1":
|
"@types/stack-utils@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
@ -5927,6 +5934,11 @@ growly@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
||||||
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
|
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
|
||||||
|
|
||||||
|
handle-thing@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||||
|
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
|
||||||
|
|
||||||
handlebars@^4.7.6:
|
handlebars@^4.7.6:
|
||||||
version "4.7.6"
|
version "4.7.6"
|
||||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
|
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
|
||||||
@ -6113,6 +6125,16 @@ hosted-git-info@^3.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^5.1.1"
|
lru-cache "^5.1.1"
|
||||||
|
|
||||||
|
hpack.js@^2.1.6:
|
||||||
|
version "2.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
|
||||||
|
integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
|
||||||
|
dependencies:
|
||||||
|
inherits "^2.0.1"
|
||||||
|
obuf "^1.0.0"
|
||||||
|
readable-stream "^2.0.1"
|
||||||
|
wbuf "^1.1.0"
|
||||||
|
|
||||||
html-encoding-sniffer@^2.0.1:
|
html-encoding-sniffer@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
|
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
|
||||||
@ -6175,6 +6197,11 @@ http-cache-semantics@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||||
|
|
||||||
|
http-deceiver@^1.2.7:
|
||||||
|
version "1.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
|
||||||
|
integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
|
||||||
|
|
||||||
http-proxy@^1.18.1:
|
http-proxy@^1.18.1:
|
||||||
version "1.18.1"
|
version "1.18.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||||
@ -8646,6 +8673,11 @@ object.pick@^1.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isobject "^3.0.1"
|
isobject "^3.0.1"
|
||||||
|
|
||||||
|
obuf@^1.0.0, obuf@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||||
|
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
||||||
|
|
||||||
oidc-token-hash@^3.0.1:
|
oidc-token-hash@^3.0.1:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-3.0.2.tgz#5bd4716cc48ad433f4e4e99276811019b165697e"
|
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-3.0.2.tgz#5bd4716cc48ad433f4e4e99276811019b165697e"
|
||||||
@ -9763,7 +9795,7 @@ read-pkg@^5.2.0:
|
|||||||
string_decoder "~1.1.1"
|
string_decoder "~1.1.1"
|
||||||
util-deprecate "~1.0.1"
|
util-deprecate "~1.0.1"
|
||||||
|
|
||||||
readable-stream@^3.1.1, readable-stream@^3.6.0:
|
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||||
@ -10294,6 +10326,11 @@ scss-tokenizer@^0.2.3:
|
|||||||
js-base64 "^2.1.8"
|
js-base64 "^2.1.8"
|
||||||
source-map "^0.4.2"
|
source-map "^0.4.2"
|
||||||
|
|
||||||
|
select-hose@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||||
|
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
|
||||||
|
|
||||||
semver-compare@^1.0.0:
|
semver-compare@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||||
@ -10603,6 +10640,29 @@ spdx-license-ids@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
|
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
|
||||||
integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
|
integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
|
||||||
|
|
||||||
|
spdy-transport@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
|
||||||
|
integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
|
||||||
|
dependencies:
|
||||||
|
debug "^4.1.0"
|
||||||
|
detect-node "^2.0.4"
|
||||||
|
hpack.js "^2.1.6"
|
||||||
|
obuf "^1.1.2"
|
||||||
|
readable-stream "^3.0.6"
|
||||||
|
wbuf "^1.7.3"
|
||||||
|
|
||||||
|
spdy@^4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b"
|
||||||
|
integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==
|
||||||
|
dependencies:
|
||||||
|
debug "^4.1.0"
|
||||||
|
handle-thing "^2.0.0"
|
||||||
|
http-deceiver "^1.2.7"
|
||||||
|
select-hose "^2.0.0"
|
||||||
|
spdy-transport "^3.0.0"
|
||||||
|
|
||||||
spectron@11.0.0:
|
spectron@11.0.0:
|
||||||
version "11.0.0"
|
version "11.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/spectron/-/spectron-11.0.0.tgz#79d785e6b8898638e77b5186711e3910ed4ca09b"
|
resolved "https://registry.yarnpkg.com/spectron/-/spectron-11.0.0.tgz#79d785e6b8898638e77b5186711e3910ed4ca09b"
|
||||||
@ -11793,6 +11853,13 @@ watchpack@^1.6.1:
|
|||||||
chokidar "^3.4.0"
|
chokidar "^3.4.0"
|
||||||
watchpack-chokidar2 "^2.0.0"
|
watchpack-chokidar2 "^2.0.0"
|
||||||
|
|
||||||
|
wbuf@^1.1.0, wbuf@^1.7.3:
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
|
||||||
|
integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
|
||||||
|
dependencies:
|
||||||
|
minimalistic-assert "^1.0.0"
|
||||||
|
|
||||||
wcwidth@^1.0.1:
|
wcwidth@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user