mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into fix/isolate-react-select-emotion-styles
This commit is contained in:
commit
5ada05940c
@ -225,6 +225,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
workspaceStore.setLastActiveClusterId(clusterId);
|
||||
}
|
||||
|
||||
deactivate(id: ClusterId) {
|
||||
if (this.isActive(id)) {
|
||||
this.setActive(null);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
||||
const clusters = this.getByWorkspaceId(workspace);
|
||||
|
||||
11
src/common/ipc/cluster.ipc.ts
Normal file
11
src/common/ipc/cluster.ipc.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This channel is broadcast on whenever the cluster fails to list namespaces
|
||||
* during a refresh and no `accessibleNamespaces` have been set.
|
||||
*/
|
||||
export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
|
||||
|
||||
export type ListNamespaceForbiddenArgs = [clusterId: string];
|
||||
|
||||
export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
|
||||
return args.length === 1 && typeof args[0] === "string";
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./ipc";
|
||||
export * from "./invalid-kubeconfig";
|
||||
export * from "./update-available";
|
||||
export * from "./update-available.ipc";
|
||||
export * from "./cluster.ipc";
|
||||
export * from "./type-enforced-ipc";
|
||||
|
||||
@ -3,10 +3,7 @@ import { UpdateInfo } from "electron-updater";
|
||||
export const UpdateAvailableChannel = "update-available";
|
||||
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
||||
|
||||
/**
|
||||
* [<back-channel>, <update-info>]
|
||||
*/
|
||||
export type UpdateAvailableFromMain = [string, UpdateInfo];
|
||||
export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo];
|
||||
|
||||
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
|
||||
if (args.length !== 2) {
|
||||
@ -32,7 +29,7 @@ export type BackchannelArg = {
|
||||
now: boolean;
|
||||
};
|
||||
|
||||
export type UpdateAvailableToBackchannel = [BackchannelArg];
|
||||
export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
|
||||
|
||||
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
|
||||
if (args.length !== 1) {
|
||||
@ -10,6 +10,7 @@ import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
||||
import { appEventBus } from "./event-bus";
|
||||
import logger from "../main/logger";
|
||||
import path from "path";
|
||||
import { fileNameMigration } from "../migrations/user-store";
|
||||
|
||||
export interface UserStoreModel {
|
||||
kubeConfigPath: string;
|
||||
@ -37,7 +38,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
// configName: "lens-user-store", // todo: migrate from default "config.json"
|
||||
configName: "lens-user-store",
|
||||
migrations,
|
||||
});
|
||||
|
||||
@ -85,6 +86,16 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
/**
|
||||
* This has to be here before the call to `new Config` in `super.load()`
|
||||
* as we have to make sure that file is in the expected place for that call
|
||||
*/
|
||||
await fileNameMigration();
|
||||
|
||||
return super.load();
|
||||
}
|
||||
|
||||
get isNewVersion() {
|
||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||
}
|
||||
|
||||
@ -85,8 +85,8 @@ describe("kubeconfig manager tests", () => {
|
||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||
|
||||
expect(logger.error).not.toBeCalled();
|
||||
expect(kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`);
|
||||
const file = await fse.readFile(kubeConfManager.getPath());
|
||||
expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`);
|
||||
const file = await fse.readFile(await kubeConfManager.getPath());
|
||||
const yml = loadYaml<any>(file.toString());
|
||||
|
||||
expect(yml["current-context"]).toBe("minikube");
|
||||
@ -104,12 +104,12 @@ describe("kubeconfig manager tests", () => {
|
||||
const contextHandler = new ContextHandler(cluster);
|
||||
const port = await getFreePort();
|
||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||
const configPath = kubeConfManager.getPath();
|
||||
const configPath = await kubeConfManager.getPath();
|
||||
|
||||
expect(await fse.pathExists(configPath)).toBe(true);
|
||||
await kubeConfManager.unlink();
|
||||
expect(await fse.pathExists(configPath)).toBe(false);
|
||||
await kubeConfManager.unlink(); // doesn't throw
|
||||
expect(kubeConfManager.getPath()).toBeUndefined();
|
||||
expect(await kubeConfManager.getPath()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,9 +4,9 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||
import type { WorkspaceId } from "../common/workspace-store";
|
||||
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc";
|
||||
import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
import { loadConfig, validateKubeConfig } from "../common/kube-helpers";
|
||||
@ -482,14 +482,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getProxyKubeconfig(): KubeConfig {
|
||||
return loadConfig(this.getProxyKubeconfigPath());
|
||||
async getProxyKubeconfig(): Promise<KubeConfig> {
|
||||
const kubeconfigPath = await this.getProxyKubeconfigPath();
|
||||
|
||||
return loadConfig(kubeconfigPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getProxyKubeconfigPath(): string {
|
||||
async getProxyKubeconfigPath(): Promise<string> {
|
||||
return this.kubeconfigManager.getPath();
|
||||
}
|
||||
|
||||
@ -565,7 +567,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @param resourceAttributes resource attributes
|
||||
*/
|
||||
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
||||
const authApi = (await this.getProxyKubeconfig()).makeApiClient(AuthorizationV1Api);
|
||||
|
||||
try {
|
||||
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||
@ -680,18 +682,22 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return this.accessibleNamespaces;
|
||||
}
|
||||
|
||||
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
|
||||
try {
|
||||
const namespaceList = await api.listNamespace();
|
||||
|
||||
return namespaceList.body.items.map(ns => ns.metadata.name);
|
||||
} catch (error) {
|
||||
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
||||
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
|
||||
const namespaceList = [ctx.namespace].filter(Boolean);
|
||||
|
||||
if (ctx.namespace) return [ctx.namespace];
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });
|
||||
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
|
||||
}
|
||||
|
||||
return [];
|
||||
return namespaceList;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ export class ContextHandler {
|
||||
async getPrometheusService(): Promise<PrometheusService> {
|
||||
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
||||
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
||||
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||
const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
|
||||
return await provider.getPrometheusService(apiClient);
|
||||
});
|
||||
|
||||
@ -56,11 +56,12 @@ export class HelmReleaseManager {
|
||||
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
|
||||
const helm = await helmCli.binaryPath();
|
||||
const fileName = tempy.file({name: "values.yaml"});
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||
|
||||
try {
|
||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
return {
|
||||
log: stdout,
|
||||
@ -73,7 +74,9 @@ export class HelmReleaseManager {
|
||||
|
||||
public async getRelease(name: string, namespace: string, cluster: Cluster) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
const release = JSON.parse(stdout);
|
||||
|
||||
release.resources = await this.getResources(name, namespace, cluster);
|
||||
@ -112,7 +115,7 @@ export class HelmReleaseManager {
|
||||
protected async getResources(name: string, namespace: string, cluster: Cluster) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const kubectl = await cluster.kubeCtl.getPath();
|
||||
const pathToKubeconfig = cluster.getProxyKubeconfigPath();
|
||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
|
||||
return { stdout: JSON.stringify({items: []})};
|
||||
});
|
||||
|
||||
@ -8,7 +8,9 @@ import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/h
|
||||
|
||||
class HelmService {
|
||||
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
||||
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath());
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async listCharts() {
|
||||
@ -53,8 +55,9 @@ class HelmService {
|
||||
|
||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||
await repoManager.init();
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
|
||||
return await releaseManager.listReleases(proxyKubeconfig, namespace);
|
||||
}
|
||||
|
||||
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
@ -64,21 +67,27 @@ class HelmService {
|
||||
}
|
||||
|
||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("Fetch release values");
|
||||
|
||||
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||
return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("Fetch release history");
|
||||
|
||||
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||
return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("Delete release");
|
||||
|
||||
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||
return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
||||
@ -88,8 +97,10 @@ class HelmService {
|
||||
}
|
||||
|
||||
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("Rollback release");
|
||||
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
|
||||
const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig);
|
||||
|
||||
return { message: output };
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import { LensProtocolRouterMain } from "./protocol-handler";
|
||||
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { bindBroadcastHandlers } from "../common/ipc";
|
||||
import { startUpdateChecking } from "./app-updater";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
let proxyPort: number;
|
||||
@ -162,7 +163,7 @@ app.on("ready", async () => {
|
||||
windowManager.initMainWindow();
|
||||
}
|
||||
|
||||
ipcMain.on("renderer:loaded", () => {
|
||||
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
||||
startUpdateChecking();
|
||||
LensProtocolRouterMain
|
||||
.getInstance<LensProtocolRouterMain>()
|
||||
|
||||
@ -24,14 +24,24 @@ export class KubeconfigManager {
|
||||
protected async init() {
|
||||
try {
|
||||
await this.contextHandler.ensurePort();
|
||||
await this.createProxyKubeconfig();
|
||||
this.tempFile = await this.createProxyKubeconfig();
|
||||
} catch (err) {
|
||||
logger.error(`Failed to created temp config for auth-proxy`, { err });
|
||||
}
|
||||
}
|
||||
|
||||
getPath() {
|
||||
async getPath() {
|
||||
// create proxy kubeconfig if it is removed
|
||||
if (this.tempFile !== undefined && !(await fs.pathExists(this.tempFile))) {
|
||||
try {
|
||||
this.tempFile = await this.createProxyKubeconfig();
|
||||
} catch (err) {
|
||||
logger.error(`Failed to created temp config for auth-proxy`, { err });
|
||||
}
|
||||
}
|
||||
|
||||
return this.tempFile;
|
||||
|
||||
}
|
||||
|
||||
protected resolveProxyUrl() {
|
||||
@ -71,9 +81,8 @@ export class KubeconfigManager {
|
||||
// write
|
||||
const configYaml = dumpConfigYaml(proxyConfig);
|
||||
|
||||
fs.ensureDir(path.dirname(tempFile));
|
||||
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
|
||||
this.tempFile = tempFile;
|
||||
await fs.ensureDir(path.dirname(tempFile));
|
||||
await fs.writeFile(tempFile, configYaml, { mode: 0o600 });
|
||||
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
||||
|
||||
return tempFile;
|
||||
|
||||
@ -17,10 +17,10 @@ export class NodeShellSession extends ShellSession {
|
||||
super(socket, cluster);
|
||||
this.nodeName = nodeName;
|
||||
this.podId = `node-shell-${uuid()}`;
|
||||
this.kc = cluster.getProxyKubeconfig();
|
||||
}
|
||||
|
||||
public async open() {
|
||||
this.kc = await this.cluster.getProxyKubeconfig();
|
||||
const shell = await this.kubectl.getPath();
|
||||
let args = [];
|
||||
|
||||
|
||||
@ -23,12 +23,13 @@ export class ResourceApplier {
|
||||
protected async kubectlApply(content: string): Promise<string> {
|
||||
const { kubeCtl } = this.cluster;
|
||||
const kubectlPath = await kubeCtl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const fileName = tempy.file({ name: "resource.yaml" });
|
||||
|
||||
fs.writeFileSync(fileName, content);
|
||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`;
|
||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${fileName}"`;
|
||||
|
||||
logger.debug(`shooting manifests with: ${cmd}`);
|
||||
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
|
||||
@ -54,6 +55,7 @@ export class ResourceApplier {
|
||||
public async kubectlApplyAll(resources: string[]): Promise<string> {
|
||||
const { kubeCtl } = this.cluster;
|
||||
const kubectlPath = await kubeCtl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tmpDir = tempy.directory();
|
||||
@ -62,7 +64,7 @@ export class ResourceApplier {
|
||||
resources.forEach((resource, index) => {
|
||||
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
|
||||
});
|
||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`;
|
||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${tmpDir}"`;
|
||||
|
||||
console.log("shooting manifests with:", cmd);
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
|
||||
@ -44,7 +44,7 @@ class KubeconfigRoute extends LensApi {
|
||||
|
||||
public async routeServiceAccountRoute(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request;
|
||||
const client = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
const secretList = await client.listNamespacedSecret(params.namespace);
|
||||
const secret = secretList.body.items.find(secret => {
|
||||
const { annotations } = secret.metadata;
|
||||
|
||||
@ -92,7 +92,7 @@ class PortForwardRoute extends LensApi {
|
||||
namespace,
|
||||
name: resourceName,
|
||||
port,
|
||||
kubeConfig: cluster.getProxyKubeconfigPath()
|
||||
kubeConfig: await cluster.getProxyKubeconfigPath()
|
||||
});
|
||||
const started = await portForward.start();
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import shellEnv from "shell-env";
|
||||
import { app } from "electron";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { Cluster } from "./cluster";
|
||||
import { ClusterPreferences } from "../common/cluster-store";
|
||||
import { helmCli } from "./helm/helm-cli";
|
||||
import { isWindows } from "../common/vars";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
@ -24,20 +23,18 @@ export class ShellSession extends EventEmitter {
|
||||
protected kubectlBinDir: string;
|
||||
protected kubectlPathDir: string;
|
||||
protected helmBinDir: string;
|
||||
protected preferences: ClusterPreferences;
|
||||
protected running = false;
|
||||
protected clusterId: string;
|
||||
protected cluster: Cluster;
|
||||
|
||||
constructor(socket: WebSocket, cluster: Cluster) {
|
||||
super();
|
||||
this.websocket = socket;
|
||||
this.kubeconfigPath = cluster.getProxyKubeconfigPath();
|
||||
this.kubectl = new Kubectl(cluster.version);
|
||||
this.preferences = cluster.preferences || {};
|
||||
this.clusterId = cluster.id;
|
||||
this.cluster = cluster;
|
||||
}
|
||||
|
||||
public async open() {
|
||||
this.kubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
this.kubectlBinDir = await this.kubectl.binDir();
|
||||
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath();
|
||||
|
||||
@ -65,11 +62,13 @@ export class ShellSession extends EventEmitter {
|
||||
}
|
||||
|
||||
protected cwd(): string {
|
||||
if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") {
|
||||
const { preferences } = this.cluster;
|
||||
|
||||
if(!preferences || !preferences.terminalCWD || preferences.terminalCWD === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.preferences.terminalCWD;
|
||||
return preferences.terminalCWD;
|
||||
}
|
||||
|
||||
protected async getShellArgs(shell: string): Promise<Array<string>> {
|
||||
@ -88,15 +87,17 @@ export class ShellSession extends EventEmitter {
|
||||
}
|
||||
|
||||
protected async getCachedShellEnv() {
|
||||
let env = ShellSession.shellEnvs.get(this.clusterId);
|
||||
const { id: clusterId } = this.cluster;
|
||||
|
||||
let env = ShellSession.shellEnvs.get(clusterId);
|
||||
|
||||
if (!env) {
|
||||
env = await this.getShellEnv();
|
||||
ShellSession.shellEnvs.set(this.clusterId, env);
|
||||
ShellSession.shellEnvs.set(clusterId, env);
|
||||
} else {
|
||||
// refresh env in the background
|
||||
this.getShellEnv().then((shellEnv: any) => {
|
||||
ShellSession.shellEnvs.set(this.clusterId, shellEnv);
|
||||
ShellSession.shellEnvs.set(clusterId, shellEnv);
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,6 +108,7 @@ export class ShellSession extends EventEmitter {
|
||||
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv())));
|
||||
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter);
|
||||
const shell = userStore.preferences.shell || process.env.SHELL || process.env.PTYSHELL;
|
||||
const { preferences } = this.cluster;
|
||||
|
||||
if(isWindows) {
|
||||
env["SystemRoot"] = process.env.SystemRoot;
|
||||
@ -138,8 +140,8 @@ export class ShellSession extends EventEmitter {
|
||||
env["TERM_PROGRAM"] = app.getName();
|
||||
env["TERM_PROGRAM_VERSION"] = app.getVersion();
|
||||
|
||||
if (this.preferences.httpsProxy) {
|
||||
env["HTTPS_PROXY"] = this.preferences.httpsProxy;
|
||||
if (preferences.httpsProxy) {
|
||||
env["HTTPS_PROXY"] = preferences.httpsProxy;
|
||||
}
|
||||
const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]];
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import { initMenu } from "./menu";
|
||||
import { initTray } from "./tray";
|
||||
import { Singleton } from "../common/utils";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import logger from "./logger";
|
||||
|
||||
export class WindowManager extends Singleton {
|
||||
@ -65,13 +66,13 @@ export class WindowManager extends Singleton {
|
||||
shell.openExternal(url);
|
||||
});
|
||||
this.mainWindow.webContents.on("dom-ready", () => {
|
||||
appEventBus.emit({name: "app", action: "dom-ready"});
|
||||
appEventBus.emit({ name: "app", action: "dom-ready" });
|
||||
});
|
||||
this.mainWindow.on("focus", () => {
|
||||
appEventBus.emit({name: "app", action: "focus"});
|
||||
appEventBus.emit({ name: "app", action: "focus" });
|
||||
});
|
||||
this.mainWindow.on("blur", () => {
|
||||
appEventBus.emit({name: "app", action: "blur"});
|
||||
appEventBus.emit({ name: "app", action: "blur" });
|
||||
});
|
||||
|
||||
// clean up
|
||||
@ -115,7 +116,7 @@ export class WindowManager extends Singleton {
|
||||
|
||||
protected bindEvents() {
|
||||
// track visible cluster from ui
|
||||
subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => {
|
||||
subscribeToBroadcast(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
|
||||
this.activeClusterId = clusterId;
|
||||
});
|
||||
}
|
||||
@ -139,7 +140,9 @@ export class WindowManager extends Singleton {
|
||||
await this.ensureMainWindow();
|
||||
|
||||
const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId);
|
||||
const channel = frameInfo ? "renderer:navigate-in-cluster" : "renderer:navigate";
|
||||
const channel = frameInfo
|
||||
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
|
||||
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
|
||||
|
||||
this.sendToView({
|
||||
channel,
|
||||
@ -152,7 +155,7 @@ export class WindowManager extends Singleton {
|
||||
const frameInfo = clusterFrameMap.get(this.activeClusterId);
|
||||
|
||||
if (frameInfo) {
|
||||
this.sendToView({ channel: "renderer:reload", frameInfo });
|
||||
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
||||
} else {
|
||||
webContents.getFocusedWebContents()?.reload();
|
||||
}
|
||||
|
||||
22
src/migrations/user-store/file-name-migration.ts
Normal file
22
src/migrations/user-store/file-name-migration.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import fse from "fs-extra";
|
||||
import { app, remote } from "electron";
|
||||
import path from "path";
|
||||
|
||||
export async function fileNameMigration() {
|
||||
const userDataPath = (app || remote.app).getPath("userData");
|
||||
const configJsonPath = path.join(userDataPath, "config.json");
|
||||
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
|
||||
|
||||
try {
|
||||
await fse.move(configJsonPath, lensUserStoreJsonPath);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT" && error.path === configJsonPath) { // (No such file or directory)
|
||||
return; // file already moved
|
||||
} else if (error.message === "dest already exists.") {
|
||||
await fse.remove(configJsonPath);
|
||||
} else {
|
||||
// pass other errors along
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,11 @@
|
||||
// User store migrations
|
||||
|
||||
import version210Beta4 from "./2.1.0-beta.4";
|
||||
import { fileNameMigration } from "./file-name-migration";
|
||||
|
||||
export {
|
||||
fileNameMigration
|
||||
};
|
||||
|
||||
export default {
|
||||
...version210Beta4,
|
||||
|
||||
@ -15,6 +15,7 @@ import { clusterStore } from "../../../common/cluster-store";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc";
|
||||
import { navigation } from "../../navigation";
|
||||
|
||||
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
||||
}
|
||||
@ -30,6 +31,10 @@ export class ClusterSettings extends React.Component<Props> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { hash } = navigation.location;
|
||||
|
||||
document.getElementById(hash.slice(1))?.scrollIntoView();
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.cluster, this.refreshCluster, {
|
||||
fireImmediately: true,
|
||||
|
||||
@ -16,7 +16,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Accessible Namespaces" />
|
||||
<SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
|
||||
<p>This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.</p>
|
||||
<EditableList
|
||||
placeholder="Add new namespace..."
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
*:target {
|
||||
color: $textColorAccent;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%; // 1 rem == 10px
|
||||
color: $textColorPrimary;
|
||||
|
||||
51
src/renderer/components/cluster-manager/cluster-actions.tsx
Normal file
51
src/renderer/components/cluster-manager/cluster-actions.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { clusterSettingsURL } from "../+cluster-settings";
|
||||
import { landingURL } from "../+landing-page";
|
||||
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { broadcastMessage, requestMain } from "../../../common/ipc";
|
||||
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { Tooltip } from "../../components//tooltip";
|
||||
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||
|
||||
const navigate = (route: string) =>
|
||||
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, route);
|
||||
|
||||
/**
|
||||
* Creates handlers for high-level actions
|
||||
* that could be performed on an individual cluster
|
||||
* @param cluster Cluster
|
||||
*/
|
||||
export const ClusterActions = (cluster: Cluster) => ({
|
||||
showSettings: () => navigate(clusterSettingsURL({
|
||||
params: { clusterId: cluster.id }
|
||||
})),
|
||||
disconnect: async () => {
|
||||
clusterStore.deactivate(cluster.id);
|
||||
navigate(landingURL());
|
||||
await requestMain(clusterDisconnectHandler, cluster.id);
|
||||
},
|
||||
remove: () => {
|
||||
const tooltipId = uniqueId("tooltip_target_");
|
||||
|
||||
return ConfirmDialog.open({
|
||||
okButtonProps: {
|
||||
primary: false,
|
||||
accent: true,
|
||||
label: "Remove"
|
||||
},
|
||||
ok: () => {
|
||||
clusterStore.deactivate(cluster.id);
|
||||
clusterStore.removeById(cluster.id);
|
||||
navigate(landingURL());
|
||||
},
|
||||
message: <p>
|
||||
Are you sure want to remove cluster <b id={tooltipId}>{cluster.name}</b>?
|
||||
<Tooltip targetId={tooltipId}>{cluster.id}</Tooltip>
|
||||
</p>
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -2,7 +2,6 @@ import "./clusters-menu.scss";
|
||||
|
||||
import React from "react";
|
||||
import { remote } from "electron";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||
import { observer } from "mobx-react";
|
||||
@ -13,12 +12,10 @@ import { Icon } from "../icon";
|
||||
import { autobind, cssNames, IClassName } from "../../utils";
|
||||
import { isActiveRoute, navigate } from "../../navigation";
|
||||
import { addClusterURL } from "../+add-cluster";
|
||||
import { clusterSettingsURL } from "../+cluster-settings";
|
||||
import { landingURL } from "../+landing-page";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { clusterViewURL } from "./cluster-view.route";
|
||||
import { ClusterActions } from "./cluster-actions";
|
||||
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
||||
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
|
||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { CommandOverlay } from "../command-palette/command-container";
|
||||
import { computed, observable } from "mobx";
|
||||
@ -40,51 +37,24 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
showContextMenu = (cluster: Cluster) => {
|
||||
const { Menu, MenuItem } = remote;
|
||||
const menu = new Menu();
|
||||
const actions = ClusterActions(cluster);
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: `Settings`,
|
||||
click: () => {
|
||||
navigate(clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: cluster.id
|
||||
}
|
||||
}));
|
||||
}
|
||||
click: actions.showSettings
|
||||
}));
|
||||
|
||||
if (cluster.online) {
|
||||
menu.append(new MenuItem({
|
||||
label: `Disconnect`,
|
||||
click: async () => {
|
||||
if (clusterStore.isActive(cluster.id)) {
|
||||
navigate(landingURL());
|
||||
clusterStore.setActive(null);
|
||||
}
|
||||
await requestMain(clusterDisconnectHandler, cluster.id);
|
||||
}
|
||||
click: actions.disconnect
|
||||
}));
|
||||
}
|
||||
|
||||
if (!cluster.isManaged) {
|
||||
menu.append(new MenuItem({
|
||||
label: `Remove`,
|
||||
click: () => {
|
||||
ConfirmDialog.open({
|
||||
okButtonProps: {
|
||||
primary: false,
|
||||
accent: true,
|
||||
label: `Remove`,
|
||||
},
|
||||
ok: () => {
|
||||
if (clusterStore.activeClusterId === cluster.id) {
|
||||
navigate(landingURL());
|
||||
clusterStore.setActive(null);
|
||||
}
|
||||
clusterStore.removeById(cluster.id);
|
||||
},
|
||||
message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>,
|
||||
});
|
||||
}
|
||||
click: actions.remove
|
||||
}));
|
||||
}
|
||||
menu.popup({
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./cluster-manager";
|
||||
export * from "./cluster-actions";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import "./edit-resource.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable, when } from "mobx";
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import jsYaml from "js-yaml";
|
||||
import { IDockTab } from "./dock.store";
|
||||
@ -11,6 +11,7 @@ import { InfoPanel } from "./info-panel";
|
||||
import { Badge } from "../badge";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { Spinner } from "../spinner";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -21,14 +22,6 @@ interface Props {
|
||||
export class EditResource extends React.Component<Props> {
|
||||
@observable error = "";
|
||||
|
||||
async componentDidMount() {
|
||||
await when(() => this.isReady);
|
||||
|
||||
if (!this.tabData.draft) {
|
||||
this.saveDraft(this.resource); // make initial dump to editor
|
||||
}
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
@ -37,20 +30,32 @@ export class EditResource extends React.Component<Props> {
|
||||
return editResourceStore.isReady(this.tabId);
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
return editResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get resource() {
|
||||
get resource(): KubeObject | undefined {
|
||||
return editResourceStore.getResource(this.tabId);
|
||||
}
|
||||
|
||||
saveDraft(draft: string | object) {
|
||||
@computed get draft(): string {
|
||||
if (!this.isReady) {
|
||||
return ""; // wait until tab's data and kube-object resource are loaded
|
||||
}
|
||||
|
||||
const { draft } = editResourceStore.getData(this.tabId);
|
||||
|
||||
if (typeof draft === "string") {
|
||||
return draft;
|
||||
}
|
||||
|
||||
return jsYaml.dump(this.resource); // dump resource first time
|
||||
}
|
||||
|
||||
@action
|
||||
saveDraft(draft: string | KubeObject) {
|
||||
if (typeof draft === "object") {
|
||||
draft = draft ? jsYaml.dump(draft) : undefined;
|
||||
}
|
||||
|
||||
editResourceStore.setData(this.tabId, {
|
||||
...this.tabData,
|
||||
...editResourceStore.getData(this.tabId),
|
||||
draft,
|
||||
});
|
||||
}
|
||||
@ -64,9 +69,8 @@ export class EditResource extends React.Component<Props> {
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
const { draft } = this.tabData;
|
||||
const store = editResourceStore.getStore(this.tabId);
|
||||
const updatedResource = await store.update(this.resource, jsYaml.safeLoad(draft));
|
||||
const updatedResource = await store.update(this.resource, jsYaml.safeLoad(this.draft));
|
||||
|
||||
this.saveDraft(updatedResource); // update with new resourceVersion to avoid further errors on save
|
||||
const resourceType = updatedResource.kind;
|
||||
@ -80,14 +84,12 @@ export class EditResource extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.isReady) {
|
||||
const { tabId, error, onChange, save, draft, isReady, resource } = this;
|
||||
|
||||
if (!isReady) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
const { tabId, error, onChange, save } = this;
|
||||
const { kind, getNs, getName } = this.resource;
|
||||
const { draft } = this.tabData;
|
||||
|
||||
return (
|
||||
<div className={cssNames("EditResource flex column", this.props.className)}>
|
||||
<InfoPanel
|
||||
@ -98,9 +100,9 @@ export class EditResource extends React.Component<Props> {
|
||||
submittingMessage="Applying.."
|
||||
controls={(
|
||||
<div className="resource-info flex gaps align-center">
|
||||
<span>Kind:</span> <Badge label={kind}/>
|
||||
<span>Name:</span><Badge label={getName()}/>
|
||||
<span>Namespace:</span> <Badge label={getNs() || "global"}/>
|
||||
<span>Kind:</span> <Badge label={resource.kind}/>
|
||||
<span>Name:</span><Badge label={resource.getName()}/>
|
||||
<span>Namespace:</span> <Badge label={resource.getNs() || "global"}/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -7,15 +7,19 @@ import "@testing-library/jest-dom/extend-expect";
|
||||
import { MainLayoutHeader } from "../main-layout-header";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { workspaceStore } from "../../../../common/workspace-store";
|
||||
import { broadcastMessage } from "../../../../common/ipc";
|
||||
import { broadcastMessage, requestMain } from "../../../../common/ipc";
|
||||
import { clusterDisconnectHandler } from "../../../../common/cluster-ipc";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { IpcRendererNavigationEvents } from "../../../navigation/events";
|
||||
|
||||
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
|
||||
const mockRequestMain = requestMain as jest.MockedFunction<typeof requestMain>;
|
||||
|
||||
const cluster: Cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
workspace: workspaceStore.currentWorkspaceId,
|
||||
});
|
||||
|
||||
describe("<MainLayoutHeader />", () => {
|
||||
@ -25,20 +29,12 @@ describe("<MainLayoutHeader />", () => {
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("renders gear icon", () => {
|
||||
it("renders three dots icon", () => {
|
||||
const { container } = render(<MainLayoutHeader cluster={cluster} />);
|
||||
const icon = container.querySelector(".Icon .icon");
|
||||
|
||||
expect(icon).toBeInstanceOf(HTMLElement);
|
||||
expect(icon).toHaveTextContent("settings");
|
||||
});
|
||||
|
||||
it("navigates to cluster settings", () => {
|
||||
const { container } = render(<MainLayoutHeader cluster={cluster} />);
|
||||
const icon = container.querySelector(".Icon");
|
||||
|
||||
fireEvent.click(icon);
|
||||
expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings");
|
||||
expect(icon).toHaveTextContent("more_vert");
|
||||
});
|
||||
|
||||
it("renders cluster name", () => {
|
||||
@ -46,4 +42,60 @@ describe("<MainLayoutHeader />", () => {
|
||||
|
||||
expect(getByText("minikube")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Cluster Actions Menu", () => {
|
||||
let settingsBtn: Element;
|
||||
let disconnectBtn: Element;
|
||||
let removeBtn: Element;
|
||||
|
||||
beforeEach(() => {
|
||||
const { container } = render(<div>
|
||||
<MainLayoutHeader cluster={cluster} />
|
||||
<ConfirmDialog />
|
||||
</div>);
|
||||
const icon = container.querySelector(".Icon");
|
||||
|
||||
cluster.online = true;
|
||||
fireEvent.click(icon);
|
||||
|
||||
[settingsBtn, disconnectBtn, removeBtn] = Array.from(document.querySelectorAll("ul.ClusterActionsMenu > li"))
|
||||
.map(el => el.querySelector("span"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cluster.online = false;
|
||||
});
|
||||
|
||||
it("renders cluster menu items", () => {
|
||||
expect(settingsBtn).toBeDefined();
|
||||
expect(settingsBtn.textContent).toBe("Settings");
|
||||
expect(disconnectBtn).toBeDefined();
|
||||
expect(disconnectBtn.textContent).toBe("Disconnect");
|
||||
expect(removeBtn).toBeDefined();
|
||||
expect(removeBtn.textContent).toBe("Remove");
|
||||
});
|
||||
|
||||
it("navigates to cluster settings", () => {
|
||||
fireEvent.click(settingsBtn);
|
||||
expect(mockBroadcastIpc).toBeCalledWith(IpcRendererNavigationEvents.NAVIGATE_IN_APP, "/cluster/foo/settings");
|
||||
});
|
||||
|
||||
it("disconnects from cluster", () => {
|
||||
fireEvent.click(disconnectBtn);
|
||||
expect(mockRequestMain).toBeCalledWith(clusterDisconnectHandler, cluster.id);
|
||||
});
|
||||
|
||||
it("opens 'Remove cluster' dialog", async () => {
|
||||
fireEvent.click(removeBtn);
|
||||
|
||||
const dialog = document.querySelector(".ConfirmDialog");
|
||||
|
||||
expect(dialog).toBeDefined();
|
||||
expect(dialog).not.toBe(null);
|
||||
|
||||
const okBtn = dialog.querySelector("button.ok");
|
||||
|
||||
expect(okBtn.textContent).toBe("Remove");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { clusterSettingsURL } from "../+cluster-settings";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { ClusterActions } from "../cluster-manager";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { MenuActions, MenuItem } from "../menu";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster
|
||||
@ -13,21 +12,28 @@ interface Props {
|
||||
}
|
||||
|
||||
export const MainLayoutHeader = observer(({ cluster, className }: Props) => {
|
||||
const actions = ClusterActions(cluster);
|
||||
const renderMenu = () => (
|
||||
<MenuActions autoCloseOnSelect className="ClusterActionsMenu">
|
||||
<MenuItem onClick={actions.showSettings}>
|
||||
<span>Settings</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={actions.disconnect}>
|
||||
<span>Disconnect</span>
|
||||
</MenuItem>
|
||||
{
|
||||
!cluster.isManaged && (
|
||||
<MenuItem onClick={actions.remove}>
|
||||
<span>Remove</span>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
</MenuActions>);
|
||||
|
||||
return (
|
||||
<header className={cssNames("flex gaps align-center justify-space-between", className)}>
|
||||
<span className="cluster">{cluster.name}</span>
|
||||
<Icon
|
||||
material="settings"
|
||||
tooltip="Open cluster settings"
|
||||
interactive
|
||||
onClick={() => {
|
||||
broadcastMessage("renderer:navigate", clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: cluster.id
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{renderMenu()}
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
@ -6,19 +6,18 @@ interface Props {
|
||||
className?: string;
|
||||
title: React.ReactNode;
|
||||
compact?: boolean; // no bottom padding
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export class SubTitle extends React.Component<Props> {
|
||||
render() {
|
||||
const { compact, title, children } = this.props;
|
||||
let { className } = this.props;
|
||||
|
||||
className = cssNames("SubTitle", className, {
|
||||
const { className, compact, title, children, id } = this.props;
|
||||
const classNames = cssNames("SubTitle", className, {
|
||||
compact,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={classNames} id={id}>
|
||||
{title} {children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import { ipcRenderer, IpcRendererEvent } from "electron";
|
||||
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc";
|
||||
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs } from "../../common/ipc";
|
||||
import { Notifications, notificationsStore } from "../components/notifications";
|
||||
import { Button } from "../components/button";
|
||||
import { isMac } from "../../common/vars";
|
||||
import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler";
|
||||
import { clusterStore } from "../../common/cluster-store";
|
||||
import { navigate } from "../navigation";
|
||||
import { clusterSettingsURL } from "../components/+cluster-settings";
|
||||
|
||||
function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
|
||||
notificationsStore.remove(notificationId);
|
||||
@ -42,7 +45,8 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
|
||||
<Button active outlined label="No" onClick={() => sendToBackchannel(backchannel, notificationId, { doUpdate: false })} />
|
||||
</div>
|
||||
</div>
|
||||
), {
|
||||
),
|
||||
{
|
||||
id: notificationId,
|
||||
onClose() {
|
||||
sendToBackchannel(backchannel, notificationId, { doUpdate: false });
|
||||
@ -51,6 +55,42 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
|
||||
);
|
||||
}
|
||||
|
||||
const listNamespacesForbiddenHandlerDisplayedAt = new Map<string, number>();
|
||||
const intervalBetweenNotifications = 1000 * 60; // 60s
|
||||
|
||||
function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void {
|
||||
const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId);
|
||||
const wasDisplayed = Boolean(lastDisplayedAt);
|
||||
const now = Date.now();
|
||||
|
||||
if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) {
|
||||
listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now);
|
||||
} else {
|
||||
// don't bother the user too often
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationId = `list-namespaces-forbidden:${clusterId}`;
|
||||
|
||||
Notifications.info(
|
||||
(
|
||||
<div className="flex column gaps">
|
||||
<b>Add Accessible Namespaces</b>
|
||||
<p>Cluster <b>{clusterStore.active.name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
|
||||
<div className="flex gaps row align-left box grow">
|
||||
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={()=> {
|
||||
navigate(clusterSettingsURL({ params: { clusterId }, fragment: "accessible-namespaces" }));
|
||||
notificationsStore.remove(notificationId);
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
id: notificationId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function registerIpcHandlers() {
|
||||
onCorrect({
|
||||
source: ipcRenderer,
|
||||
@ -59,4 +99,10 @@ export function registerIpcHandlers() {
|
||||
verifier: areArgsUpdateAvailableFromMain,
|
||||
});
|
||||
onCorrect(invalidKubeconfigHandler);
|
||||
onCorrect({
|
||||
source: ipcRenderer,
|
||||
channel: ClusterListNamespaceForbiddenChannel,
|
||||
listener: ListNamespacesForbiddenHandler,
|
||||
verifier: isListNamespaceForbiddenArgs,
|
||||
});
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import { LensProtocolRouterRenderer, bindProtocolAddRouteHandlers } from "./prot
|
||||
import { registerIpcHandlers } from "./ipc";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { IpcRendererNavigationEvents } from "./navigation/events";
|
||||
import createCache from "@emotion/cache";
|
||||
|
||||
@observer
|
||||
@ -32,7 +33,7 @@ export class LensApp extends React.Component {
|
||||
});
|
||||
|
||||
registerIpcHandlers();
|
||||
ipcRenderer.send("renderer:loaded");
|
||||
ipcRenderer.send(IpcRendererNavigationEvents.LOADED);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@ -4,6 +4,14 @@ import { getMatchedClusterId, navigate } from "./helpers";
|
||||
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export const enum IpcRendererNavigationEvents {
|
||||
RELOAD_PAGE = "renderer:page-reload",
|
||||
CLUSTER_VIEW_CURRENT_ID = "renderer:cluster-id-of-active-view",
|
||||
NAVIGATE_IN_APP = "renderer:navigate",
|
||||
NAVIGATE_IN_CLUSTER = "renderer:navigate-in-cluster",
|
||||
LOADED = "renderer:loaded",
|
||||
}
|
||||
|
||||
export function bindEvents() {
|
||||
if (!ipcRenderer) {
|
||||
return;
|
||||
@ -16,7 +24,7 @@ export function bindEvents() {
|
||||
}
|
||||
|
||||
// Reload dashboard window
|
||||
subscribeToBroadcast("renderer:reload", () => {
|
||||
subscribeToBroadcast(IpcRendererNavigationEvents.RELOAD_PAGE, () => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
@ -25,13 +33,13 @@ export function bindEvents() {
|
||||
function bindClusterManagerRouteEvents() {
|
||||
// Keep track of active cluster-id for handling IPC/menus/etc.
|
||||
reaction(() => getMatchedClusterId(), clusterId => {
|
||||
broadcastMessage("cluster-view:current-id", clusterId);
|
||||
broadcastMessage(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, clusterId);
|
||||
}, {
|
||||
fireImmediately: true
|
||||
});
|
||||
|
||||
// Handle navigation via IPC
|
||||
subscribeToBroadcast("renderer:navigate", (event, url: string) => {
|
||||
subscribeToBroadcast(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => {
|
||||
logger.info(`[IPC]: ${event.type}: ${url}`, { currentLocation: location.href });
|
||||
navigate(url);
|
||||
});
|
||||
@ -39,7 +47,7 @@ function bindClusterManagerRouteEvents() {
|
||||
|
||||
// Handle cluster-view renderer process events within iframes
|
||||
function bindClusterFrameRouteEvents() {
|
||||
subscribeToBroadcast("renderer:navigate-cluster-view", (event, url: string) => {
|
||||
subscribeToBroadcast(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER, (event, url: string) => {
|
||||
logger.info(`[IPC]: ${event.type}: ${url}`, { currentLocation: location.href });
|
||||
navigate(url);
|
||||
});
|
||||
|
||||
@ -31,23 +31,14 @@ metricRelabelings:
|
||||
|
||||
### Jsonnet
|
||||
|
||||
The required label replacements are bundled in [jsonnet/custom-prometheus](../jsonnet/custom-prometheus.jsonnet). To install it copy the file or use
|
||||
[Jsonnet Bundler](https://github.com/jsonnet-bundler/jsonnet-bundler). For jsonnet bundler add the following dependency to your `jsonnetfile.json`:
|
||||
The required label replacements are bundled in [jsonnet/lens/custom-prometheus](../jsonnet/lens/custom-prometheus.jsonnet). To install it copy the file or use
|
||||
[Jsonnet Bundler](https://github.com/jsonnet-bundler/jsonnet-bundler).
|
||||
|
||||
```
|
||||
{
|
||||
"name": "lens",
|
||||
"source": {
|
||||
"git": {
|
||||
"remote": "https://github.com/lensapp/lens",
|
||||
"subdir": "jsonnet"
|
||||
}
|
||||
},
|
||||
"version": "master"
|
||||
}
|
||||
```bash
|
||||
jb init && jb install https://github.com/lensapp/lens/jsonnet/lens@master
|
||||
```
|
||||
|
||||
and run `jb install`. When the installation was successful include it into your definitions. Using the [example](https://github.com/coreos/kube-prometheus#compiling)
|
||||
When the installation was successful include it into your definitions. Using the [example](https://github.com/coreos/kube-prometheus#compiling)
|
||||
of kube-prometheus, e.g.:
|
||||
|
||||
```
|
||||
|
||||
Loading…
Reference in New Issue
Block a user