mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix cluster frame display issue
- Add some defensive code to prevent this sort of infinite loop - Add some unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
96ed99a06b
commit
5dca04a12d
@ -7,14 +7,13 @@ import fs from "fs";
|
|||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import type { Cluster } from "../cluster/cluster";
|
|
||||||
import type { ClusterStore } from "../cluster-store/cluster-store";
|
import type { ClusterStore } from "../cluster-store/cluster-store";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||||
import type { ClusterModel } from "../cluster-types";
|
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
|
||||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||||
@ -25,17 +24,19 @@ import directoryForTempInjectable from "../app-paths/directory-for-temp/director
|
|||||||
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
|
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
|
||||||
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
|
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
|
||||||
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
|
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
|
||||||
|
import fsInjectable from "../fs/fs.injectable";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
const testDataIcon = fs.readFileSync(
|
const testDataIcon = fs.readFileSync(
|
||||||
"test-data/cluster-store-migration-icon.png",
|
"test-data/cluster-store-migration-icon.png",
|
||||||
);
|
);
|
||||||
|
const clusterServerUrl = "https://localhost";
|
||||||
const kubeconfig = `
|
const kubeconfig = `
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
server: https://localhost
|
server: ${clusterServerUrl}
|
||||||
name: test
|
name: test
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
||||||
@ -78,7 +79,7 @@ jest.mock("electron", () => ({
|
|||||||
describe("cluster-store", () => {
|
describe("cluster-store", () => {
|
||||||
let mainDi: DiContainer;
|
let mainDi: DiContainer;
|
||||||
let clusterStore: ClusterStore;
|
let clusterStore: ClusterStore;
|
||||||
let createCluster: (model: ClusterModel) => Cluster;
|
let createCluster: CreateCluster;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
@ -94,6 +95,7 @@ describe("cluster-store", () => {
|
|||||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||||
mainDi.permitSideEffects(appVersionInjectable);
|
mainDi.permitSideEffects(appVersionInjectable);
|
||||||
mainDi.permitSideEffects(clusterStoreInjectable);
|
mainDi.permitSideEffects(clusterStoreInjectable);
|
||||||
|
mainDi.permitSideEffects(fsInjectable);
|
||||||
|
|
||||||
mainDi.unoverride(clusterStoreInjectable);
|
mainDi.unoverride(clusterStoreInjectable);
|
||||||
});
|
});
|
||||||
@ -143,6 +145,8 @@ describe("cluster-store", () => {
|
|||||||
getCustomKubeConfigDirectory("foo"),
|
getCustomKubeConfigDirectory("foo"),
|
||||||
kubeconfig,
|
kubeconfig,
|
||||||
),
|
),
|
||||||
|
}, {
|
||||||
|
clusterServerUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
clusterStore.addCluster(cluster);
|
clusterStore.addCluster(cluster);
|
||||||
|
|||||||
@ -16,13 +16,17 @@ import { disposer, toJS } from "../utils";
|
|||||||
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
|
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
|
||||||
import { requestInitialClusterStates } from "../../renderer/ipc";
|
import { requestInitialClusterStates } from "../../renderer/ipc";
|
||||||
import { clusterStates } from "../ipc/cluster";
|
import { clusterStates } from "../ipc/cluster";
|
||||||
|
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
|
||||||
|
import { loadConfigFromString, validateKubeConfig } from "../kube-helpers";
|
||||||
|
import type { ReadFileSync } from "../fs/read-file-sync.injectable";
|
||||||
|
|
||||||
export interface ClusterStoreModel {
|
export interface ClusterStoreModel {
|
||||||
clusters?: ClusterModel[];
|
clusters?: ClusterModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
createCluster: (model: ClusterModel) => Cluster;
|
createCluster: CreateCluster;
|
||||||
|
readFileSync: ReadFileSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
@ -111,12 +115,24 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createNewCluster(model: ClusterModel): Cluster {
|
||||||
|
const kubeConfigData = this.dependencies.readFileSync(model.kubeConfigPath);
|
||||||
|
const { config } = loadConfigFromString(kubeConfigData);
|
||||||
|
const result = validateKubeConfig(config, model.contextName);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dependencies.createCluster(model, { clusterServerUrl: result.cluster.server });
|
||||||
|
}
|
||||||
|
|
||||||
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||||
appEventBus.emit({ name: "cluster", action: "add" });
|
appEventBus.emit({ name: "cluster", action: "add" });
|
||||||
|
|
||||||
const cluster = clusterOrModel instanceof Cluster
|
const cluster = clusterOrModel instanceof Cluster
|
||||||
? clusterOrModel
|
? clusterOrModel
|
||||||
: this.dependencies.createCluster(clusterOrModel);
|
: this.createNewCluster(clusterOrModel);
|
||||||
|
|
||||||
this.clusters.set(cluster.id, cluster);
|
this.clusters.set(cluster.id, cluster);
|
||||||
|
|
||||||
@ -136,7 +152,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.updateModel(clusterModel);
|
cluster.updateModel(clusterModel);
|
||||||
} else {
|
} else {
|
||||||
cluster = this.dependencies.createCluster(clusterModel);
|
cluster = this.createNewCluster(clusterModel);
|
||||||
}
|
}
|
||||||
newClusters.set(clusterModel.id, cluster);
|
newClusters.set(clusterModel.id, cluster);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON serializable metadata type
|
* JSON serializable metadata type
|
||||||
*/
|
*/
|
||||||
@ -67,6 +68,15 @@ export interface ClusterModel {
|
|||||||
labels?: Record<string, string>;
|
labels?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This data is retreived from the kubeconfig file before calling the cluster constructor.
|
||||||
|
*
|
||||||
|
* That is done to remove the external dependency on the construction of Cluster instances.
|
||||||
|
*/
|
||||||
|
export interface ClusterConfigData {
|
||||||
|
clusterServerUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The complete set of cluster settings or preferences
|
* The complete set of cluster settings or preferences
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,13 +10,13 @@ import type { KubeConfig } from "@kubernetes/client-node";
|
|||||||
import { HttpError } from "@kubernetes/client-node";
|
import { HttpError } from "@kubernetes/client-node";
|
||||||
import type { Kubectl } from "../../main/kubectl/kubectl";
|
import type { Kubectl } from "../../main/kubectl/kubectl";
|
||||||
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
|
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
|
||||||
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
|
import { loadConfigFromFile } from "../kube-helpers";
|
||||||
import type { KubeApiResource, KubeResource } from "../rbac";
|
import type { KubeApiResource, KubeResource } from "../rbac";
|
||||||
import { apiResourceRecord, apiResources } from "../rbac";
|
import { apiResourceRecord, apiResources } from "../rbac";
|
||||||
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
||||||
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
|
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
|
||||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
||||||
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
||||||
import type { Response } from "request";
|
import type { Response } from "request";
|
||||||
@ -236,27 +236,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return this.preferences.defaultNamespace;
|
return this.preferences.defaultNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel) {
|
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel, configData: ClusterConfigData) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.id = model.id;
|
this.id = model.id;
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
|
this.apiUrl = configData.clusterServerUrl;
|
||||||
const { config } = loadConfigFromFileSync(this.kubeConfigPath);
|
|
||||||
const validationError = validateKubeConfig(config, this.contextName);
|
|
||||||
|
|
||||||
if (validationError) {
|
|
||||||
throw validationError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = config.getContextObject(this.contextName);
|
|
||||||
|
|
||||||
assert(context);
|
|
||||||
|
|
||||||
const cluster = config.getCluster(context.cluster);
|
|
||||||
|
|
||||||
assert(cluster);
|
|
||||||
|
|
||||||
this.apiUrl = cluster.server;
|
|
||||||
|
|
||||||
// for the time being, until renderer gets its own cluster type
|
// for the time being, until renderer gets its own cluster type
|
||||||
this._contextHandler = this.dependencies.createContextHandler(this);
|
this._contextHandler = this.dependencies.createContextHandler(this);
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import type { ClusterModel } from "../cluster-types";
|
import type { ClusterConfigData, ClusterModel } from "../cluster-types";
|
||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
|
|
||||||
export type CreateCluster = (model: ClusterModel) => Cluster;
|
export type CreateCluster = (model: ClusterModel, configData: ClusterConfigData) => Cluster;
|
||||||
|
|
||||||
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
|
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
|
||||||
id: "create-cluster-token",
|
id: "create-cluster-token",
|
||||||
|
|||||||
19
src/common/fs/read-file-sync.injectable.ts
Normal file
19
src/common/fs/read-file-sync.injectable.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import fsInjectable from "./fs.injectable";
|
||||||
|
|
||||||
|
export type ReadFileSync = (filePath: string) => string;
|
||||||
|
|
||||||
|
const readFileSyncInjectable = getInjectable({
|
||||||
|
id: "read-file-sync",
|
||||||
|
instantiate: (di): ReadFileSync => {
|
||||||
|
const { readFileSync } = di.inject(fsInjectable);
|
||||||
|
|
||||||
|
return (filePath) => readFileSync(filePath, "utf-8");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default readFileSyncInjectable;
|
||||||
@ -150,7 +150,7 @@ export function loadConfigFromString(content: string): ConfigResult {
|
|||||||
|
|
||||||
export interface SplitConfigEntry {
|
export interface SplitConfigEntry {
|
||||||
config: KubeConfig;
|
config: KubeConfig;
|
||||||
error?: string;
|
validationResult: ValidateKubeConfigResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,7 +179,7 @@ export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
error: validateKubeConfig(config, ctx.name)?.toString(),
|
validationResult: validateKubeConfig(config, ctx.name),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -243,25 +243,44 @@ export function dumpConfigYaml(kubeConfig: PartialDeep<KubeConfig>): string {
|
|||||||
return yaml.dump(config, { skipInvalid: true });
|
return yaml.dump(config, { skipInvalid: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ValidateKubeConfigResult = {
|
||||||
|
error: Error;
|
||||||
|
} | {
|
||||||
|
error?: undefined;
|
||||||
|
context: Context;
|
||||||
|
cluster: Cluster;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
||||||
*
|
*
|
||||||
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
|
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
|
||||||
*/
|
*/
|
||||||
export function validateKubeConfig(config: KubeConfig, contextName: string): Error | undefined {
|
export function validateKubeConfig(config: KubeConfig, contextName: string): ValidateKubeConfigResult {
|
||||||
const contextObject = config.getContextObject(contextName);
|
const context = config.getContextObject(contextName);
|
||||||
|
|
||||||
if (!contextObject) {
|
if (!context) {
|
||||||
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
return {
|
||||||
|
error: new Error(`No valid context object provided in kubeconfig for context '${contextName}'`),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.getCluster(contextObject.cluster)) {
|
const cluster = config.getCluster(context.cluster);
|
||||||
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
|
||||||
|
if (!cluster) {
|
||||||
|
return {
|
||||||
|
error: new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.getUser(contextObject.user)) {
|
const user = config.getUser(context.user);
|
||||||
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
error: new Error(`No valid user object provided in kubeconfig for context '${contextName}'`),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return { cluster, user, context };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,5 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|||||||
@ -25,9 +25,11 @@ import { createHash } from "crypto";
|
|||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import globToRegExp from "glob-to-regexp";
|
import globToRegExp from "glob-to-regexp";
|
||||||
import { inspect } from "util";
|
import { inspect } from "util";
|
||||||
import type { ClusterModel, UpdateClusterModel } from "../../../common/cluster-types";
|
import type { ClusterConfigData, UpdateClusterModel } from "../../../common/cluster-types";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
import type { CatalogEntityRegistry } from "../../catalog/entity-registry";
|
import type { CatalogEntityRegistry } from "../../catalog/entity-registry";
|
||||||
|
import type { CreateCluster } from "../../../common/cluster/create-cluster-injection-token";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
const logPrefix = "[KUBECONFIG-SYNC]:";
|
const logPrefix = "[KUBECONFIG-SYNC]:";
|
||||||
|
|
||||||
@ -56,7 +58,7 @@ interface KubeconfigSyncManagerDependencies {
|
|||||||
readonly directoryForKubeConfigs: string;
|
readonly directoryForKubeConfigs: string;
|
||||||
readonly entityRegistry: CatalogEntityRegistry;
|
readonly entityRegistry: CatalogEntityRegistry;
|
||||||
readonly clusterManager: ClusterManager;
|
readonly clusterManager: ClusterManager;
|
||||||
createCluster: (model: ClusterModel) => Cluster;
|
createCluster: CreateCluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kubeConfigSyncName = "lens:kube-sync";
|
const kubeConfigSyncName = "lens:kube-sync";
|
||||||
@ -147,17 +149,26 @@ export class KubeconfigSyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// exported for testing
|
// exported for testing
|
||||||
export function configToModels(rootConfig: KubeConfig, filePath: string): UpdateClusterModel[] {
|
export function configToModels(rootConfig: KubeConfig, filePath: string): [UpdateClusterModel, ClusterConfigData][] {
|
||||||
const validConfigs = [];
|
const validConfigs: ReturnType<typeof configToModels> = [];
|
||||||
|
|
||||||
for (const { config, error } of splitConfig(rootConfig)) {
|
for (const { config, error } of splitConfig(rootConfig)) {
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.debug(`${logPrefix} context failed validation: ${error}`, { context: config.currentContext, filePath });
|
logger.debug(`${logPrefix} context failed validation: ${error}`, { context: config.currentContext, filePath });
|
||||||
} else {
|
} else {
|
||||||
validConfigs.push({
|
const cluster = config.getCluster(config.currentContext);
|
||||||
|
|
||||||
|
assert(cluster, "Config somehow passed validations but still doesn't have a cluster");
|
||||||
|
|
||||||
|
validConfigs.push([
|
||||||
|
{
|
||||||
kubeConfigPath: filePath,
|
kubeConfigPath: filePath,
|
||||||
contextName: config.currentContext,
|
contextName: config.currentContext,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
clusterServerUrl: cluster.server,
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +180,7 @@ type RootSource = ObservableMap<string, RootSourceValue>;
|
|||||||
|
|
||||||
interface ComputeDiffDependencies {
|
interface ComputeDiffDependencies {
|
||||||
directoryForKubeConfigs: string;
|
directoryForKubeConfigs: string;
|
||||||
createCluster: (model: ClusterModel) => Cluster;
|
createCluster: CreateCluster;
|
||||||
clusterManager: ClusterManager;
|
clusterManager: ClusterManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,15 +195,15 @@ export const computeDiff = ({ directoryForKubeConfigs, createCluster, clusterMan
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rawModels = configToModels(config, filePath);
|
const rawModels = configToModels(config, filePath);
|
||||||
const models = new Map(rawModels.map(m => [m.contextName, m]));
|
const models = new Map(rawModels.map(([model, configData]) => [model.contextName, [model, configData] as const]));
|
||||||
|
|
||||||
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
|
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
|
||||||
|
|
||||||
for (const [contextName, value] of source) {
|
for (const [contextName, value] of source) {
|
||||||
const model = models.get(contextName);
|
const data = models.get(contextName);
|
||||||
|
|
||||||
// remove and disconnect clusters that were removed from the config
|
// remove and disconnect clusters that were removed from the config
|
||||||
if (!model) {
|
if (!data) {
|
||||||
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
|
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
|
||||||
clusterManager.deleting.delete(value[0].id);
|
clusterManager.deleting.delete(value[0].id);
|
||||||
|
|
||||||
@ -207,17 +218,17 @@ export const computeDiff = ({ directoryForKubeConfigs, createCluster, clusterMan
|
|||||||
// diff against that
|
// diff against that
|
||||||
|
|
||||||
// or update the model and mark it as not needed to be added
|
// or update the model and mark it as not needed to be added
|
||||||
value[0].updateModel(model);
|
value[0].updateModel(data[0]);
|
||||||
models.delete(contextName);
|
models.delete(contextName);
|
||||||
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
|
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [contextName, model] of models) {
|
for (const [contextName, [model, configData]] of models) {
|
||||||
// add new clusters to the source
|
// add new clusters to the source
|
||||||
try {
|
try {
|
||||||
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
|
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
|
||||||
|
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId });
|
const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId }, configData);
|
||||||
|
|
||||||
if (!cluster.apiUrl) {
|
if (!cluster.apiUrl) {
|
||||||
throw new Error("Cluster constructor failed, see above error");
|
throw new Error("Cluster constructor failed, see above error");
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const createClusterInjectable = getInjectable({
|
|||||||
createVersionDetector: di.inject(createVersionDetectorInjectable),
|
createVersionDetector: di.inject(createVersionDetectorInjectable),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (model) => new Cluster(dependencies, model);
|
return (model, configData) => new Cluster(dependencies, model, configData);
|
||||||
},
|
},
|
||||||
|
|
||||||
injectionToken: createClusterInjectionToken,
|
injectionToken: createClusterInjectionToken,
|
||||||
|
|||||||
@ -41,7 +41,7 @@ function getContexts(config: KubeConfig): Map<string, Option> {
|
|||||||
splitConfig(config)
|
splitConfig(config)
|
||||||
.map(({ config, error }) => [config.currentContext, {
|
.map(({ config, error }) => [config.currentContext, {
|
||||||
config,
|
config,
|
||||||
error,
|
error: error?.toString(),
|
||||||
}]),
|
}]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import { ClusterIssues } from "./cluster-issues";
|
|||||||
import { ClusterMetrics } from "./cluster-metrics";
|
import { ClusterMetrics } from "./cluster-metrics";
|
||||||
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
|
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
|
||||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||||
import { getActiveClusterEntity } from "../../api/catalog/entity/legacy-globals";
|
|
||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import type { EventStore } from "../+events/store";
|
import type { EventStore } from "../+events/store";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
@ -71,7 +70,7 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
|||||||
this.metricPoller.stop();
|
this.metricPoller.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMetrics(isMetricsHidden?: boolean) {
|
renderMetrics(isMetricsHidden: boolean) {
|
||||||
if (isMetricsHidden) {
|
if (isMetricsHidden) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -84,7 +83,7 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderClusterOverview(isLoaded: boolean, isMetricsHidden?: boolean) {
|
renderClusterOverview(isLoaded: boolean, isMetricsHidden: boolean) {
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
return <Spinner center/>;
|
return <Spinner center/>;
|
||||||
}
|
}
|
||||||
@ -98,9 +97,9 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { eventStore, nodeStore } = this.props;
|
const { eventStore, nodeStore, hostedCluster } = this.props;
|
||||||
const isLoaded = nodeStore.isLoaded && eventStore.isLoaded;
|
const isLoaded = nodeStore.isLoaded && eventStore.isLoaded;
|
||||||
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Cluster);
|
const isMetricHidden = hostedCluster.isMetricHidden(ClusterMetricsResourceType.Cluster);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabLayout>
|
<TabLayout>
|
||||||
|
|||||||
@ -12,51 +12,48 @@ import { CommandDialog } from "./command-dialog";
|
|||||||
import type { ClusterId } from "../../../common/cluster-types";
|
import type { ClusterId } from "../../../common/cluster-types";
|
||||||
import type { CommandOverlay } from "./command-overlay.injectable";
|
import type { CommandOverlay } from "./command-overlay.injectable";
|
||||||
import commandOverlayInjectable from "./command-overlay.injectable";
|
import commandOverlayInjectable from "./command-overlay.injectable";
|
||||||
import { isMac } from "../../../common/vars";
|
import type { ipcRendererOn } from "../../../common/ipc";
|
||||||
import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc";
|
|
||||||
import type { Disposer } from "../../utils";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import type { AddWindowEventListener } from "../../window/event-listener.injectable";
|
||||||
import windowAddEventListenerInjectable from "../../window/event-listener.injectable";
|
import windowAddEventListenerInjectable from "../../window/event-listener.injectable";
|
||||||
import type { IComputedValue } from "mobx";
|
import type { IComputedValue } from "mobx";
|
||||||
import matchedClusterIdInjectable from "../../navigation/matched-cluster-id.injectable";
|
import matchedClusterIdInjectable from "../../navigation/matched-cluster-id.injectable";
|
||||||
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
|
|
||||||
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
||||||
|
import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
||||||
|
import legacyOnChannelListenInjectable from "../../ipc/legacy-channel-listen.injectable";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
addWindowEventListener: <K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => Disposer;
|
addWindowEventListener: AddWindowEventListener;
|
||||||
commandOverlay: CommandOverlay;
|
commandOverlay: CommandOverlay;
|
||||||
clusterId?: ClusterId;
|
clusterId: ClusterId | undefined;
|
||||||
matchedClusterId: IComputedValue<ClusterId>;
|
matchedClusterId: IComputedValue<ClusterId | undefined>;
|
||||||
entityRegistry: CatalogEntityRegistry;
|
isMac: boolean;
|
||||||
|
legacyOnChannelListen: typeof ipcRendererOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class NonInjectedCommandContainer extends React.Component<Dependencies> {
|
class NonInjectedCommandContainer extends React.Component<Dependencies> {
|
||||||
private escHandler(event: KeyboardEvent) {
|
private escHandler = (event: KeyboardEvent) => {
|
||||||
const { commandOverlay } = this.props;
|
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
commandOverlay.close();
|
this.props.commandOverlay.close();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleCommandPalette = () => {
|
handleCommandPalette = () => {
|
||||||
const { commandOverlay, entityRegistry } = this.props;
|
const matchedClusterId = this.props.matchedClusterId.get();
|
||||||
const clusterIsActive = this.props.matchedClusterId.get() !== undefined;
|
|
||||||
|
|
||||||
if (clusterIsActive) {
|
if (matchedClusterId !== undefined) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
broadcastMessage(`command-palette:${matchedClusterId}:open`);
|
||||||
broadcastMessage(`command-palette:${entityRegistry.activeEntity!.getId()}:open`);
|
|
||||||
} else {
|
} else {
|
||||||
commandOverlay.open(<CommandDialog />);
|
this.props.commandOverlay.open(<CommandDialog />);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyboardShortcut(action: () => void) {
|
onKeyboardShortcut(action: () => void) {
|
||||||
return ({ key, shiftKey, ctrlKey, altKey, metaKey }: KeyboardEvent) => {
|
return ({ key, shiftKey, ctrlKey, altKey, metaKey }: KeyboardEvent) => {
|
||||||
const ctrlOrCmd = isMac ? metaKey && !ctrlKey : !metaKey && ctrlKey;
|
const ctrlOrCmd = this.props.isMac ? metaKey && !ctrlKey : !metaKey && ctrlKey;
|
||||||
|
|
||||||
if (key === "p" && shiftKey && ctrlOrCmd && !altKey) {
|
if (key === "p" && shiftKey && ctrlOrCmd && !altKey) {
|
||||||
action();
|
action();
|
||||||
@ -75,9 +72,9 @@ class NonInjectedCommandContainer extends React.Component<Dependencies> {
|
|||||||
: "command-palette:open";
|
: "command-palette:open";
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
ipcRendererOn(ipcChannel, action),
|
this.props.legacyOnChannelListen(ipcChannel, action),
|
||||||
addWindowEventListener("keydown", this.onKeyboardShortcut(action)),
|
addWindowEventListener("keydown", this.onKeyboardShortcut(action)),
|
||||||
addWindowEventListener("keyup", (e) => this.escHandler(e), true),
|
addWindowEventListener("keyup", this.escHandler, true),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +103,7 @@ export const CommandContainer = withInjectables<Dependencies>(NonInjectedCommand
|
|||||||
addWindowEventListener: di.inject(windowAddEventListenerInjectable),
|
addWindowEventListener: di.inject(windowAddEventListenerInjectable),
|
||||||
commandOverlay: di.inject(commandOverlayInjectable),
|
commandOverlay: di.inject(commandOverlayInjectable),
|
||||||
matchedClusterId: di.inject(matchedClusterIdInjectable),
|
matchedClusterId: di.inject(matchedClusterIdInjectable),
|
||||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
isMac: di.inject(isMacInjectable),
|
||||||
|
legacyOnChannelListen: di.inject(legacyOnChannelListenInjectable),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -80,9 +80,7 @@ class NonInjectedSidebarItem extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssNames("SidebarItem")}
|
className={cssNames("SidebarItem")}
|
||||||
data-testid="sidebar-item"
|
data-testid={`sidebar-item-${this.id}`}
|
||||||
data-test-id={this.id}
|
|
||||||
data-id-test={this.id}
|
|
||||||
data-is-active-test={this.isActive}
|
data-is-active-test={this.isActive}
|
||||||
data-parent-id-test={this.registration.parentId}
|
data-parent-id-test={this.registration.parentId}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const createClusterInjectable = getInjectable({
|
|||||||
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (model) => new Cluster(dependencies, model);
|
return (model, configData) => new Cluster(dependencies, model, configData);
|
||||||
},
|
},
|
||||||
|
|
||||||
injectionToken: createClusterInjectionToken,
|
injectionToken: createClusterInjectionToken,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
11
src/renderer/frames/cluster-frame/cluster-frame.module.css
Normal file
11
src/renderer/frames/cluster-frame/cluster-frame.module.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.centering {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
124
src/renderer/frames/cluster-frame/cluster-frame.test.tsx
Normal file
124
src/renderer/frames/cluster-frame/cluster-frame.test.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import type { RenderResult } from "@testing-library/react";
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import { render as testingLibraryRender } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||||
|
import { Router } from "react-router";
|
||||||
|
import { DefaultProps } from "../../mui-base-theme";
|
||||||
|
import { ClusterFrame } from "./cluster-frame";
|
||||||
|
import historyInjectable from "../../navigation/history.injectable";
|
||||||
|
import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
|
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
|
||||||
|
import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable";
|
||||||
|
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||||
|
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||||
|
import hostedClusterIdInjectable from "../../../common/cluster-store/hosted-cluster-id.injectable";
|
||||||
|
import legacyOnChannelListenInjectable from "../../ipc/legacy-channel-listen.injectable";
|
||||||
|
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
||||||
|
|
||||||
|
describe("<ClusterFrame />", () => {
|
||||||
|
let render: () => RenderResult;
|
||||||
|
let di: DiContainer;
|
||||||
|
let cluster: Cluster;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
render = () => testingLibraryRender((
|
||||||
|
<DiContextProvider value={{ di }}>
|
||||||
|
<Router history={di.inject(historyInjectable)}>
|
||||||
|
{DefaultProps(ClusterFrame)}
|
||||||
|
</Router>
|
||||||
|
</DiContextProvider>
|
||||||
|
));
|
||||||
|
|
||||||
|
di.override(subscribeStoresInjectable, () => jest.fn().mockImplementation(() => jest.fn()));
|
||||||
|
di.override(legacyOnChannelListenInjectable, () => jest.fn().mockImplementation(() => jest.fn()));
|
||||||
|
di.override(directoryForUserDataInjectable, () => "/some/irrelavent/path");
|
||||||
|
di.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||||
|
|
||||||
|
const createCluster = di.inject(createClusterInjectable);
|
||||||
|
|
||||||
|
cluster = createCluster(
|
||||||
|
{
|
||||||
|
contextName: "my-cluster",
|
||||||
|
id: "123456",
|
||||||
|
kubeConfigPath: "/irrelavent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clusterServerUrl: "https://localhost",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
di.override(hostedClusterInjectable, () => cluster);
|
||||||
|
di.override(hostedClusterIdInjectable, () => cluster.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given cluster with list nodes and namespaces permissions", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
di.override(allowedResourcesInjectable, () => computed(() => new Set(["nodes", "namespaces"])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
const result = render();
|
||||||
|
|
||||||
|
expect(result.container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows cluster overview sidebar item as active", () => {
|
||||||
|
const result = render();
|
||||||
|
const clusterOverviewSidebarItem = result.getByTestId("sidebar-item-cluster-overview");
|
||||||
|
|
||||||
|
expect(clusterOverviewSidebarItem.getAttribute("data-is-active-test")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given no matching component", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
di.override(currentRouteComponentInjectable, () => computed(() => undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given current url is starting url", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const result = render();
|
||||||
|
|
||||||
|
expect(result.container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows warning message", () => {
|
||||||
|
const result = render();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.getByText("An error has occured. No route can be found matching the current route, which is also the starting route."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given cluster without list nodes, but with namespaces permissions", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
di.override(allowedResourcesInjectable, () => computed(() => new Set(["namespaces"])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
const result = render();
|
||||||
|
|
||||||
|
expect(result.container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows workloads overview sidebar item as active", () => {
|
||||||
|
const result = render();
|
||||||
|
const workloadsOverviewSidebarItem = result.getByTestId("sidebar-item-workloads");
|
||||||
|
|
||||||
|
expect(workloadsOverviewSidebarItem.getAttribute("data-is-active-test")).toBe("true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -2,6 +2,7 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
import styles from "./cluster-frame.module.css";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import type { IComputedValue } from "mobx";
|
import type { IComputedValue } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -30,12 +31,14 @@ import { disposer } from "../../utils";
|
|||||||
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
||||||
import startUrlInjectable from "./start-url.injectable";
|
import startUrlInjectable from "./start-url.injectable";
|
||||||
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||||
|
import currentPathInjectable from "../../routes/current-path.injectable";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
namespaceStore: NamespaceStore;
|
namespaceStore: NamespaceStore;
|
||||||
currentRouteComponent: IComputedValue<React.ElementType<{}> | undefined | null>;
|
currentRouteComponent: IComputedValue<React.ElementType<{}> | undefined>;
|
||||||
startUrl: IComputedValue<string>;
|
startUrl: IComputedValue<string>;
|
||||||
subscribeStores: SubscribeStores;
|
subscribeStores: SubscribeStores;
|
||||||
|
currentPath: IComputedValue<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NonInjectedClusterFrame = observer(({
|
export const NonInjectedClusterFrame = observer(({
|
||||||
@ -43,6 +46,7 @@ export const NonInjectedClusterFrame = observer(({
|
|||||||
currentRouteComponent,
|
currentRouteComponent,
|
||||||
startUrl,
|
startUrl,
|
||||||
subscribeStores,
|
subscribeStores,
|
||||||
|
currentPath,
|
||||||
}: Dependencies) => {
|
}: Dependencies) => {
|
||||||
useEffect(() => disposer(
|
useEffect(() => disposer(
|
||||||
subscribeStores([
|
subscribeStores([
|
||||||
@ -52,6 +56,8 @@ export const NonInjectedClusterFrame = observer(({
|
|||||||
), []);
|
), []);
|
||||||
|
|
||||||
const Component = currentRouteComponent.get();
|
const Component = currentRouteComponent.get();
|
||||||
|
const starting = startUrl.get();
|
||||||
|
const current = currentPath.get();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@ -62,7 +68,16 @@ export const NonInjectedClusterFrame = observer(({
|
|||||||
{
|
{
|
||||||
Component
|
Component
|
||||||
? <Component />
|
? <Component />
|
||||||
: <Redirect to={startUrl.get()} />
|
// NOTE: this check is to prevent an infinite loop
|
||||||
|
: starting !== current
|
||||||
|
? <Redirect to={startUrl.get()} />
|
||||||
|
: (
|
||||||
|
<div className={styles.centering}>
|
||||||
|
<div className="error">
|
||||||
|
An error has occured. No route can be found matching the current route, which is also the starting route.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
||||||
@ -87,6 +102,7 @@ export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFram
|
|||||||
subscribeStores: di.inject(subscribeStoresInjectable),
|
subscribeStores: di.inject(subscribeStoresInjectable),
|
||||||
startUrl: di.inject(startUrlInjectable),
|
startUrl: di.inject(startUrlInjectable),
|
||||||
currentRouteComponent: di.inject(currentRouteComponentInjectable),
|
currentRouteComponent: di.inject(currentRouteComponentInjectable),
|
||||||
|
currentPath: di.inject(currentPathInjectable),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -4,29 +4,30 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import type { KubeResource } from "../../../common/rbac";
|
|
||||||
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
|
|
||||||
import clusterOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable";
|
import clusterOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable";
|
||||||
import workloadsOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable";
|
import workloadsOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable";
|
||||||
import { buildURL } from "../../../common/utils/buildUrl";
|
|
||||||
|
|
||||||
const startUrlInjectable = getInjectable({
|
const startUrlInjectable = getInjectable({
|
||||||
id: "start-url",
|
id: "start-url",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const isAllowedResource = (resourceName: string) => di.inject(isAllowedResourceInjectable, resourceName);
|
|
||||||
|
|
||||||
const clusterOverviewRoute = di.inject(clusterOverviewRouteInjectable);
|
const clusterOverviewRoute = di.inject(clusterOverviewRouteInjectable);
|
||||||
const workloadOverviewRoute = di.inject(workloadsOverviewRouteInjectable);
|
const workloadOverviewRoute = di.inject(workloadsOverviewRouteInjectable);
|
||||||
const clusterOverviewUrl = buildURL(clusterOverviewRoute.path);
|
|
||||||
const workloadOverviewUrl = buildURL(workloadOverviewRoute.path);
|
|
||||||
|
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
const resources: KubeResource[] = ["events", "nodes", "pods"];
|
if (clusterOverviewRoute.isEnabled.get()) {
|
||||||
|
return clusterOverviewRoute.path;
|
||||||
|
}
|
||||||
|
|
||||||
return resources.every((resourceName) => isAllowedResource(resourceName))
|
if (workloadOverviewRoute.isEnabled.get()) {
|
||||||
? clusterOverviewUrl
|
return workloadOverviewRoute.path;
|
||||||
: workloadOverviewUrl;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: This will never be executed as `workloadOverviewRoute.isEnabled` always is true. It
|
||||||
|
* is here is guard against accidental changes at a distance within `workloadOverviewRoute`.
|
||||||
|
*/
|
||||||
|
throw new Error("Exhausted all possible starting locations and none are active. This is a bug.");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
15
src/renderer/ipc/legacy-channel-listen.injectable.ts
Normal file
15
src/renderer/ipc/legacy-channel-listen.injectable.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { ipcRendererOn } from "../../common/ipc";
|
||||||
|
|
||||||
|
const legacyOnChannelListenInjectable = getInjectable({
|
||||||
|
id: "legacy-on-channel-listen",
|
||||||
|
instantiate: () => ipcRendererOn,
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default legacyOnChannelListenInjectable;
|
||||||
@ -6,6 +6,8 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { Disposer } from "../utils";
|
import type { Disposer } from "../utils";
|
||||||
|
|
||||||
|
export type AddWindowEventListener = typeof addWindowEventListener;
|
||||||
|
|
||||||
function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer {
|
function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer {
|
||||||
window.addEventListener(type, listener, options);
|
window.addEventListener(type, listener, options);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user