mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Release/v5.2.1 (#3819)
* Use PATCH verb to scale deployments and statefulsets (#3744) Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Stop Sentry from capturing console.error and logger.error (#3785) Signed-off-by: Hung-Han (Henry) Chen <chenhungh@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Sidebar cluster avatar (#3765) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Second click on list item closes the details view (#3809) Signed-off-by: Juho Heikka <juho.heikka@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix fallback to bundled kubectl (#3812) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix cluster's accessibleNamespaces being reset on restarting Lens (#3817) Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Don't change permissions when deleting a cluster (#3798) Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Draggable region on macOS should be the same as the main header (#3800) Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix check for user exec command (#3664) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix shell connection readiness heuristic (#3795) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * release v5.2.1 Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix test action Signed-off-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: chh <1474479+chenhunghan@users.noreply.github.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Juho Heikka <juho.heikka@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
a06f0c1ab4
commit
20ca49327e
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -2,7 +2,7 @@ name: Test
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- "**"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|||||||
@ -36,7 +36,7 @@ function getSidebarSelectors(itemId: string) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
expandSubMenu: `${root} .nav-item`,
|
expandSubMenu: `${root} .nav-item`,
|
||||||
subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
|
subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="/${href}"]`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -113,7 +113,7 @@ export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Fra
|
|||||||
|
|
||||||
const frame = await minikubeFrame.contentFrame();
|
const frame = await minikubeFrame.contentFrame();
|
||||||
|
|
||||||
await frame.waitForSelector("div.Sidebar");
|
await frame.waitForSelector("[data-testid=cluster-sidebar]");
|
||||||
|
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.2.0",
|
"version": "5.2.1",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -185,6 +185,7 @@
|
|||||||
"@kubernetes/client-node": "^0.15.1",
|
"@kubernetes/client-node": "^0.15.1",
|
||||||
"@sentry/electron": "^2.5.0",
|
"@sentry/electron": "^2.5.0",
|
||||||
"@sentry/integrations": "^6.10.0",
|
"@sentry/integrations": "^6.10.0",
|
||||||
|
"@types/which": "^2.0.1",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"array-move": "^3.0.1",
|
"array-move": "^3.0.1",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
@ -193,7 +194,6 @@
|
|||||||
"byline": "^5.0.0",
|
"byline": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"chokidar": "^3.4.3",
|
"chokidar": "^3.4.3",
|
||||||
"command-exists": "1.2.9",
|
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
@ -246,6 +246,7 @@
|
|||||||
"tempy": "^0.5.0",
|
"tempy": "^0.5.0",
|
||||||
"url-parse": "^1.5.1",
|
"url-parse": "^1.5.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
"which": "^2.0.2",
|
||||||
"win-ca": "^3.2.0",
|
"win-ca": "^3.2.0",
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"winston-console-format": "^1.0.8",
|
"winston-console-format": "^1.0.8",
|
||||||
@ -347,7 +348,6 @@
|
|||||||
"jest-canvas-mock": "^2.3.1",
|
"jest-canvas-mock": "^2.3.1",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.16",
|
"jest-mock-extended": "^1.0.16",
|
||||||
"json-to-pretty-yaml": "^1.2.2",
|
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^1.6.0",
|
"mini-css-extract-plugin": "^1.6.0",
|
||||||
"node-gyp": "7.1.2",
|
"node-gyp": "7.1.2",
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
|||||||
material?: string;
|
material?: string;
|
||||||
background?: string;
|
background?: string;
|
||||||
};
|
};
|
||||||
|
accessibleNamespaces?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||||
|
|||||||
@ -19,15 +19,18 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export class ExecValidationNotFoundError extends Error {
|
export class ExecValidationNotFoundError extends Error {
|
||||||
constructor(execPath: string, isAbsolute: boolean) {
|
constructor(execPath: string) {
|
||||||
super(`User Exec command "${execPath}" not found on host.`);
|
|
||||||
let message = `User Exec command "${execPath}" not found on host.`;
|
let message = `User Exec command "${execPath}" not found on host.`;
|
||||||
|
|
||||||
if (!isAbsolute) {
|
if (!path.isAbsolute(execPath)) {
|
||||||
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
||||||
}
|
}
|
||||||
this.message = message;
|
|
||||||
|
super(message);
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,38 +19,42 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.Sidebar {
|
import { Deployment, DeploymentApi } from "../endpoints/deployment.api";
|
||||||
$iconSize: 24px;
|
import type { KubeJsonApi } from "../kube-json-api";
|
||||||
$itemSpacing: floor($unit / 2.6) floor($unit / 1.6);
|
|
||||||
|
|
||||||
.sidebar-nav {
|
class DeploymentApiTest extends DeploymentApi {
|
||||||
width: var(--sidebar-width);
|
public setRequest(request: any) {
|
||||||
padding-bottom: calc(var(--padding) * 3);
|
this.request = request;
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
--size: #{$iconSize};
|
|
||||||
|
|
||||||
box-sizing: content-box;
|
|
||||||
padding: floor($padding / 2.6);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
describe("DeploymentApi", () => {
|
||||||
padding: $padding;
|
describe("scale", () => {
|
||||||
text-align: center;
|
const requestMock = {
|
||||||
}
|
patch: () => ({}),
|
||||||
|
} as unknown as KubeJsonApi;
|
||||||
|
|
||||||
.cluster-name {
|
const sub = new DeploymentApiTest({ objectConstructor: Deployment });
|
||||||
padding: 1.25rem;
|
|
||||||
font-weight: bold;
|
sub.setRequest(requestMock);
|
||||||
font-size: 1.5rem;
|
|
||||||
word-break: break-all;
|
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
|
||||||
color: var(--textColorAccent);
|
const patchSpy = jest.spyOn(requestMock, "patch");
|
||||||
|
|
||||||
|
sub.scale({ namespace: "default", name: "deployment-1"}, 5);
|
||||||
|
|
||||||
|
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
|
||||||
|
data: {
|
||||||
|
spec: {
|
||||||
|
replicas: 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/merge-patch+json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
60
src/common/k8s-api/__tests__/stateful-set.api.test.ts
Normal file
60
src/common/k8s-api/__tests__/stateful-set.api.test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { StatefulSet, StatefulSetApi } from "../endpoints/stateful-set.api";
|
||||||
|
import type { KubeJsonApi } from "../kube-json-api";
|
||||||
|
|
||||||
|
class StatefulSetApiTest extends StatefulSetApi {
|
||||||
|
public setRequest(request: any) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("StatefulSetApi", () => {
|
||||||
|
describe("scale", () => {
|
||||||
|
const requestMock = {
|
||||||
|
patch: () => ({}),
|
||||||
|
} as unknown as KubeJsonApi;
|
||||||
|
|
||||||
|
const sub = new StatefulSetApiTest({ objectConstructor: StatefulSet });
|
||||||
|
|
||||||
|
sub.setRequest(requestMock);
|
||||||
|
|
||||||
|
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
|
||||||
|
const patchSpy = jest.spyOn(requestMock, "patch");
|
||||||
|
|
||||||
|
sub.scale({ namespace: "default", name: "statefulset-1"}, 5);
|
||||||
|
|
||||||
|
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
|
||||||
|
data: {
|
||||||
|
spec: {
|
||||||
|
replicas: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/merge-patch+json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -41,13 +41,17 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||||
return this.request.put(this.getScaleApiUrl(params), {
|
return this.request.patch(this.getScaleApiUrl(params), {
|
||||||
data: {
|
data: {
|
||||||
metadata: params,
|
|
||||||
spec: {
|
spec: {
|
||||||
replicas
|
replicas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/merge-patch+json"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,13 +39,17 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||||
return this.request.put(this.getScaleApiUrl(params), {
|
return this.request.patch(this.getScaleApiUrl(params), {
|
||||||
data: {
|
data: {
|
||||||
metadata: params,
|
|
||||||
spec: {
|
spec: {
|
||||||
replicas
|
replicas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/merge-patch+json"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,11 +25,11 @@ import path from "path";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import commandExists from "command-exists";
|
|
||||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||||
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
||||||
import { resolvePath } from "./utils";
|
import { resolvePath } from "./utils";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
import which from "which";
|
||||||
|
|
||||||
export type KubeConfigValidationOpts = {
|
export type KubeConfigValidationOpts = {
|
||||||
validateCluster?: boolean;
|
validateCluster?: boolean;
|
||||||
@ -295,13 +295,17 @@ export function validateKubeConfig(config: KubeConfig, contextName: string, vali
|
|||||||
|
|
||||||
// Validate exec command if present
|
// Validate exec command if present
|
||||||
if (validateExec && user?.exec) {
|
if (validateExec && user?.exec) {
|
||||||
const execCommand = user.exec["command"];
|
try {
|
||||||
// check if the command is absolute or not
|
which.sync(user.exec.command);
|
||||||
const isAbsolute = path.isAbsolute(execCommand);
|
|
||||||
|
|
||||||
// validate the exec struct in the user object, start with the command field
|
// If this doesn't throw an error it also means that it has found the executable.
|
||||||
if (!commandExists.sync(execCommand)) {
|
} catch (error) {
|
||||||
return new ExecValidationNotFoundError(execCommand, isAbsolute);
|
switch (error?.code) {
|
||||||
|
case "ENOENT":
|
||||||
|
return new ExecValidationNotFoundError(user.exec.command);
|
||||||
|
default:
|
||||||
|
return error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import type Transport from "winston-transport";
|
|||||||
import { consoleFormat } from "winston-console-format";
|
import { consoleFormat } from "winston-console-format";
|
||||||
import { isDebugging, isTestEnv } from "./vars";
|
import { isDebugging, isTestEnv } from "./vars";
|
||||||
import BrowserConsole from "winston-transport-browserconsole";
|
import BrowserConsole from "winston-transport-browserconsole";
|
||||||
import { SentryTransport } from "./logger-transports";
|
|
||||||
import { getPath } from "./utils/getPath";
|
import { getPath } from "./utils/getPath";
|
||||||
|
|
||||||
const logLevel = process.env.LOG_LEVEL
|
const logLevel = process.env.LOG_LEVEL
|
||||||
@ -36,9 +35,7 @@ const logLevel = process.env.LOG_LEVEL
|
|||||||
? "error"
|
? "error"
|
||||||
: "info";
|
: "info";
|
||||||
|
|
||||||
const transports: Transport[] = [
|
const transports: Transport[] = [];
|
||||||
new SentryTransport("error")
|
|
||||||
];
|
|
||||||
|
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
transports.push(
|
transports.push(
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
|
import { Dedupe, Offline } from "@sentry/integrations";
|
||||||
import * as Sentry from "@sentry/electron";
|
import * as Sentry from "@sentry/electron";
|
||||||
import { sentryDsn, isProduction } from "./vars";
|
import { sentryDsn, isProduction } from "./vars";
|
||||||
import { UserStore } from "./user-store";
|
import { UserStore } from "./user-store";
|
||||||
@ -65,7 +65,6 @@ export function SentryInit() {
|
|||||||
},
|
},
|
||||||
dsn: sentryDsn,
|
dsn: sentryDsn,
|
||||||
integrations: [
|
integrations: [
|
||||||
new CaptureConsole({ levels: ["error"] }),
|
|
||||||
new Dedupe(),
|
new Dedupe(),
|
||||||
new Offline(),
|
new Offline(),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -193,7 +193,10 @@ export class ClusterManager extends Singleton {
|
|||||||
} else {
|
} else {
|
||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
cluster.accessibleNamespaces = entity.spec.accessibleNamespaces ?? [];
|
|
||||||
|
if (entity.spec.accessibleNamespace) {
|
||||||
|
cluster.accessibleNamespaces = entity.spec.accessibleNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
this.updateEntityFromCluster(cluster);
|
this.updateEntityFromCluster(cluster);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -275,7 +275,7 @@ export class Kubectl {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
isValid = !await this.checkBinary(this.path, false);
|
isValid = await this.checkBinary(this.path, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export class WindowManager extends Singleton {
|
|||||||
show: false,
|
show: false,
|
||||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hiddenInset",
|
||||||
backgroundColor: "#1e2124",
|
backgroundColor: "#1e2124",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__static, "build", "preload.js"),
|
preload: path.join(__static, "build", "preload.js"),
|
||||||
|
|||||||
@ -58,7 +58,6 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
|
|
||||||
public onReady = new EventEmitter<[]>();
|
public onReady = new EventEmitter<[]>();
|
||||||
@observable public isReady = false;
|
@observable public isReady = false;
|
||||||
@observable public shellRunCommandsFinished = false;
|
|
||||||
public readonly url: string;
|
public readonly url: string;
|
||||||
|
|
||||||
constructor(protected options: TerminalApiQuery) {
|
constructor(protected options: TerminalApiQuery) {
|
||||||
@ -92,7 +91,6 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
connect() {
|
connect() {
|
||||||
this.emitStatus("Connecting ...");
|
this.emitStatus("Connecting ...");
|
||||||
this.onData.addListener(this._onReady, { prepend: true });
|
this.onData.addListener(this._onReady, { prepend: true });
|
||||||
this.onData.addListener(this._onShellRunCommandsFinished);
|
|
||||||
super.connect(this.url);
|
super.connect(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,24 +107,6 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
this.onReady.removeAllListeners();
|
this.onReady.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onShellRunCommandsFinished = (data: string) => {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a heuistic for ditermining when a shell has finished executing
|
|
||||||
* its own rc file (or RunCommands file) such as `.bashrc` or `.zshrc`.
|
|
||||||
*
|
|
||||||
* This heuistic assumes that the prompt line of a terminal is a single line
|
|
||||||
* and ends with a whitespace character.
|
|
||||||
*/
|
|
||||||
if (data.match(/\r?\n/) === null && data.match(/\s$/)) {
|
|
||||||
this.shellRunCommandsFinished = true;
|
|
||||||
this.onData.removeListener(this._onShellRunCommandsFinished);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
protected _onReady(data: string) {
|
protected _onReady(data: string) {
|
||||||
if (!data) return true;
|
if (!data) return true;
|
||||||
|
|||||||
@ -50,4 +50,11 @@
|
|||||||
display: block;
|
display: block;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions-panel {
|
||||||
|
.Spinner {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: $spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import "./add-cluster.scss";
|
|||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { KubeConfig } from "@kubernetes/client-node";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { action, computed, observable, makeObservable } from "mobx";
|
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -41,6 +41,7 @@ import { SettingLayout } from "../layout/setting-layout";
|
|||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
config: KubeConfig;
|
config: KubeConfig;
|
||||||
@ -62,6 +63,7 @@ export class AddCluster extends React.Component {
|
|||||||
@observable kubeContexts = observable.map<string, Option>();
|
@observable kubeContexts = observable.map<string, Option>();
|
||||||
@observable customConfig = "";
|
@observable customConfig = "";
|
||||||
@observable isWaiting = false;
|
@observable isWaiting = false;
|
||||||
|
@observable isCheckingInput = false;
|
||||||
@observable errorText: string;
|
@observable errorText: string;
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: {}) {
|
||||||
@ -80,14 +82,35 @@ export class AddCluster extends React.Component {
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
_refreshContexts = debounce(() => {
|
||||||
refreshContexts = debounce(() => {
|
runInAction(() => {
|
||||||
const { config, error } = loadConfigFromString(this.customConfig.trim() || "{}");
|
try {
|
||||||
|
const text = this.customConfig.trim();
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return this.kubeContexts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config, error } = loadConfigFromString(text);
|
||||||
|
|
||||||
this.kubeContexts.replace(getContexts(config));
|
this.kubeContexts.replace(getContexts(config));
|
||||||
this.errorText = error?.toString();
|
this.errorText = error?.toString();
|
||||||
|
} catch (error) {
|
||||||
|
this.kubeContexts.clear();
|
||||||
|
this.errorText = error?.toString() || "An error occured";
|
||||||
|
} finally {
|
||||||
|
this.isCheckingInput = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
refreshContexts = () => {
|
||||||
|
// Clear the kubeContexts immediately
|
||||||
|
this.isCheckingInput = true;
|
||||||
|
this.kubeContexts.clear();
|
||||||
|
this._refreshContexts();
|
||||||
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters = async () => {
|
addClusters = async () => {
|
||||||
this.isWaiting = true;
|
this.isWaiting = true;
|
||||||
@ -145,6 +168,7 @@ export class AddCluster extends React.Component {
|
|||||||
tooltip={this.kubeContexts.size === 0 || "Paste in at least one cluster to add."}
|
tooltip={this.kubeContexts.size === 0 || "Paste in at least one cluster to add."}
|
||||||
tooltipOverrideDisabled
|
tooltipOverrideDisabled
|
||||||
/>
|
/>
|
||||||
|
{this.isCheckingInput && <Spinner />}
|
||||||
</div>
|
</div>
|
||||||
</SettingLayout>
|
</SettingLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -55,6 +55,14 @@ export class HelmCharts extends Component<Props> {
|
|||||||
return helmChartStore.getByName(chartName, repo);
|
return helmChartStore.getByName(chartName, repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDetails = (chart: HelmChart) => {
|
||||||
|
if (chart === this.selectedChart) {
|
||||||
|
this.hideDetails();
|
||||||
|
} else {
|
||||||
|
this.showDetails(chart);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
showDetails = (chart: HelmChart) => {
|
showDetails = (chart: HelmChart) => {
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
navigation.push(helmChartsURL());
|
navigation.push(helmChartsURL());
|
||||||
@ -121,7 +129,7 @@ export class HelmCharts extends Component<Props> {
|
|||||||
{ className: "menu" }
|
{ className: "menu" }
|
||||||
]}
|
]}
|
||||||
detailsItem={this.selectedChart}
|
detailsItem={this.selectedChart}
|
||||||
onDetails={this.showDetails}
|
onDetails={this.onDetails}
|
||||||
/>
|
/>
|
||||||
{this.selectedChart && (
|
{this.selectedChart && (
|
||||||
<HelmChartDetails
|
<HelmChartDetails
|
||||||
|
|||||||
@ -75,6 +75,14 @@ export class HelmReleases extends Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDetails = (item: HelmRelease) => {
|
||||||
|
if (item === this.selectedRelease) {
|
||||||
|
this.hideDetails();
|
||||||
|
} else {
|
||||||
|
this.showDetails(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
showDetails = (item: HelmRelease) => {
|
showDetails = (item: HelmRelease) => {
|
||||||
navigation.push(releaseURL({
|
navigation.push(releaseURL({
|
||||||
params: {
|
params: {
|
||||||
@ -169,7 +177,7 @@ export class HelmReleases extends Component<Props> {
|
|||||||
message: this.renderRemoveDialogMessage(selectedItems)
|
message: this.renderRemoveDialogMessage(selectedItems)
|
||||||
})}
|
})}
|
||||||
detailsItem={this.selectedRelease}
|
detailsItem={this.selectedRelease}
|
||||||
onDetails={this.showDetails}
|
onDetails={this.onDetails}
|
||||||
/>
|
/>
|
||||||
<ReleaseDetails
|
<ReleaseDetails
|
||||||
release={this.selectedRelease}
|
release={this.selectedRelease}
|
||||||
|
|||||||
@ -43,7 +43,6 @@ import { catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
|||||||
import { CatalogMenu } from "./catalog-menu";
|
import { CatalogMenu } from "./catalog-menu";
|
||||||
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
import { RenderDelay } from "../render-delay/render-delay";
|
import { RenderDelay } from "../render-delay/render-delay";
|
||||||
import { TopBar } from "../layout/topbar";
|
|
||||||
|
|
||||||
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
|
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
|
||||||
|
|
||||||
@ -109,7 +108,11 @@ export class Catalog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDetails = (item: CatalogEntityItem<CatalogEntity>) => {
|
onDetails = (item: CatalogEntityItem<CatalogEntity>) => {
|
||||||
|
if (this.catalogEntityStore.selectedItemId === item.getId()) {
|
||||||
|
this.catalogEntityStore.selectedItemId = null;
|
||||||
|
} else {
|
||||||
this.catalogEntityStore.selectedItemId = item.getId();
|
this.catalogEntityStore.selectedItemId = item.getId();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
@ -245,8 +248,6 @@ export class Catalog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<TopBar/>
|
|
||||||
<MainLayout sidebar={this.renderNavigation()}>
|
<MainLayout sidebar={this.renderNavigation()}>
|
||||||
<div className="p-6 h-full">
|
<div className="p-6 h-full">
|
||||||
{ this.renderList() }
|
{ this.renderList() }
|
||||||
@ -266,7 +267,6 @@ export class Catalog extends React.Component<Props> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { boundMethod, cssNames, prevDefault } from "../../utils";
|
|||||||
import type { ItemObject } from "../../../common/item.store";
|
import type { ItemObject } from "../../../common/item.store";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { kubeSelectedUrlParam, showDetails } from "../kube-detail-params";
|
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
||||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
key={getId()}
|
key={getId()}
|
||||||
sortItem={warning}
|
sortItem={warning}
|
||||||
selected={selfLink === kubeSelectedUrlParam.get()}
|
selected={selfLink === kubeSelectedUrlParam.get()}
|
||||||
onClick={prevDefault(() => showDetails(selfLink))}
|
onClick={prevDefault(() => toggleDetails(selfLink))}
|
||||||
>
|
>
|
||||||
<TableCell className="message">
|
<TableCell className="message">
|
||||||
{message}
|
{message}
|
||||||
|
|||||||
@ -51,23 +51,6 @@ describe("<Welcome/>", () => {
|
|||||||
WelcomeBannerRegistry.resetInstance();
|
WelcomeBannerRegistry.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders items in the top bar", async () => {
|
|
||||||
const testId = "testId";
|
|
||||||
const text = "topBarItem";
|
|
||||||
|
|
||||||
TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
Item: () => <span data-testid={testId}>{text}</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<Welcome />);
|
|
||||||
|
|
||||||
expect(screen.getByTestId(testId)).toHaveTextContent(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
||||||
const testId = "testId";
|
const testId = "testId";
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import { Icon } from "../icon";
|
|||||||
import { productName, slackUrl } from "../../../common/vars";
|
import { productName, slackUrl } from "../../../common/vars";
|
||||||
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
||||||
import { WelcomeBannerRegistry } from "../../../extensions/registries";
|
import { WelcomeBannerRegistry } from "../../../extensions/registries";
|
||||||
import { TopBar } from "../layout/topbar";
|
|
||||||
|
|
||||||
export const defaultWidth = 320;
|
export const defaultWidth = 320;
|
||||||
|
|
||||||
@ -48,8 +47,6 @@ export class Welcome extends React.Component {
|
|||||||
}, defaultWidth);
|
}, defaultWidth);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<TopBar/>
|
|
||||||
<div className="flex justify-center Welcome align-center">
|
<div className="flex justify-center Welcome align-center">
|
||||||
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
|
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
|
||||||
{welcomeBanner.length > 0 ? (
|
{welcomeBanner.length > 0 ? (
|
||||||
@ -97,7 +94,6 @@ export class Welcome extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,6 @@
|
|||||||
--font-weight-normal: 400;
|
--font-weight-normal: 400;
|
||||||
--font-weight-bold: 500;
|
--font-weight-bold: 500;
|
||||||
--main-layout-header: 40px;
|
--main-layout-header: 40px;
|
||||||
--drag-region-height: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
@ -105,7 +104,7 @@ html, body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--drag-region-height);
|
height: var(--main-layout-header);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,11 @@
|
|||||||
|
|
||||||
.ClusterManager {
|
.ClusterManager {
|
||||||
--bottom-bar-height: 22px;
|
--bottom-bar-height: 22px;
|
||||||
|
--hotbar-width: 75px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"menu topbar"
|
"topbar topbar"
|
||||||
"menu main"
|
"menu main"
|
||||||
"bottom-bar bottom-bar";
|
"bottom-bar bottom-bar";
|
||||||
grid-template-rows: auto 1fr min-content;
|
grid-template-rows: auto 1fr min-content;
|
||||||
@ -48,7 +49,7 @@
|
|||||||
#lens-views {
|
#lens-views {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 40px; // Move below top bar
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
|||||||
import { reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
||||||
|
import { TopBar } from "../layout/topbar";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterManager extends React.Component {
|
export class ClusterManager extends React.Component {
|
||||||
@ -51,6 +52,7 @@ export class ClusterManager extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ClusterManager">
|
<div className="ClusterManager">
|
||||||
|
<TopBar/>
|
||||||
<main>
|
<main>
|
||||||
<div id="lens-views"/>
|
<div id="lens-views"/>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@ -34,7 +34,6 @@ import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
|||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
||||||
import { previousActiveTab } from "../+catalog";
|
import { previousActiveTab } from "../+catalog";
|
||||||
import { TopBar } from "../layout/topbar";
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -105,7 +104,6 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ClusterView flex column align-center">
|
<div className="ClusterView flex column align-center">
|
||||||
<TopBar/>
|
|
||||||
{this.renderStatus()}
|
{this.renderStatus()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,25 +19,18 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import { dumpYaml, KubeConfig } from "@kubernetes/client-node";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import tempy from "tempy";
|
|
||||||
import * as lockFile from "proper-lockfile";
|
import * as lockFile from "proper-lockfile";
|
||||||
import YAML from "json-to-pretty-yaml";
|
|
||||||
import { noop } from "../../utils";
|
|
||||||
|
|
||||||
export async function saveKubeconfig(config: KubeConfig, path: string) {
|
export async function saveKubeconfig(config: KubeConfig, path: string) {
|
||||||
const tmpFilePath = tempy.file();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const release = await lockFile.lock(path);
|
const release = await lockFile.lock(path);
|
||||||
const contents = YAML.stringify(JSON.parse(config.exportConfig()));
|
const contents = dumpYaml(JSON.parse(config.exportConfig()));
|
||||||
|
|
||||||
await fs.promises.writeFile(tmpFilePath, contents);
|
await fs.promises.writeFile(path, contents);
|
||||||
await fs.promises.rename(tmpFilePath, path);
|
await release();
|
||||||
release();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await fs.unlink(tmpFilePath, noop);
|
|
||||||
throw new Error(`Failed to acquire lock file.\n${e}`);
|
throw new Error(`Failed to acquire lock file.\n${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,15 +117,18 @@ export class TerminalStore extends Singleton {
|
|||||||
|
|
||||||
await when(() => this.connections.has(tab.id));
|
await when(() => this.connections.has(tab.id));
|
||||||
|
|
||||||
const rcIsFinished = when(() => this.connections.get(tab.id).shellRunCommandsFinished);
|
const shellIsReady = when(() => this.connections.get(tab.id).isReady);
|
||||||
const notifyVeryLong = setTimeout(() => {
|
const notifyVeryLong = setTimeout(() => {
|
||||||
rcIsFinished.cancel();
|
shellIsReady.cancel();
|
||||||
Notifications.info("Terminal shell is taking a long time to complete startup. Please check your .rc file. Bypassing shell completion check.", {
|
Notifications.info(
|
||||||
|
"If terminal shell is not ready please check your shell init files, if applicable.",
|
||||||
|
{
|
||||||
timeout: 4_000,
|
timeout: 4_000,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|
||||||
await rcIsFinished.catch(noop);
|
await shellIsReady.catch(noop);
|
||||||
clearTimeout(notifyVeryLong);
|
clearTimeout(notifyVeryLong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,13 +25,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: $clusterMenuBackground;
|
background: $clusterMenuBackground;
|
||||||
padding-top: 28px;
|
padding-top: 1px;
|
||||||
width: 75px;
|
width: var(--hotbar-width);
|
||||||
|
overflow: hidden;
|
||||||
.is-mac &:before {
|
|
||||||
content: "";
|
|
||||||
height: 4px; // extra spacing for mac-os "traffic-light" buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
.HotbarItems {
|
.HotbarItems {
|
||||||
--cellWidth: 40px;
|
--cellWidth: 40px;
|
||||||
|
|||||||
@ -41,6 +41,16 @@ export const kubeSelectedUrlParam = createPageParam({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function toggleDetails(selfLink: string, resetSelected = true) {
|
||||||
|
const current = kubeSelectedUrlParam.get() === selfLink;
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
hideDetails();
|
||||||
|
} else {
|
||||||
|
showDetails(selfLink, resetSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function showDetails(selfLink = "", resetSelected = true) {
|
export function showDetails(selfLink = "", resetSelected = true) {
|
||||||
const detailsUrl = getDetailsUrl(selfLink, resetSelected);
|
const detailsUrl = getDetailsUrl(selfLink, resetSelected);
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
|||||||
import { clusterContext } from "../context";
|
import { clusterContext } from "../context";
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
||||||
import { kubeSelectedUrlParam, showDetails } from "../kube-detail-params";
|
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
||||||
|
|
||||||
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
|
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
|
||||||
store: KubeObjectStore<K>;
|
store: KubeObjectStore<K>;
|
||||||
@ -39,7 +39,7 @@ export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<KubeObjectListLayoutProps<KubeObject>> = {
|
const defaultProps: Partial<KubeObjectListLayoutProps<KubeObject>> = {
|
||||||
onDetails: (item: KubeObject) => showDetails(item.selfLink),
|
onDetails: (item: KubeObject) => toggleDetails(item.selfLink),
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
|||||||
56
src/renderer/components/layout/sidebar.module.css
Normal file
56
src/renderer/components/layout/sidebar.module.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.sidebarNav {
|
||||||
|
@apply flex overflow-auto flex-col;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
padding-bottom: calc(var(--padding) * 3);
|
||||||
|
|
||||||
|
/* Shadow above scrolling content from https://gist.github.com/distinctgrey/7548778 */
|
||||||
|
background:
|
||||||
|
linear-gradient(var(--sidebarBackground) 30%, rgba(255,255,255,0)),
|
||||||
|
linear-gradient(rgba(255,255,255,0), var(--sidebarBackground) 70%) 0 100%,
|
||||||
|
radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)),
|
||||||
|
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 40px, 100% 40px, 100% 12px, 100% 12px;
|
||||||
|
background-attachment: local, local, scroll, scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarNav :global(.Icon) {
|
||||||
|
box-sizing: content-box;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cluster {
|
||||||
|
@apply flex items-center m-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clusterName {
|
||||||
|
@apply font-bold overflow-hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
display: -webkit-box;
|
||||||
|
/* Simulate text-overflow:ellipsis styles but for multiple text lines */
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./sidebar.scss";
|
import styles from "./sidebar.module.css";
|
||||||
import type { TabLayoutRoute } from "./tab-layout";
|
import type { TabLayoutRoute } from "./tab-layout";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -41,6 +41,7 @@ import { Apps } from "../+apps";
|
|||||||
import * as routes from "../../../common/routes";
|
import * as routes from "../../../common/routes";
|
||||||
import { Config } from "../+config";
|
import { Config } from "../+config";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -177,6 +178,29 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCluster() {
|
||||||
|
if (!this.clusterEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata, spec } = this.clusterEntity;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.cluster}>
|
||||||
|
<HotbarIcon
|
||||||
|
uid={metadata.uid}
|
||||||
|
title={metadata.name}
|
||||||
|
source={metadata.source}
|
||||||
|
src={spec.icon?.src}
|
||||||
|
className="mr-5"
|
||||||
|
/>
|
||||||
|
<div className={styles.clusterName}>
|
||||||
|
{metadata.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get clusterEntity() {
|
get clusterEntity() {
|
||||||
return catalogEntityRegistry.activeEntity;
|
return catalogEntityRegistry.activeEntity;
|
||||||
}
|
}
|
||||||
@ -185,13 +209,9 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
const { className } = this.props;
|
const { className } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames(Sidebar.displayName, "flex column", className)}>
|
<div className={cssNames("flex flex-col", className)} data-testid="cluster-sidebar">
|
||||||
{this.clusterEntity && (
|
{this.renderCluster()}
|
||||||
<div className="cluster-name">
|
<div className={styles.sidebarNav}>
|
||||||
{this.clusterEntity.metadata.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={cssNames("sidebar-nav flex column box grow-fixed")}>
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
id="cluster"
|
id="cluster"
|
||||||
text="Cluster"
|
text="Cluster"
|
||||||
|
|||||||
@ -30,6 +30,10 @@
|
|||||||
grid-area: topbar;
|
grid-area: topbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.is-mac) .topBar {
|
||||||
|
padding-left: var(--hotbar-width);
|
||||||
|
}
|
||||||
|
|
||||||
.history {
|
.history {
|
||||||
@apply flex items-center;
|
@apply flex items-center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
|
|||||||
<Icon
|
<Icon
|
||||||
data-testid="home-button"
|
data-testid="home-button"
|
||||||
material="home"
|
material="home"
|
||||||
className="ml-5"
|
className="ml-4"
|
||||||
onClick={goHome}
|
onClick={goHome}
|
||||||
disabled={isActiveRoute(catalogRoute)}
|
disabled={isActiveRoute(catalogRoute)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
28
yarn.lock
28
yarn.lock
@ -2147,6 +2147,11 @@
|
|||||||
anymatch "^3.0.0"
|
anymatch "^3.0.0"
|
||||||
source-map "^0.6.0"
|
source-map "^0.6.0"
|
||||||
|
|
||||||
|
"@types/which@^2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.1.tgz#27ecd67f915b7c3d6ba552135bb1eecd66e63501"
|
||||||
|
integrity sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==
|
||||||
|
|
||||||
"@types/ws@^6.0.1":
|
"@types/ws@^6.0.1":
|
||||||
version "6.0.4"
|
version "6.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
|
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
|
||||||
@ -4093,11 +4098,6 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
command-exists@1.2.9:
|
|
||||||
version "1.2.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
|
|
||||||
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
|
|
||||||
|
|
||||||
commander@2.9.0:
|
commander@2.9.0:
|
||||||
version "2.9.0"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
|
||||||
@ -8704,14 +8704,6 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||||
|
|
||||||
json-to-pretty-yaml@^1.2.2:
|
|
||||||
version "1.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz#f4cd0bd0a5e8fe1df25aaf5ba118b099fd992d5b"
|
|
||||||
integrity sha1-9M0L0KXo/h3yWq9boRiwmf2ZLVs=
|
|
||||||
dependencies:
|
|
||||||
remedial "^1.0.7"
|
|
||||||
remove-trailing-spaces "^1.0.6"
|
|
||||||
|
|
||||||
json3@^3.3.3:
|
json3@^3.3.3:
|
||||||
version "3.3.3"
|
version "3.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
|
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
|
||||||
@ -12306,21 +12298,11 @@ relateurl@^0.2.7:
|
|||||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||||
|
|
||||||
remedial@^1.0.7:
|
|
||||||
version "1.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0"
|
|
||||||
integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==
|
|
||||||
|
|
||||||
remove-trailing-separator@^1.0.1:
|
remove-trailing-separator@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||||
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
|
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
|
||||||
|
|
||||||
remove-trailing-spaces@^1.0.6:
|
|
||||||
version "1.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7"
|
|
||||||
integrity sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==
|
|
||||||
|
|
||||||
renderkid@^2.0.1:
|
renderkid@^2.0.1:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
|
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user