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) => {
|
||||
await app.client.click("div.add-cluster")
|
||||
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")
|
||||
}
|
||||
|
||||
const waitForMinikubeDashboard = async (app: Application) => {
|
||||
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.frame("minikube")
|
||||
await app.client.waitUntilTextExists("span.link-text", "Cluster")
|
||||
@ -35,10 +32,8 @@ describe("app start", () => {
|
||||
app = util.setup()
|
||||
await app.start()
|
||||
await app.client.waitUntilWindowLoaded()
|
||||
let windowCount = await app.client.getWindowCount()
|
||||
while (windowCount > 1) { // Wait for splash screen to be closed
|
||||
windowCount = await app.client.getWindowCount()
|
||||
}
|
||||
// Wait for splash screen to be closed
|
||||
while (await app.client.getWindowCount() > 1);
|
||||
await app.client.windowByIndex(0)
|
||||
await app.client.waitUntilWindowLoaded()
|
||||
}, 20000)
|
||||
@ -48,7 +43,7 @@ describe("app start", () => {
|
||||
})
|
||||
|
||||
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) {
|
||||
console.warn("minikube not running, skipping test")
|
||||
return
|
||||
@ -61,7 +56,7 @@ describe("app start", () => {
|
||||
})
|
||||
|
||||
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) {
|
||||
console.warn("minikube not running, skipping test")
|
||||
return
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "kontena-lens",
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "3.6.0-rc.2",
|
||||
"version": "3.6.4",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"license": "MIT",
|
||||
@ -151,6 +151,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"include": "build/installer.nsh"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
@ -214,6 +217,7 @@
|
||||
"semver": "^7.3.2",
|
||||
"serializr": "^2.0.3",
|
||||
"shell-env": "^3.0.0",
|
||||
"spdy": "^4.0.2",
|
||||
"tar": "^6.0.2",
|
||||
"tcp-port-used": "^1.0.1",
|
||||
"tempy": "^0.5.0",
|
||||
@ -258,6 +262,7 @@
|
||||
"@types/request-promise-native": "^1.0.17",
|
||||
"@types/semver": "^7.2.0",
|
||||
"@types/shelljs": "^0.8.8",
|
||||
"@types/spdy": "^3.4.4",
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/terser-webpack-plugin": "^3.0.0",
|
||||
|
||||
@ -77,7 +77,7 @@ export class BaseStore<T = any> extends Singleton {
|
||||
);
|
||||
if (ipcMain) {
|
||||
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);
|
||||
};
|
||||
ipcMain.on(this.syncChannel, callback);
|
||||
@ -85,7 +85,7 @@ export class BaseStore<T = any> extends Singleton {
|
||||
}
|
||||
if (ipcRenderer) {
|
||||
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);
|
||||
};
|
||||
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({
|
||||
channel: "cluster:disconnect",
|
||||
handle: (clusterId: ClusterId) => {
|
||||
|
||||
@ -76,7 +76,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
||||
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);
|
||||
})
|
||||
})
|
||||
|
||||
@ -68,7 +68,7 @@ export function broadcastIpc({ channel, frameId, frameOnly, webContentId, filter
|
||||
}
|
||||
views.forEach(webContent => {
|
||||
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) {
|
||||
webContent.send(channel, ...args);
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export class ClusterManager {
|
||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||
const clusterId = req.url.split("/")[1]
|
||||
const cluster = clusterStore.getById(clusterId)
|
||||
cluster = clusterStore.getById(clusterId)
|
||||
if (cluster) {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
|
||||
|
||||
@ -92,7 +92,7 @@ export class Cluster implements ClusterModel {
|
||||
async init(port: number) {
|
||||
try {
|
||||
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.initialized = true;
|
||||
logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
|
||||
@ -111,12 +111,10 @@ export class Cluster implements ClusterModel {
|
||||
protected bindEvents() {
|
||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||
const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s
|
||||
const refreshEventsTimer = setInterval(() => this.online && this.refreshEvents(), 3000); // every 3s
|
||||
|
||||
this.eventDisposers.push(
|
||||
reaction(this.getState, this.pushState),
|
||||
() => clearInterval(refreshTimer),
|
||||
() => clearInterval(refreshEventsTimer),
|
||||
);
|
||||
}
|
||||
|
||||
@ -135,7 +133,13 @@ export class Cluster implements ClusterModel {
|
||||
if (this.disconnected || (!init && !this.accessible)) {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -161,15 +165,14 @@ export class Cluster implements ClusterModel {
|
||||
@action
|
||||
async refresh() {
|
||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
await this.refreshConnectionStatus(); // refresh "version", "online", etc.
|
||||
await this.whenInitialized;
|
||||
await this.refreshConnectionStatus();
|
||||
if (this.accessible) {
|
||||
this.kubeCtl = new Kubectl(this.version)
|
||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||
const [features, isAdmin, nodesCount] = await Promise.all([
|
||||
getFeatures(this),
|
||||
this.isClusterAdmin(),
|
||||
this.getNodeCount(),
|
||||
this.kubeCtl.ensureKubectl()
|
||||
]);
|
||||
this.features = features;
|
||||
this.isAdmin = isAdmin;
|
||||
@ -178,8 +181,8 @@ export class Cluster implements ClusterModel {
|
||||
this.refreshEvents(),
|
||||
this.refreshAllowedResources(),
|
||||
]);
|
||||
this.ready = true
|
||||
}
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
@action
|
||||
@ -396,7 +399,7 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
|
||||
pushState = (state = this.getState()): ClusterState => {
|
||||
logger.debug(`[CLUSTER]: push-state`, state);
|
||||
logger.silly(`[CLUSTER]: push-state`, state);
|
||||
broadcastIpc({
|
||||
channel: "cluster:state",
|
||||
frameId: this.frameId,
|
||||
|
||||
@ -70,7 +70,8 @@ export class ContextHandler {
|
||||
|
||||
async resolveAuthProxyUrl() {
|
||||
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> {
|
||||
@ -88,7 +89,7 @@ export class ContextHandler {
|
||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||
const proxyUrl = await this.resolveAuthProxyUrl();
|
||||
return {
|
||||
target: proxyUrl + this.clusterUrl.path,
|
||||
target: proxyUrl,
|
||||
changeOrigin: true,
|
||||
timeout: timeout,
|
||||
headers: {
|
||||
|
||||
@ -11,7 +11,7 @@ export class KubeconfigManager {
|
||||
protected configDir = app.getPath("temp")
|
||||
protected tempFile: string;
|
||||
|
||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) {
|
||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
@ -28,6 +28,10 @@ export class KubeconfigManager {
|
||||
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.
|
||||
* 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: [
|
||||
{
|
||||
name: contextName,
|
||||
server: await contextHandler.resolveAuthProxyUrl(),
|
||||
server: this.resolveProxyUrl(),
|
||||
skipTLSVerify: undefined,
|
||||
}
|
||||
],
|
||||
@ -62,7 +66,7 @@ export class KubeconfigManager {
|
||||
// write
|
||||
const configYaml = dumpConfigYaml(proxyConfig);
|
||||
fs.ensureDir(path.dirname(tempFile));
|
||||
fs.writeFileSync(tempFile, configYaml);
|
||||
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
|
||||
this.tempFile = tempFile;
|
||||
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
||||
return tempFile;
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import net from "net";
|
||||
import http from "http";
|
||||
import spdy from "spdy";
|
||||
import httpProxy from "http-proxy";
|
||||
import url from "url";
|
||||
import * as WebSocket from "ws"
|
||||
import { apiPrefix, apiKubePrefix } from "../common/vars"
|
||||
import { openShell } from "./node-shell-session";
|
||||
import { Router } from "./router"
|
||||
import { ClusterManager } from "./cluster-manager"
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import logger from "./logger"
|
||||
|
||||
export class LensProxy {
|
||||
@ -40,37 +41,49 @@ export class LensProxy {
|
||||
|
||||
protected buildCustomProxy(): http.Server {
|
||||
const proxy = this.createProxy();
|
||||
const customProxy = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res);
|
||||
});
|
||||
customProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
});
|
||||
customProxy.on("error", (err) => {
|
||||
const spdyProxy = spdy.createServer({
|
||||
spdy: {
|
||||
plain: true,
|
||||
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)
|
||||
} else {
|
||||
if (req.headers.upgrade?.startsWith("SPDY")) {
|
||||
this.handleSpdyProxy(proxy, req, socket, head)
|
||||
} else {
|
||||
socket.end()
|
||||
}
|
||||
}
|
||||
})
|
||||
spdyProxy.on("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 {
|
||||
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) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
|
||||
@ -2,14 +2,16 @@ import { app, remote } from "electron";
|
||||
import winston from "winston"
|
||||
import { isDebugging } from "../common/vars";
|
||||
|
||||
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info"
|
||||
|
||||
const consoleOptions: winston.transports.ConsoleTransportOptions = {
|
||||
handleExceptions: false,
|
||||
level: isDebugging ? "debug" : "info",
|
||||
level: logLevel,
|
||||
}
|
||||
|
||||
const fileOptions: winston.transports.FileTransportOptions = {
|
||||
handleExceptions: false,
|
||||
level: isDebugging ? "debug" : "info",
|
||||
level: logLevel,
|
||||
filename: "lens.log",
|
||||
dirname: (app ?? remote?.app)?.getPath("logs"),
|
||||
maxsize: 16 * 1024,
|
||||
|
||||
@ -39,7 +39,7 @@ export class ShellSession extends EventEmitter {
|
||||
public async open() {
|
||||
this.kubectlBinDir = await this.kubectl.binDir()
|
||||
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()
|
||||
const env = await this.getCachedShellEnv()
|
||||
const shell = env.PTYSHELL
|
||||
|
||||
@ -23,8 +23,8 @@ export class WindowManager {
|
||||
this.mainView = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 900,
|
||||
minHeight: 760,
|
||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import React from "react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
|
||||
export class NotFound extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<MainLayout className="NotFound" contentClass="flex" footer={null}>
|
||||
<TabLayout className="NotFound" contentClass="flex">
|
||||
<p className="box center">
|
||||
<Trans>Page not found</Trans>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
</TabLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -80,7 +80,6 @@ export class AddCluster extends React.Component {
|
||||
const contexts = this.getContexts(this.kubeConfigLocal);
|
||||
this.kubeContexts.replace(contexts);
|
||||
break;
|
||||
|
||||
case KubeConfigSourceTab.TEXT:
|
||||
try {
|
||||
this.error = ""
|
||||
@ -91,6 +90,10 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.kubeContexts.size === 1) {
|
||||
this.selectedContexts.push(this.kubeContexts.keys().next().value)
|
||||
}
|
||||
}
|
||||
|
||||
getContexts(config: KubeConfig): Map<string, KubeConfig> {
|
||||
@ -206,7 +209,7 @@ export class AddCluster extends React.Component {
|
||||
<Tab
|
||||
value={KubeConfigSourceTab.FILE}
|
||||
label={<Trans>Select kubeconfig file</Trans>}
|
||||
active={this.sourceTab == KubeConfigSourceTab.FILE}/>
|
||||
active={this.sourceTab == KubeConfigSourceTab.FILE} />
|
||||
<Tab
|
||||
value={KubeConfigSourceTab.TEXT}
|
||||
label={<Trans>Paste as text</Trans>}
|
||||
@ -320,13 +323,15 @@ export class AddCluster extends React.Component {
|
||||
return (
|
||||
<div className={cssNames("kube-context flex gaps align-center", context)}>
|
||||
<span>{context}</span>
|
||||
{isNew && <Icon small material="fiber_new"/>}
|
||||
{isSelected && <Icon small material="check" className="box right"/>}
|
||||
{isNew && <Icon small material="fiber_new" />}
|
||||
{isSelected && <Icon small material="check" className="box right" />}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
render() {
|
||||
const addDisabled = this.selectedContexts.length === 0
|
||||
|
||||
return (
|
||||
<WizardLayout
|
||||
className="AddCluster"
|
||||
@ -374,9 +379,12 @@ export class AddCluster extends React.Component {
|
||||
<div className="actions-panel">
|
||||
<Button
|
||||
primary
|
||||
disabled={addDisabled}
|
||||
label={<Trans>Add cluster(s)</Trans>}
|
||||
onClick={this.addClusters}
|
||||
waiting={this.isWaiting}
|
||||
tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
|
||||
tooltipOverrideDisabled
|
||||
/>
|
||||
</div>
|
||||
</WizardLayout>
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
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 { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
@ -30,12 +30,12 @@ export class Apps extends React.Component {
|
||||
render() {
|
||||
const tabRoutes = Apps.tabRoutes;
|
||||
return (
|
||||
<MainLayout className="Apps" tabs={tabRoutes}>
|
||||
<TabLayout className="Apps" tabs={tabRoutes}>
|
||||
<Switch>
|
||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||
<Redirect to={tabRoutes[0].url}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,38 @@
|
||||
import "./cluster-settings.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { Features } from "./features";
|
||||
import { Removal } from "./removal";
|
||||
import { Status } from "./status";
|
||||
import { General } from "./general";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { ClusterIcon } from "../cluster-icon";
|
||||
import { Icon } from "../icon";
|
||||
import { getMatchedCluster } from "../cluster-manager/cluster-view.route";
|
||||
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
|
||||
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() {
|
||||
window.addEventListener('keydown', this.onEscapeKey);
|
||||
disposeOnUnmount(this,
|
||||
autorun(() => {
|
||||
this.refreshCluster();
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -29,12 +46,18 @@ export class ClusterSettings extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
refreshCluster = () => {
|
||||
if(this.cluster) {
|
||||
clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
render() {
|
||||
const cluster = getMatchedCluster();
|
||||
const cluster = this.cluster
|
||||
if (!cluster) return null;
|
||||
const header = (
|
||||
<>
|
||||
|
||||
@ -3,7 +3,7 @@ import "./cluster.scss"
|
||||
import React from "react";
|
||||
import { computed, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
import { Spinner } from "../spinner";
|
||||
import { cssNames, interval, isElectron } from "../../utils";
|
||||
@ -54,7 +54,7 @@ export class Cluster extends React.Component {
|
||||
render() {
|
||||
const { isLoaded } = this;
|
||||
return (
|
||||
<MainLayout>
|
||||
<TabLayout>
|
||||
<div className="Cluster">
|
||||
{!isLoaded && <Spinner center/>}
|
||||
{isLoaded && (
|
||||
@ -65,7 +65,7 @@ export class Cluster extends React.Component {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
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 { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
@ -68,12 +68,12 @@ export class Config extends React.Component {
|
||||
render() {
|
||||
const tabRoutes = Config.tabRoutes;
|
||||
return (
|
||||
<MainLayout className="Config" tabs={tabRoutes}>
|
||||
<TabLayout className="Config" tabs={tabRoutes}>
|
||||
<Switch>
|
||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
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 { CrdList } from "./crd-list";
|
||||
import { CrdResources } from "./crd-resources";
|
||||
@ -25,13 +25,13 @@ export class CustomResources extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MainLayout>
|
||||
<TabLayout>
|
||||
<Switch>
|
||||
<Route component={CrdList} {...crdDefinitionsRoute} exact/>
|
||||
<Route component={CrdResources} {...crdResourcesRoute}/>
|
||||
<Redirect to={crdURL()}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ import "./events.scss";
|
||||
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { eventStore } from "./event.store";
|
||||
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
||||
import { Trans } from "@lingui/macro";
|
||||
@ -118,9 +118,9 @@ export class Events extends React.Component<Props> {
|
||||
return events;
|
||||
}
|
||||
return (
|
||||
<MainLayout>
|
||||
<TabLayout>
|
||||
{events}
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +87,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
super.reset();
|
||||
this.contextNs.clear();
|
||||
}
|
||||
|
||||
async remove(item: Namespace) {
|
||||
await super.remove(item);
|
||||
this.contextNs.remove(item.getName());
|
||||
}
|
||||
}
|
||||
|
||||
export const namespaceStore = new NamespaceStore();
|
||||
|
||||
@ -4,7 +4,7 @@ import React from "react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Namespace, namespacesApi, NamespaceStatus } from "../../api/endpoints";
|
||||
import { AddNamespaceDialog } from "./add-namespace-dialog";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { Badge } from "../badge";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
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> {
|
||||
render() {
|
||||
return (
|
||||
<MainLayout>
|
||||
<TabLayout>
|
||||
<KubeObjectListLayout
|
||||
isClusterScoped
|
||||
className="Namespaces" store={namespaceStore}
|
||||
@ -65,7 +65,7 @@ export class Namespaces extends React.Component<Props> {
|
||||
})}
|
||||
/>
|
||||
<AddNamespaceDialog/>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ export class IngressDetails extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
|
||||
if (ingressPoints.length === 0) return null
|
||||
if (!ingressPoints || ingressPoints.length === 0) return null
|
||||
return (
|
||||
<div>
|
||||
<Table className="ingress-points">
|
||||
|
||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
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 { Endpoints, endpointRoute, endpointURL } from "../+network-endpoints";
|
||||
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
||||
@ -60,12 +60,12 @@ export class Network extends React.Component<Props> {
|
||||
render() {
|
||||
const tabRoutes = Network.tabRoutes;
|
||||
return (
|
||||
<MainLayout className="Network" tabs={tabRoutes}>
|
||||
<TabLayout className="Network" tabs={tabRoutes}>
|
||||
<Switch>
|
||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { cssNames, interval } from "../../utils";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { nodesStore } from "./nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
@ -123,7 +123,7 @@ export class Nodes extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MainLayout>
|
||||
<TabLayout>
|
||||
<KubeObjectListLayout
|
||||
className="Nodes"
|
||||
store={nodesStore} isClusterScoped
|
||||
@ -182,7 +182,7 @@ export class Nodes extends React.Component<Props> {
|
||||
return <NodeMenu object={item}/>
|
||||
}}
|
||||
/>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
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 { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
||||
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
||||
@ -52,12 +52,12 @@ export class Storage extends React.Component<Props> {
|
||||
render() {
|
||||
const tabRoutes = Storage.tabRoutes;
|
||||
return (
|
||||
<MainLayout className="Storage" tabs={tabRoutes}>
|
||||
<TabLayout className="Storage" tabs={tabRoutes}>
|
||||
<Switch>
|
||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ServiceAccount> {
|
||||
}
|
||||
@ -22,21 +23,25 @@ interface Props extends KubeObjectDetailsProps<ServiceAccount> {
|
||||
@observer
|
||||
export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
@observable secrets: Secret[];
|
||||
@observable imagePullSecrets: Secret[];
|
||||
|
||||
@disposeOnUnmount
|
||||
loadSecrets = autorun(async () => {
|
||||
this.secrets = null;
|
||||
this.imagePullSecrets = null;
|
||||
const { object: serviceAccount } = this.props;
|
||||
if (!serviceAccount) {
|
||||
return;
|
||||
}
|
||||
const namespace = serviceAccount.getNs();
|
||||
const secrets = serviceAccount.getSecrets().map(({ name }) => {
|
||||
const secret = secretsStore.getByName(name, namespace);
|
||||
if (!secret) return secretsStore.load({ name, namespace });
|
||||
return secret;
|
||||
return secretsStore.load({ name, namespace });
|
||||
});
|
||||
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() {
|
||||
@ -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[]) {
|
||||
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 (
|
||||
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
||||
{secret.getName()}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
generateDummySecretObject(name: string) {
|
||||
return new Secret({
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {
|
||||
name: name,
|
||||
uid: null,
|
||||
selfLink: null,
|
||||
resourceVersion: null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -69,9 +105,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
secret.getNs() == serviceAccount.getNs() &&
|
||||
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`)
|
||||
)
|
||||
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(({ name }) =>
|
||||
secretsStore.getByName(name, serviceAccount.getNs())
|
||||
)
|
||||
const imagePullSecrets = serviceAccount.getImagePullSecrets()
|
||||
return (
|
||||
<div className="ServiceAccountsDetails">
|
||||
<KubeObjectMeta object={serviceAccount}/>
|
||||
@ -83,7 +117,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
}
|
||||
{imagePullSecrets.length > 0 &&
|
||||
<DrawerItem name={<Trans>ImagePullSecrets</Trans>} className="links">
|
||||
{this.renderSecretLinks(imagePullSecrets)}
|
||||
{this.renderImagePullSecrets()}
|
||||
</DrawerItem>
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
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 { RoleBindings } from "../+user-management-roles-bindings";
|
||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||
@ -55,12 +55,12 @@ export class UserManagement extends React.Component<Props> {
|
||||
render() {
|
||||
const tabRoutes = UserManagement.tabRoutes;
|
||||
return (
|
||||
<MainLayout className="UserManagement" tabs={tabRoutes}>
|
||||
<TabLayout className="UserManagement" tabs={tabRoutes}>
|
||||
<Switch>
|
||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||
<Redirect to={usersManagementURL({ query: namespaceStore.getContextParams() })}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,24 @@
|
||||
.WhatsNew {
|
||||
$spacing: $padding * 2;
|
||||
|
||||
background: $mainBackground url(../../components/icon/crane.svg) no-repeat;
|
||||
background-position: 0 35%;
|
||||
background-size: 85%;
|
||||
background-clip: content-box;
|
||||
&::after {
|
||||
content: "";
|
||||
background: url(../../components/icon/crane.svg) no-repeat;
|
||||
background-position: 0 35%;
|
||||
background-size: 85%;
|
||||
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 {
|
||||
width: 200px;
|
||||
|
||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
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 { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL, workloadsURL } from "./workloads.route";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
@ -86,12 +86,12 @@ export class Workloads extends React.Component<Props> {
|
||||
render() {
|
||||
const tabRoutes = Workloads.tabRoutes;
|
||||
return (
|
||||
<MainLayout className="Workloads" tabs={tabRoutes}>
|
||||
<TabLayout className="Workloads" tabs={tabRoutes}>
|
||||
<Switch>
|
||||
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
|
||||
<Redirect to={workloadsURL({ query: namespaceStore.getContextParams() })}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dial
|
||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||
import { crdRoute } from "./+custom-resources";
|
||||
import { isAllowedResource } from "../../common/rbac";
|
||||
import { MainLayout } from "./layout/main-layout";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { Terminal } from "./dock/terminal";
|
||||
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
||||
@ -60,24 +61,25 @@ export class App extends React.Component {
|
||||
<I18nProvider i18n={_i18n}>
|
||||
<Router history={history}>
|
||||
<ErrorBoundary>
|
||||
<Switch>
|
||||
<Route component={Cluster} {...clusterRoute}/>
|
||||
<Route component={Nodes} {...nodesRoute}/>
|
||||
<Route component={Workloads} {...workloadsRoute}/>
|
||||
<Route component={Config} {...configRoute}/>
|
||||
<Route component={Network} {...networkRoute}/>
|
||||
<Route component={Storage} {...storageRoute}/>
|
||||
<Route component={Namespaces} {...namespacesRoute}/>
|
||||
<Route component={Events} {...eventRoute}/>
|
||||
<Route component={CustomResources} {...crdRoute}/>
|
||||
<Route component={UserManagement} {...usersManagementRoute}/>
|
||||
<Route component={Apps} {...appsRoute}/>
|
||||
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
|
||||
return <Route key={path} path={path} component={Page}/>
|
||||
})}
|
||||
<Redirect exact from="/" to={this.startURL}/>
|
||||
<Route component={NotFound}/>
|
||||
</Switch>
|
||||
<MainLayout>
|
||||
<Switch>
|
||||
<Route component={Cluster} {...clusterRoute}/>
|
||||
<Route component={Nodes} {...nodesRoute}/>
|
||||
<Route component={Workloads} {...workloadsRoute}/>
|
||||
<Route component={Config} {...configRoute}/>
|
||||
<Route component={Network} {...networkRoute}/>
|
||||
<Route component={Storage} {...storageRoute}/>
|
||||
<Route component={Namespaces} {...namespacesRoute}/>
|
||||
<Route component={Events} {...eventRoute}/>
|
||||
<Route component={CustomResources} {...crdRoute}/>
|
||||
<Route component={UserManagement} {...usersManagementRoute}/>
|
||||
<Route component={Apps} {...appsRoute}/>
|
||||
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
|
||||
return <Route key={path} path={path} component={Page}/>
|
||||
})}
|
||||
<Redirect exact from="/" to={this.startURL}/>
|
||||
<Route component={NotFound}/>
|
||||
</Switch></MainLayout>
|
||||
<Notifications/>
|
||||
<ConfirmDialog/>
|
||||
<KubeObjectDetails/>
|
||||
|
||||
@ -13,11 +13,18 @@ interface Props extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||
export class Badge extends React.Component<Props> {
|
||||
render() {
|
||||
const { className, label, small, children, ...elemProps } = this.props;
|
||||
return (
|
||||
return <>
|
||||
<span className={cssNames("Badge", { small }, className)} {...elemProps}>
|
||||
{label}
|
||||
{children}
|
||||
</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),
|
||||
&:disabled {
|
||||
color: silver;
|
||||
background: $buttonDisabledBackground;
|
||||
opacity: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ export class ClusterStatus extends React.Component<Props> {
|
||||
});
|
||||
})
|
||||
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}`);
|
||||
}
|
||||
|
||||
refreshCluster = async () => {
|
||||
activateCluster = async () => {
|
||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
|
||||
}
|
||||
|
||||
reconnect = async () => {
|
||||
this.isReconnecting = true;
|
||||
await this.refreshCluster();
|
||||
await this.activateCluster();
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import { ipcRenderer } from "electron";
|
||||
import { matchPath, RouteProps } from "react-router";
|
||||
import { buildURL, navigation } from "../../navigation";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route";
|
||||
|
||||
export interface IClusterViewRouteParams {
|
||||
clusterId: string;
|
||||
@ -19,10 +18,7 @@ export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute
|
||||
export function getMatchedClusterId(): string {
|
||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||
exact: true,
|
||||
path: [
|
||||
clusterViewRoute.path,
|
||||
clusterSettingsRoute.path,
|
||||
].flat(),
|
||||
path: clusterViewRoute.path
|
||||
})
|
||||
if (matched) {
|
||||
return matched.params.clusterId;
|
||||
|
||||
@ -49,12 +49,11 @@
|
||||
|
||||
.Badge {
|
||||
$boxSize: 17px;
|
||||
$offset: -7px;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
transform: translateX(-50%) translateY(50%);
|
||||
font-size: $font-size-small;
|
||||
right: $offset;
|
||||
bottom: $offset;
|
||||
line-height: $boxSize;
|
||||
min-width: $boxSize;
|
||||
min-height: $boxSize;
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import "./clusters-menu.scss"
|
||||
|
||||
import { remote } from "electron"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { ClusterIcon } from "../cluster-icon";
|
||||
import { Icon } from "../icon";
|
||||
import { autobind, cssNames, IClassName } from "../../utils";
|
||||
import { cssNames, IClassName, autobind } from "../../utils";
|
||||
import { Badge } from "../badge";
|
||||
import { navigate } from "../../navigation";
|
||||
import { addClusterURL } from "../+add-cluster";
|
||||
@ -19,8 +20,8 @@ import { landingURL } from "../+landing-page";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route";
|
||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||
import { clusterViewURL } from "./cluster-view.route";
|
||||
import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd";
|
||||
import { dynamicPages } from "../../../extensions/register-page";
|
||||
|
||||
interface Props {
|
||||
@ -36,6 +37,7 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
|
||||
addCluster = () => {
|
||||
navigate(addClusterURL());
|
||||
clusterStore.setActive(null);
|
||||
}
|
||||
|
||||
showContextMenu = (cluster: Cluster) => {
|
||||
@ -45,6 +47,7 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Settings`),
|
||||
click: () => {
|
||||
clusterStore.setActive(cluster.id);
|
||||
navigate(clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: cluster.id
|
||||
@ -58,6 +61,7 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
click: async () => {
|
||||
if (clusterStore.isActive(cluster.id)) {
|
||||
navigate(landingURL());
|
||||
clusterStore.setActive(null);
|
||||
}
|
||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
||||
}
|
||||
@ -113,26 +117,29 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{clusters.map((cluster, index) => (
|
||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
||||
{(provided: DraggableProvided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<ClusterIcon
|
||||
key={cluster.id}
|
||||
showErrors={true}
|
||||
cluster={cluster}
|
||||
isActive={cluster.id === getMatchedClusterId()}
|
||||
onClick={() => this.showCluster(cluster.id)}
|
||||
onContextMenu={() => this.showContextMenu(cluster)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{clusters.map((cluster, index) => {
|
||||
const isActive = cluster.id === clusterStore.activeClusterId;
|
||||
return (
|
||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
||||
{(provided: DraggableProvided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<ClusterIcon
|
||||
key={cluster.id}
|
||||
showErrors={true}
|
||||
cluster={cluster}
|
||||
isActive={isActive}
|
||||
onClick={() => this.showCluster(cluster.id)}
|
||||
onContextMenu={() => this.showContextMenu(cluster)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)}
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
@ -143,9 +150,9 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
<Tooltip targetId="add-cluster-icon">
|
||||
<Trans>Add Cluster</Trans>
|
||||
</Tooltip>
|
||||
<Icon big material="add" id="add-cluster-icon"/>
|
||||
<Icon big material="add" id="add-cluster-icon" />
|
||||
{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 className="dynamic-pages">
|
||||
|
||||
@ -48,6 +48,7 @@ export class EditorPanel extends React.Component<Props> {
|
||||
|
||||
onResize = () => {
|
||||
this.editor.resize();
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
onCursorPosChange = (pos: Ace.Point) => {
|
||||
|
||||
@ -62,14 +62,13 @@ export class InfoPanel extends Component<Props> {
|
||||
this.error = "";
|
||||
this.waiting = true;
|
||||
try {
|
||||
this.result = await this.props.submit().finally(() => {
|
||||
this.waiting = false;
|
||||
});
|
||||
this.result = await this.props.submit()
|
||||
if (showNotifications) Notifications.ok(this.result);
|
||||
} catch (error) {
|
||||
this.error = error.toString();
|
||||
if (showNotifications) Notifications.error(this.error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.waiting = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,12 +90,13 @@ export class InfoPanel extends Component<Props> {
|
||||
<>
|
||||
{result && (
|
||||
<div className="success flex align-center">
|
||||
<Icon material="done"/> <span>{result}</span>
|
||||
<Icon material="done" />
|
||||
<span>{result}</span>
|
||||
</div>
|
||||
)}
|
||||
{errorInfo && (
|
||||
<div className="error flex align-center">
|
||||
<Icon material="error_outline"/>
|
||||
<Icon material="error_outline" />
|
||||
<span>{errorInfo}</span>
|
||||
</div>
|
||||
)}
|
||||
@ -114,9 +114,9 @@ export class InfoPanel extends Component<Props> {
|
||||
{controls}
|
||||
</div>
|
||||
<div className="info flex gaps align-center">
|
||||
{waiting ? <><Spinner/> {submittingMessage}</> : this.renderInfo()}
|
||||
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderInfo()}
|
||||
</div>
|
||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close}/>
|
||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
||||
<Button
|
||||
primary active
|
||||
label={submitLabel}
|
||||
|
||||
@ -152,6 +152,7 @@ export class Terminal {
|
||||
onResize = () => {
|
||||
if (!this.isActive) return;
|
||||
this.fitLazy();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
onActivate = () => {
|
||||
|
||||
@ -44,7 +44,7 @@ export const isUrl: Validator = {
|
||||
|
||||
export const isPath: Validator = {
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
|
||||
.MainLayout {
|
||||
--sidebar-max-size: 200px;
|
||||
|
||||
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-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
|
||||
|
||||
height: 100%;
|
||||
|
||||
> .Tabs {
|
||||
grid-area: tabs;
|
||||
background: $layoutTabsBackground;
|
||||
}
|
||||
|
||||
header {
|
||||
> header {
|
||||
grid-area: header;
|
||||
background: $layoutBackground;
|
||||
padding: $padding $padding * 2;
|
||||
@ -28,7 +27,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
aside {
|
||||
> aside {
|
||||
grid-area: aside;
|
||||
position: relative;
|
||||
background: $sidebarBackground;
|
||||
@ -48,25 +47,14 @@
|
||||
&.accessible:hover {
|
||||
width: var(--sidebar-max-size);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
> main {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
||||
@ -3,26 +3,16 @@ import "./main-layout.scss";
|
||||
import React from "react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { matchPath, RouteProps } from "react-router-dom";
|
||||
import { createStorage, cssNames } from "../../utils";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { ErrorBoundary } from "../error-boundary";
|
||||
import { Dock } from "../dock";
|
||||
import { navigate, navigation } from "../../navigation";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
|
||||
export interface TabRoute extends RouteProps {
|
||||
title: React.ReactNode;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface MainLayoutProps {
|
||||
className?: any;
|
||||
tabs?: TabRoute[];
|
||||
footer?: React.ReactNode;
|
||||
headerClass?: string;
|
||||
contentClass?: string;
|
||||
footerClass?: string;
|
||||
}
|
||||
|
||||
@ -35,18 +25,17 @@ export class MainLayout extends React.Component<MainLayoutProps> {
|
||||
|
||||
@disposeOnUnmount syncPinnedStateWithStorage = reaction(
|
||||
() => this.isPinned,
|
||||
isPinned => this.storage.merge({ pinnedSidebar: isPinned })
|
||||
(isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
|
||||
);
|
||||
|
||||
toggleSidebar = () => {
|
||||
this.isPinned = !this.isPinned;
|
||||
this.isAccessible = false;
|
||||
setTimeout(() => this.isAccessible = true, 250);
|
||||
}
|
||||
setTimeout(() => (this.isAccessible = true), 250);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
|
||||
const routePath = navigation.location.pathname;
|
||||
const { className, headerClass, footer, footerClass, children } = this.props;
|
||||
const cluster = getHostedCluster();
|
||||
if (!cluster) {
|
||||
return null; // fix: skip render when removing active (visible) cluster
|
||||
@ -54,37 +43,18 @@ export class MainLayout extends React.Component<MainLayoutProps> {
|
||||
return (
|
||||
<div className={cssNames("MainLayout", className)}>
|
||||
<header className={cssNames("flex gaps align-center", headerClass)}>
|
||||
<span className="cluster">
|
||||
{cluster.preferences.clusterName || cluster.contextName}
|
||||
</span>
|
||||
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
|
||||
</header>
|
||||
|
||||
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
||||
<Sidebar
|
||||
className="box grow"
|
||||
isPinned={this.isPinned}
|
||||
toggle={this.toggleSidebar}
|
||||
/>
|
||||
<Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} />
|
||||
</aside>
|
||||
|
||||
{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>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</main>
|
||||
|
||||
<footer className={footerClass}>
|
||||
{footer === undefined ? <Dock/> : footer}
|
||||
</footer>
|
||||
<footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { TabRoute } from "./main-layout";
|
||||
import type { TabRoute } from "./tab-layout";
|
||||
import "./sidebar.scss";
|
||||
|
||||
import React from "react";
|
||||
@ -44,21 +44,21 @@ interface Props {
|
||||
@observer
|
||||
export class Sidebar extends React.Component<Props> {
|
||||
async componentDidMount() {
|
||||
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) {
|
||||
crdStore.loadAll()
|
||||
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
|
||||
crdStore.loadAll();
|
||||
}
|
||||
}
|
||||
|
||||
renderCustomResources() {
|
||||
return Object.entries(crdStore.groups).map(([group, crds]) => {
|
||||
const submenus = crds.map(crd => {
|
||||
const submenus = crds.map((crd) => {
|
||||
return {
|
||||
title: crd.getResourceKind(),
|
||||
component: CrdList,
|
||||
url: crd.getResourceUrl(),
|
||||
path: crdResourcesRoute.path,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
return (
|
||||
<SidebarNavItem
|
||||
key={group}
|
||||
@ -68,8 +68,8 @@ export class Sidebar extends React.Component<Props> {
|
||||
subMenus={submenus}
|
||||
text={group}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -80,7 +80,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
|
||||
<div className="header flex align-center">
|
||||
<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>
|
||||
</NavLink>
|
||||
<Icon
|
||||
@ -94,17 +94,17 @@ export class Sidebar extends React.Component<Props> {
|
||||
<div className="sidebar-nav flex column box grow-fixed">
|
||||
<SidebarNavItem
|
||||
id="cluster"
|
||||
isHidden={!isAllowedResource('nodes')}
|
||||
isHidden={!isAllowedResource("nodes")}
|
||||
url={clusterURL()}
|
||||
text={<Trans>Cluster</Trans>}
|
||||
icon={<Icon svg="kube"/>}
|
||||
icon={<Icon svg="kube" />}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="nodes"
|
||||
isHidden={!isAllowedResource('nodes')}
|
||||
isHidden={!isAllowedResource("nodes")}
|
||||
url={nodesURL()}
|
||||
text={<Trans>Nodes</Trans>}
|
||||
icon={<Icon svg="nodes"/>}
|
||||
icon={<Icon svg="nodes" />}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="workloads"
|
||||
@ -113,7 +113,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
routePath={workloadsRoute.path}
|
||||
subMenus={Workloads.tabRoutes}
|
||||
text={<Trans>Workloads</Trans>}
|
||||
icon={<Icon svg="workloads"/>}
|
||||
icon={<Icon svg="workloads" />}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="config"
|
||||
@ -122,7 +122,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
routePath={configRoute.path}
|
||||
subMenus={Config.tabRoutes}
|
||||
text={<Trans>Configuration</Trans>}
|
||||
icon={<Icon material="list"/>}
|
||||
icon={<Icon material="list" />}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="networks"
|
||||
@ -131,7 +131,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
routePath={networkRoute.path}
|
||||
subMenus={Network.tabRoutes}
|
||||
text={<Trans>Network</Trans>}
|
||||
icon={<Icon material="device_hub"/>}
|
||||
icon={<Icon material="device_hub" />}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="storage"
|
||||
@ -139,22 +139,22 @@ export class Sidebar extends React.Component<Props> {
|
||||
url={storageURL({ query })}
|
||||
routePath={storageRoute.path}
|
||||
subMenus={Storage.tabRoutes}
|
||||
icon={<Icon svg="storage"/>}
|
||||
icon={<Icon svg="storage" />}
|
||||
text={<Trans>Storage</Trans>}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="namespaces"
|
||||
isHidden={!isAllowedResource('namespaces')}
|
||||
isHidden={!isAllowedResource("namespaces")}
|
||||
url={namespacesURL()}
|
||||
icon={<Icon material="layers"/>}
|
||||
icon={<Icon material="layers" />}
|
||||
text={<Trans>Namespaces</Trans>}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="events"
|
||||
isHidden={!isAllowedResource('events')}
|
||||
isHidden={!isAllowedResource("events")}
|
||||
url={eventsURL({ query })}
|
||||
routePath={eventRoute.path}
|
||||
icon={<Icon material="access_time"/>}
|
||||
icon={<Icon material="access_time" />}
|
||||
text={<Trans>Events</Trans>}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
@ -162,7 +162,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
url={appsURL({ query })}
|
||||
subMenus={Apps.tabRoutes}
|
||||
routePath={appsRoute.path}
|
||||
icon={<Icon material="apps"/>}
|
||||
icon={<Icon material="apps" />}
|
||||
text={<Trans>Apps</Trans>}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
@ -170,16 +170,16 @@ export class Sidebar extends React.Component<Props> {
|
||||
url={usersManagementURL({ query })}
|
||||
routePath={usersManagementRoute.path}
|
||||
subMenus={UserManagement.tabRoutes}
|
||||
icon={<Icon material="security"/>}
|
||||
icon={<Icon material="security" />}
|
||||
text={<Trans>Access Control</Trans>}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
id="custom-resources"
|
||||
isHidden={!isAllowedResource('customresourcedefinitions')}
|
||||
isHidden={!isAllowedResource("customresourcedefinitions")}
|
||||
url={crdURL()}
|
||||
subMenus={CustomResources.tabRoutes}
|
||||
routePath={crdRoute.path}
|
||||
icon={<Icon material="extension"/>}
|
||||
icon={<Icon material="extension" />}
|
||||
text={<Trans>Custom Resources</Trans>}
|
||||
>
|
||||
{this.renderCustomResources()}
|
||||
@ -199,7 +199,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
</div>
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,7 +216,10 @@ interface SidebarNavItemProps {
|
||||
|
||||
const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []);
|
||||
const navItemState = observable.map<string, boolean>(navItemStorage.get());
|
||||
reaction(() => [...navItemState], value => navItemStorage.set(value));
|
||||
reaction(
|
||||
() => [...navItemState],
|
||||
(value) => navItemStorage.set(value)
|
||||
);
|
||||
|
||||
@observer
|
||||
class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
||||
@ -229,15 +232,15 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
||||
|
||||
toggleSubMenu = () => {
|
||||
navItemState.set(this.props.id, !this.isExpanded);
|
||||
}
|
||||
};
|
||||
|
||||
isActive = () => {
|
||||
const { routePath, url } = this.props;
|
||||
const { pathname } = navigation.location;
|
||||
return !!matchPath(pathname, {
|
||||
path: routePath || url
|
||||
path: routePath || url,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
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}>
|
||||
{icon}
|
||||
<span className="link-text">{text}</span>
|
||||
<Icon
|
||||
className="expand-icon"
|
||||
material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}
|
||||
/>
|
||||
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
|
||||
</div>
|
||||
<ul className={cssNames("sub-menu", { active: isActive })}>
|
||||
{subMenus.map(({ title, url }) => (
|
||||
@ -265,18 +265,18 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
||||
))}
|
||||
{React.Children.toArray(children).map((child: React.ReactElement<any>) => {
|
||||
return React.cloneElement(child, {
|
||||
className: cssNames(child.props.className, { visible: this.isExpanded })
|
||||
className: cssNames(child.props.className, { visible: this.isExpanded }),
|
||||
});
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={this.isActive}>
|
||||
{icon}
|
||||
<span className="link-text">{text}</span>
|
||||
</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;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
padding: $padding * 2;
|
||||
max-height: 100vh;
|
||||
z-index: 100000;
|
||||
height: min-content!important;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
||||
@ -15,6 +15,7 @@ export enum TooltipPosition {
|
||||
|
||||
export interface TooltipProps {
|
||||
targetId: string; // html-id of target element to bind for
|
||||
tooltipOnParentHover?: boolean; // detect hover on parent of target
|
||||
visible?: boolean; // initial visibility
|
||||
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
|
||||
@ -50,14 +51,22 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
return document.getElementById(this.props.targetId)
|
||||
}
|
||||
|
||||
get hoverTarget(): HTMLElement {
|
||||
if (this.props.tooltipOnParentHover) {
|
||||
return this.targetElem.parentElement
|
||||
}
|
||||
|
||||
return this.targetElem
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.targetElem.addEventListener("mouseenter", this.onEnterTarget)
|
||||
this.targetElem.addEventListener("mouseleave", this.onLeaveTarget)
|
||||
this.hoverTarget.addEventListener("mouseenter", this.onEnterTarget)
|
||||
this.hoverTarget.addEventListener("mouseleave", this.onLeaveTarget)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.targetElem.removeEventListener("mouseenter", this.onEnterTarget)
|
||||
this.targetElem.removeEventListener("mouseleave", this.onLeaveTarget)
|
||||
this.hoverTarget.removeEventListener("mouseenter", this.onEnterTarget)
|
||||
this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget)
|
||||
}
|
||||
|
||||
@autobind()
|
||||
|
||||
@ -8,6 +8,11 @@ import uniqueId from "lodash/uniqueId"
|
||||
|
||||
export interface TooltipDecoratorProps {
|
||||
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 {
|
||||
@ -17,22 +22,25 @@ export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
|
||||
protected tooltipId = uniqueId("tooltip_target_");
|
||||
|
||||
render() {
|
||||
const { tooltip, ...targetProps } = this.props;
|
||||
const { tooltip, tooltipOverrideDisabled, ...targetProps } = this.props;
|
||||
if (tooltip) {
|
||||
const tooltipId = targetProps.id || this.tooltipId;
|
||||
const tooltipProps: TooltipProps = {
|
||||
targetId: tooltipId,
|
||||
tooltipOnParentHover: tooltipOverrideDisabled,
|
||||
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
|
||||
};
|
||||
targetProps.id = tooltipId;
|
||||
targetProps.children = (
|
||||
<>
|
||||
{targetProps.children}
|
||||
<Tooltip {...tooltipProps}/>
|
||||
<div>
|
||||
{targetProps.children}
|
||||
</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!
|
||||
|
||||
## 3.6.0-rc.2 (current version)
|
||||
- Refresh input values on cluster change
|
||||
- Update logo
|
||||
- Fix margins in cluster menu
|
||||
## 3.6.4 (current version)
|
||||
- Fix: deleted namespace does not get auto unselected
|
||||
- Get focus to dock tab (terminal & resource editor) content after resize
|
||||
- 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 path to Kubectl binary, instead of using bundled Kubectl
|
||||
- Log application logs also to log file
|
||||
- Restrict file permissions to only the user for pasted kubeconfigs
|
||||
- Close Preferences and Cluster Setting on Esc keypress
|
||||
- Allow user to select Kubeconfig from filesystem
|
||||
- Show the path of the cluster's Kubeconfig in cluster settings
|
||||
- Store reference to added Kubeconfig files
|
||||
- Update logo
|
||||
- Update Kubectl versions used with Lens
|
||||
- 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 port-forwarding for containers in pod
|
||||
- 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
|
||||
- Show devtools in menu
|
||||
- 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
|
||||
- Handle unsupported resources properly after they've been created from editor
|
||||
- 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
|
||||
- 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: Handle invalid metrics responses properly
|
||||
- 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"
|
||||
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":
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "4.7.6"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
|
||||
@ -6113,6 +6125,16 @@ hosted-git-info@^3.0.4:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
@ -8646,6 +8673,11 @@ object.pick@^1.3.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "3.0.2"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
@ -10294,6 +10326,11 @@ scss-tokenizer@^0.2.3:
|
||||
js-base64 "^2.1.8"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/spectron/-/spectron-11.0.0.tgz#79d785e6b8898638e77b5186711e3910ed4ca09b"
|
||||
@ -11793,6 +11853,13 @@ watchpack@^1.6.1:
|
||||
chokidar "^3.4.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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user