mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix cluster connect statuses being intertwined
- Broadcast on a signle channel and add clusterId as data argument - Make everything else computed based on the current props.cluster value - Fix view staying on cluster connect screen after disconnect - Initially clicking on a disconnected cluster should not immediately nativate to the Catalog if currently viewing a connected cluster - Make route-path-parameters more type correct by not using keyedSingleton Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
a9f4bcecb2
commit
853573afcb
@ -106,14 +106,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.clusters.size > 0;
|
||||
}
|
||||
|
||||
getById(id: ClusterId | undefined): Cluster | undefined {
|
||||
if (id) {
|
||||
getById(id: ClusterId): Cluster | undefined {
|
||||
return this.clusters.get(id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||
appEventBus.emit({ name: "cluster", action: "add" });
|
||||
|
||||
|
||||
@ -120,7 +120,7 @@ export enum ClusterStatus {
|
||||
}
|
||||
|
||||
/**
|
||||
* The message format for the "cluster:<cluster-id>:connection-update" channels
|
||||
* The message format for the "cluster:connection-update" channel
|
||||
*/
|
||||
export interface KubeAuthUpdate {
|
||||
message: string;
|
||||
|
||||
@ -625,7 +625,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const update: KubeAuthUpdate = { message, isError };
|
||||
|
||||
this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
|
||||
broadcastMessage(`cluster:${this.id}:connection-update`, update);
|
||||
broadcastMessage(`cluster:connection-update`, this.id, update);
|
||||
}
|
||||
|
||||
protected async getAllowedNamespaces(proxyConfig: KubeConfig) {
|
||||
|
||||
@ -5,15 +5,20 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import { frontEndRouteInjectionToken } from "../../front-end-route-injection-token";
|
||||
import type { Route } from "../../front-end-route-injection-token";
|
||||
|
||||
export interface ClusterViewRouteParams {
|
||||
clusterId: string;
|
||||
}
|
||||
|
||||
const clusterViewRouteInjectable = getInjectable({
|
||||
id: "cluster-view-route",
|
||||
|
||||
instantiate: () => ({
|
||||
path: "/cluster/:clusterId",
|
||||
path: "/cluster/:clusterId" as const,
|
||||
clusterFrame: false,
|
||||
isEnabled: computed(() => true),
|
||||
}),
|
||||
}) as Route<ClusterViewRouteParams>,
|
||||
|
||||
injectionToken: frontEndRouteInjectionToken,
|
||||
});
|
||||
|
||||
@ -219,7 +219,7 @@ describe("kube auth proxy tests", () => {
|
||||
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
|
||||
|
||||
const cluster = createCluster({
|
||||
id: "foobar",
|
||||
id: "some-cluster-id",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
contextName: "minikube",
|
||||
}, {
|
||||
@ -233,34 +233,34 @@ describe("kube auth proxy tests", () => {
|
||||
await proxy.run();
|
||||
listeners.emit("error", { message: "foobarbat" });
|
||||
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", isError: true });
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "foobarbat", isError: true });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast exit", async () => {
|
||||
await proxy.run();
|
||||
listeners.emit("exit", 0);
|
||||
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", isError: false });
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "proxy exited with code: 0", isError: false });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast errors from stderr", async () => {
|
||||
await proxy.run();
|
||||
listeners.emit("stderr/data", "an error");
|
||||
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", isError: true });
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "an error", isError: true });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast stdout serving info", async () => {
|
||||
await proxy.run();
|
||||
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", isError: false });
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "Authentication proxy started", isError: false });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast stdout other info", async () => {
|
||||
await proxy.run();
|
||||
listeners.emit("stdout/data", "some info");
|
||||
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", isError: false });
|
||||
expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "some info", isError: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,7 +13,7 @@ const clusterManagerInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const clusterManager = new ClusterManager({
|
||||
store: di.inject(clusterStoreInjectable),
|
||||
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
});
|
||||
|
||||
clusterManager.init();
|
||||
|
||||
@ -23,8 +23,8 @@ const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
|
||||
|
||||
interface Dependencies {
|
||||
store: ClusterStore;
|
||||
catalogEntityRegistry: CatalogEntityRegistry;
|
||||
readonly store: ClusterStore;
|
||||
readonly entityRegistry: CatalogEntityRegistry;
|
||||
}
|
||||
|
||||
export class ClusterManager {
|
||||
@ -32,7 +32,7 @@ export class ClusterManager {
|
||||
|
||||
@observable visibleCluster: ClusterId | undefined = undefined;
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@ -52,12 +52,12 @@ export class ClusterManager {
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
() => this.dependencies.entityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
entities => this.syncClustersFromCatalog(entities),
|
||||
);
|
||||
|
||||
reaction(() => [
|
||||
this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
this.dependencies.entityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
this.visibleCluster,
|
||||
] as const, ([entities, visibleCluster]) => {
|
||||
for (const entity of entities) {
|
||||
@ -71,7 +71,7 @@ export class ClusterManager {
|
||||
|
||||
observe(this.deleting, change => {
|
||||
if (change.type === "add") {
|
||||
this.updateEntityStatus(this.dependencies.catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
|
||||
this.updateEntityStatus(this.dependencies.entityRegistry.findById(change.newValue) as KubernetesCluster);
|
||||
}
|
||||
});
|
||||
|
||||
@ -89,13 +89,13 @@ export class ClusterManager {
|
||||
}
|
||||
|
||||
protected updateEntityFromCluster(cluster: Cluster) {
|
||||
const index = this.dependencies.catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id);
|
||||
const index = this.dependencies.entityRegistry.items.findIndex((entity) => entity.getId() === cluster.id);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = this.dependencies.catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||
const entity = this.dependencies.entityRegistry.items[index] as KubernetesCluster;
|
||||
|
||||
this.updateEntityStatus(entity, cluster);
|
||||
|
||||
@ -136,7 +136,7 @@ export class ClusterManager {
|
||||
cluster.preferences.icon = undefined;
|
||||
}
|
||||
|
||||
this.dependencies.catalogEntityRegistry.items.splice(index, 1, entity);
|
||||
this.dependencies.entityRegistry.items.splice(index, 1, entity);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -279,7 +279,13 @@ export class ClusterManager {
|
||||
return cluster;
|
||||
}
|
||||
|
||||
return this.dependencies.store.getById(getClusterIdFromHost(req.headers.host));
|
||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||
|
||||
if (!clusterId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.dependencies.store.getById(clusterId);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,15 @@ const getActiveClusterEntityInjectable = getInjectable({
|
||||
const store = di.inject(clusterStoreInjectable);
|
||||
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
return () => store.getById(entityRegistry.activeEntity?.getId());
|
||||
return () => {
|
||||
const entityId = entityRegistry.activeEntity?.getId();
|
||||
|
||||
if (!entityId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return store.getById(entityId);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -13,6 +13,10 @@ const hostedClusterInjectable = getInjectable({
|
||||
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||
const store = di.inject(clusterStoreInjectable);
|
||||
|
||||
if (!hostedClusterId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return store.getById(hostedClusterId);
|
||||
},
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ const catalogRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(catalogRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
group: computed(() => pathParameters.get().group),
|
||||
|
||||
@ -56,8 +56,8 @@ interface Dependencies {
|
||||
customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>;
|
||||
emitEvent: (event: AppEvent) => void;
|
||||
routeParameters: {
|
||||
group: IComputedValue<string>;
|
||||
kind: IComputedValue<string>;
|
||||
group: IComputedValue<string | undefined>;
|
||||
kind: IComputedValue<string | undefined>;
|
||||
};
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
hotbarStore: HotbarStore;
|
||||
|
||||
@ -27,8 +27,8 @@ enum columnId {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
group: IComputedValue<string>;
|
||||
name: IComputedValue<string>;
|
||||
group: IComputedValue<string | undefined>;
|
||||
name: IComputedValue<string | undefined>;
|
||||
apiManager: ApiManager;
|
||||
customResourceDefinitionStore: CustomResourceDefinitionStore;
|
||||
}
|
||||
@ -41,7 +41,14 @@ class NonInjectedCustomResources extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
@computed get crd() {
|
||||
return this.props.customResourceDefinitionStore.getByGroup(this.props.group.get(), this.props.name.get());
|
||||
const group = this.props.group.get();
|
||||
const name = this.props.name.get();
|
||||
|
||||
if (!group || !name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.props.customResourceDefinitionStore.getByGroup(group, name);
|
||||
}
|
||||
|
||||
@computed get store() {
|
||||
|
||||
@ -12,7 +12,7 @@ const customResourcesRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(customResourcesRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
group: computed(() => pathParameters.get().group),
|
||||
|
||||
@ -12,7 +12,7 @@ const entitySettingsRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(entitySettingsRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
entityId: computed(() => pathParameters.get().entityId),
|
||||
|
||||
@ -24,7 +24,7 @@ import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.i
|
||||
import observableHistoryInjectable from "../../navigation/observable-history.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
entityId: IComputedValue<string>;
|
||||
entityId: IComputedValue<string | undefined>;
|
||||
entityRegistry: CatalogEntityRegistry;
|
||||
observableHistory: ObservableHistory<unknown>;
|
||||
}
|
||||
@ -54,8 +54,15 @@ class NonInjectedEntitySettings extends React.Component<Dependencies> {
|
||||
return this.props.entityId.get();
|
||||
}
|
||||
|
||||
@computed
|
||||
get entity() {
|
||||
return this.props.entityRegistry.getById(this.entityId);
|
||||
const { entityId } = this;
|
||||
|
||||
if (!entityId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.props.entityRegistry.getById(entityId);
|
||||
}
|
||||
|
||||
get menuItems() {
|
||||
|
||||
@ -12,7 +12,7 @@ const helmChartsRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(helmChartsRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
chartName: computed(() => pathParameters.get().chartName),
|
||||
|
||||
@ -32,8 +32,8 @@ enum columnId {
|
||||
|
||||
interface Dependencies {
|
||||
routeParameters: {
|
||||
chartName: IComputedValue<string>;
|
||||
repo: IComputedValue<string>;
|
||||
chartName: IComputedValue<string | undefined>;
|
||||
repo: IComputedValue<string | undefined>;
|
||||
};
|
||||
|
||||
navigateToHelmCharts: NavigateToHelmCharts;
|
||||
|
||||
@ -12,7 +12,7 @@ const helmReleasesRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(helmReleasesRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
namespace: computed(() => pathParameters.get().namespace),
|
||||
|
||||
@ -40,7 +40,7 @@ interface Dependencies {
|
||||
releases: IComputedValue<RemovableHelmRelease[]>;
|
||||
releasesArePending: IComputedValue<boolean>;
|
||||
selectNamespace: (namespace: string) => void;
|
||||
namespace: IComputedValue<string>;
|
||||
namespace: IComputedValue<string | undefined>;
|
||||
navigateToHelmReleases: NavigateToHelmReleases;
|
||||
}
|
||||
|
||||
@ -213,20 +213,12 @@ class NonInjectedHelmReleases extends Component<Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const HelmReleases = withInjectables<Dependencies>(
|
||||
NonInjectedHelmReleases,
|
||||
|
||||
{
|
||||
getProps: (di) => {
|
||||
const routeParameters = di.inject(helmReleasesRouteParametersInjectable);
|
||||
|
||||
return {
|
||||
export const HelmReleases = withInjectables<Dependencies>(NonInjectedHelmReleases, {
|
||||
getProps: (di) => ({
|
||||
releases: di.inject(removableReleasesInjectable),
|
||||
releasesArePending: di.inject(releasesInjectable).pending,
|
||||
selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces,
|
||||
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
|
||||
namespace: routeParameters.namespace,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
...di.inject(helmReleasesRouteParametersInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ const portForwardsRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(portForwardsRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
forwardport: computed(() => pathParameters.get().forwardport),
|
||||
|
||||
@ -32,7 +32,7 @@ enum columnId {
|
||||
|
||||
interface Dependencies {
|
||||
portForwardStore: PortForwardStore;
|
||||
forwardport: IComputedValue<string>;
|
||||
forwardport: IComputedValue<string | undefined>;
|
||||
navigateToPortForwards: NavigateToPortForwards;
|
||||
}
|
||||
|
||||
@ -158,19 +158,11 @@ class NonInjectedPortForwards extends React.Component<Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const PortForwards = withInjectables<Dependencies>(
|
||||
NonInjectedPortForwards,
|
||||
|
||||
{
|
||||
getProps: (di) => {
|
||||
const routeParameters = di.inject(portForwardsRouteParametersInjectable);
|
||||
|
||||
return {
|
||||
export const PortForwards = withInjectables<Dependencies>(NonInjectedPortForwards, {
|
||||
getProps: (di) => ({
|
||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||
forwardport: routeParameters.forwardport,
|
||||
navigateToPortForwards: di.inject(navigateToPortForwardsInjectable),
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
...di.inject(portForwardsRouteParametersInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 { action, reaction } from "mobx";
|
||||
import type { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
import type { ClusterId, KubeAuthUpdate } from "../../../common/cluster-types";
|
||||
import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
|
||||
import type { ClusterConnectionStatusState } from "./cluster-status.state.injectable";
|
||||
|
||||
function computeRisingEdgeForClusterDisconnect(store: ClusterStore, onNewlyDisconnected: (clusterId: ClusterId) => void) {
|
||||
const disconnectedStateComputer = () => store.clustersList.map(cluster => [cluster.id, cluster.disconnected] as const);
|
||||
const state = new Map(disconnectedStateComputer());
|
||||
|
||||
reaction(
|
||||
disconnectedStateComputer,
|
||||
(disconnectedStates) => {
|
||||
for (const [clusterId, isDisconnected] of disconnectedStates) {
|
||||
if (state.get(clusterId) === isDisconnected) {
|
||||
// do nothing
|
||||
} else {
|
||||
state.set(clusterId, isDisconnected); // save the new state
|
||||
|
||||
if (isDisconnected) {
|
||||
// If the new value is `true` then the previous value was falsy and this is the rising edge.
|
||||
onNewlyDisconnected(clusterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// This needs to be an `init` function to bypass a bug in the setup -> injectable -> setup path
|
||||
const initClusterStatusWatcherInjectable = getInjectable({
|
||||
id: "cluster-status-watcher",
|
||||
instantiate: (di) => {
|
||||
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||
const clusterStore = di.inject(clusterStoreInjectable);
|
||||
|
||||
return (state: ClusterConnectionStatusState) => {
|
||||
ipcRenderer.on("cluster:connection-update", (evt, clusterId: ClusterId, update: KubeAuthUpdate) => {
|
||||
state.forCluster(clusterId).appendAuthUpdate(update);
|
||||
});
|
||||
computeRisingEdgeForClusterDisconnect(clusterStore, action((clusterId) => {
|
||||
const forCluster = state.forCluster(clusterId);
|
||||
|
||||
forCluster.clearReconnectingState();
|
||||
forCluster.resetAuthOutput();
|
||||
}));
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default initClusterStatusWatcherInjectable;
|
||||
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 type { IComputedValue } from "mobx";
|
||||
import { action, computed, observable } from "mobx";
|
||||
import type { ClusterId, KubeAuthUpdate } from "../../../common/cluster-types";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import { getOrInsert, hasTypedProperty, isBoolean, isObject, isString } from "../../utils";
|
||||
import initClusterStatusWatcherInjectable from "./cluster-status-watcher.injectable";
|
||||
|
||||
export interface ClusterConnectionStatus {
|
||||
readonly authOutput: IComputedValue<KubeAuthUpdate[]>;
|
||||
readonly hasErrorOutput: IComputedValue<boolean>;
|
||||
readonly isReconnecting: IComputedValue<boolean>;
|
||||
resetAuthOutput(): void;
|
||||
setAsReconnecting(): void;
|
||||
clearReconnectingState(): void;
|
||||
appendAuthUpdate(update: unknown): void;
|
||||
}
|
||||
|
||||
export interface ClusterConnectionStatusState {
|
||||
forCluster(clusterId: ClusterId): Readonly<ClusterConnectionStatus>;
|
||||
}
|
||||
|
||||
const clusterConnectionStatusStateInjectable = getInjectable({
|
||||
id: "cluster-connection-status-state",
|
||||
instantiate: (di) => {
|
||||
const authOutputs = observable.map<ClusterId, KubeAuthUpdate[]>();
|
||||
const reconnecting = observable.set<ClusterId>();
|
||||
const initWatcher = di.inject(initClusterStatusWatcherInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
const state: ClusterConnectionStatusState = {
|
||||
forCluster: (clusterId) => {
|
||||
const authOutput = computed(() => authOutputs.get(clusterId) ?? []);
|
||||
|
||||
return {
|
||||
authOutput,
|
||||
isReconnecting: computed(() => reconnecting.has(clusterId)),
|
||||
hasErrorOutput: computed(() => authOutput.get().some(output => output.isError)),
|
||||
resetAuthOutput: action(() => {
|
||||
authOutputs.delete(clusterId);
|
||||
}),
|
||||
setAsReconnecting: action(() => {
|
||||
reconnecting.add(clusterId);
|
||||
}),
|
||||
clearReconnectingState: action(() => {
|
||||
reconnecting.delete(clusterId);
|
||||
}),
|
||||
appendAuthUpdate: action((update) => {
|
||||
if (
|
||||
isObject(update)
|
||||
&& hasTypedProperty(update, "message", isString)
|
||||
&& hasTypedProperty(update, "isError", isBoolean)
|
||||
) {
|
||||
getOrInsert(authOutputs, clusterId, []).push(update);
|
||||
} else {
|
||||
logger.warn(`[CLUSTER]: invalid connection update`, { update, clusterId });
|
||||
}
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
initWatcher(state);
|
||||
|
||||
return state;
|
||||
},
|
||||
});
|
||||
|
||||
export default clusterConnectionStatusStateInjectable;
|
||||
@ -5,23 +5,23 @@
|
||||
|
||||
import styles from "./cluster-status.module.scss";
|
||||
|
||||
import { computed, observable, makeObservable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { runInAction } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { ipcRendererOn } from "../../../common/ipc";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type { IClassName } from "../../utils";
|
||||
import { isBoolean, hasTypedProperty, isObject, isString, cssNames } from "../../utils";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { Spinner } from "../spinner";
|
||||
import type { KubeAuthUpdate } from "../../../common/cluster-types";
|
||||
import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry";
|
||||
import { requestClusterActivation } from "../../ipc";
|
||||
import type { NavigateToEntitySettings } from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import navigateToEntitySettingsInjectable from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable";
|
||||
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
|
||||
import type { ClusterConnectionStatus } from "./cluster-status.state.injectable";
|
||||
import clusterConnectionStatusStateInjectable from "./cluster-status.state.injectable";
|
||||
|
||||
export interface ClusterStatusProps {
|
||||
className?: IClassName;
|
||||
@ -31,78 +31,47 @@ export interface ClusterStatusProps {
|
||||
interface Dependencies {
|
||||
navigateToEntitySettings: NavigateToEntitySettings;
|
||||
entityRegistry: CatalogEntityRegistry;
|
||||
state: ClusterConnectionStatus;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedClusterStatus extends React.Component<ClusterStatusProps & Dependencies> {
|
||||
@observable authOutput: KubeAuthUpdate[] = [];
|
||||
@observable isReconnecting = false;
|
||||
const NonInjectedClusterStatus = observer((props: ClusterStatusProps & Dependencies) => {
|
||||
const {
|
||||
cluster,
|
||||
navigateToEntitySettings,
|
||||
state,
|
||||
className,
|
||||
entityRegistry,
|
||||
} = props;
|
||||
const entity = entityRegistry.getById(cluster.id);
|
||||
const clusterName = entity?.getName() ?? cluster.name;
|
||||
|
||||
constructor(props: ClusterStatusProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get cluster(): Cluster {
|
||||
return this.props.cluster;
|
||||
}
|
||||
|
||||
@computed get entity() {
|
||||
return this.props.entityRegistry.getById(this.cluster.id);
|
||||
}
|
||||
|
||||
@computed get hasErrors(): boolean {
|
||||
return this.authOutput.some(({ isError }) => isError);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
ipcRendererOn(`cluster:${this.cluster.id}:connection-update`, (evt, res: unknown) => {
|
||||
if (
|
||||
isObject(res)
|
||||
&& hasTypedProperty(res, "message", isString)
|
||||
&& hasTypedProperty(res, "isError", isBoolean)
|
||||
) {
|
||||
this.authOutput.push(res);
|
||||
} else {
|
||||
console.warn(`Got invalid connection update for ${this.cluster.id}`, { update: res });
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<ClusterStatusProps>): void {
|
||||
if (prevProps.cluster.id !== this.props.cluster.id) {
|
||||
this.isReconnecting = false;
|
||||
this.authOutput = [];
|
||||
}
|
||||
}
|
||||
|
||||
reconnect = async () => {
|
||||
this.authOutput = [];
|
||||
this.isReconnecting = true;
|
||||
const reconnect = async () => {
|
||||
runInAction(() => {
|
||||
state.resetAuthOutput();
|
||||
state.setAsReconnecting();
|
||||
});
|
||||
|
||||
try {
|
||||
await requestClusterActivation(this.cluster.id, true);
|
||||
await requestClusterActivation(cluster.id, true);
|
||||
} catch (error) {
|
||||
this.authOutput.push({
|
||||
state.appendAuthUpdate({
|
||||
message: String(error),
|
||||
isError: true,
|
||||
});
|
||||
} finally {
|
||||
this.isReconnecting = false;
|
||||
state.clearReconnectingState();
|
||||
}
|
||||
};
|
||||
|
||||
manageProxySettings = () => {
|
||||
this.props.navigateToEntitySettings(this.cluster.id, "proxy");
|
||||
};
|
||||
const manageProxySettings = () => navigateToEntitySettings(cluster.id, "proxy");
|
||||
|
||||
renderAuthenticationOutput() {
|
||||
const renderAuthenticationOutput = () => {
|
||||
return (
|
||||
<pre>
|
||||
{
|
||||
this.authOutput.map(({ message, isError }, index) => (
|
||||
state.authOutput
|
||||
.get()
|
||||
.map(({ message, isError }, index) => (
|
||||
<p key={index} className={cssNames({ error: isError })}>
|
||||
{message.trim()}
|
||||
</p>
|
||||
@ -110,10 +79,10 @@ class NonInjectedClusterStatus extends React.Component<ClusterStatusProps & Depe
|
||||
}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderStatusIcon() {
|
||||
if (this.hasErrors) {
|
||||
const renderStatusIcon = () => {
|
||||
if (state.hasErrorOutput.get()) {
|
||||
return <Icon material="cloud_off" className={styles.icon} />;
|
||||
}
|
||||
|
||||
@ -122,28 +91,28 @@ class NonInjectedClusterStatus extends React.Component<ClusterStatusProps & Depe
|
||||
<Spinner singleColor={false} className={styles.spinner} />
|
||||
<pre className="kube-auth-out">
|
||||
<p>
|
||||
{this.isReconnecting ? "Reconnecting" : "Connecting"}
|
||||
{state.isReconnecting.get() ? "Reconnecting" : "Connecting"}
|
||||
…
|
||||
</p>
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderReconnectionHelp() {
|
||||
if (this.hasErrors && !this.isReconnecting) {
|
||||
const renderReconnectionHelp = () => {
|
||||
if (state.hasErrorOutput.get() && !state.isReconnecting.get()) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
primary
|
||||
label="Reconnect"
|
||||
className="box center"
|
||||
onClick={this.reconnect}
|
||||
waiting={this.isReconnecting}
|
||||
onClick={reconnect}
|
||||
waiting={state.isReconnecting.get()}
|
||||
/>
|
||||
<a
|
||||
className="box center interactive"
|
||||
onClick={this.manageProxySettings}
|
||||
onClick={manageProxySettings}
|
||||
>
|
||||
Manage Proxy Settings
|
||||
</a>
|
||||
@ -152,26 +121,25 @@ class NonInjectedClusterStatus extends React.Component<ClusterStatusProps & Depe
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={cssNames(styles.status, "flex column box center align-center justify-center", this.props.className)}>
|
||||
<div className={cssNames(styles.status, "flex column box center align-center justify-center", className)}>
|
||||
<div className="flex items-center column gaps">
|
||||
<h2>{this.entity?.getName() ?? this.cluster.name}</h2>
|
||||
{this.renderStatusIcon()}
|
||||
{this.renderAuthenticationOutput()}
|
||||
{this.renderReconnectionHelp()}
|
||||
<h2>{clusterName}</h2>
|
||||
{renderStatusIcon()}
|
||||
{renderAuthenticationOutput()}
|
||||
{renderReconnectionHelp()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const ClusterStatus = withInjectables<Dependencies, ClusterStatusProps>(NonInjectedClusterStatus, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
navigateToEntitySettings: di.inject(navigateToEntitySettingsInjectable),
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
state: di.inject(clusterConnectionStatusStateInjectable).forCluster(props.cluster.id),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ const clusterViewRouteParametersInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(clusterViewRouteInjectable);
|
||||
const pathParameters = di.inject(routePathParametersInjectable, route);
|
||||
const pathParameters = di.inject(routePathParametersInjectable)(route);
|
||||
|
||||
return {
|
||||
clusterId: computed(() => pathParameters.get().clusterId),
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
import "./cluster-view.scss";
|
||||
import React from "react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, makeObservable, reaction } from "mobx";
|
||||
import { when, computed, makeObservable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ClusterStatus } from "./cluster-status";
|
||||
import type { ClusterFrameHandler } from "./cluster-frame-handler";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import type { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { requestClusterActivation } from "../../ipc";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
|
||||
@ -20,17 +20,24 @@ import clusterViewRouteParametersInjectable from "./cluster-view-route-parameter
|
||||
import clusterFrameHandlerInjectable from "./cluster-frame-handler.injectable";
|
||||
import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry";
|
||||
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
|
||||
import type { ClusterConnectionStatusState } from "./cluster-status.state.injectable";
|
||||
import clusterConnectionStatusStateInjectable from "./cluster-status.state.injectable";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
import type { Disposer } from "../../utils";
|
||||
import { disposer } from "../../utils";
|
||||
|
||||
interface Dependencies {
|
||||
clusterId: IComputedValue<string>;
|
||||
clusterId: IComputedValue<string | undefined>;
|
||||
clusterFrames: ClusterFrameHandler;
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
entityRegistry: CatalogEntityRegistry;
|
||||
clusterConnectionStatusState: ClusterConnectionStatusState;
|
||||
clusterStore: ClusterStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedClusterView extends React.Component<Dependencies> {
|
||||
private readonly store = ClusterStore.getInstance();
|
||||
private navigateToClusterDisposer?: Disposer;
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
@ -42,13 +49,30 @@ class NonInjectedClusterView extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
@computed get cluster(): Cluster | undefined {
|
||||
return this.store.getById(this.clusterId);
|
||||
const { clusterId } = this;
|
||||
|
||||
if (!clusterId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private readonly isViewLoaded = computed(() => this.props.clusterFrames.hasLoadedView(this.clusterId), {
|
||||
return this.props.clusterStore.getById(clusterId);
|
||||
}
|
||||
|
||||
private readonly isViewLoaded = computed(
|
||||
() => {
|
||||
const { clusterId } = this;
|
||||
|
||||
if (!clusterId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.props.clusterFrames.hasLoadedView(clusterId);
|
||||
},
|
||||
{
|
||||
keepAlive: true,
|
||||
requiresReaction: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@computed get isReady(): boolean {
|
||||
const { cluster } = this;
|
||||
@ -68,8 +92,13 @@ class NonInjectedClusterView extends React.Component<Dependencies> {
|
||||
bindEvents() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.clusterId, async (clusterId) => {
|
||||
// TODO: replace with better handling
|
||||
if (clusterId && !this.props.entityRegistry.getById(clusterId)) {
|
||||
this.navigateToClusterDisposer?.();
|
||||
|
||||
if (!clusterId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.props.entityRegistry.getById(clusterId)) {
|
||||
return this.props.navigateToCatalog(); // redirect to catalog when the clusterId does not correspond to an entity
|
||||
}
|
||||
|
||||
@ -77,15 +106,30 @@ class NonInjectedClusterView extends React.Component<Dependencies> {
|
||||
this.props.clusterFrames.initView(clusterId);
|
||||
requestClusterActivation(clusterId, false); // activate and fetch cluster's state from main
|
||||
this.props.entityRegistry.activeEntity = clusterId;
|
||||
|
||||
const navigateToClusterDisposer = disposer(
|
||||
when(
|
||||
() => this.cluster?.disconnected === false,
|
||||
() => {
|
||||
navigateToClusterDisposer.push(when(
|
||||
// The clusterId check makes sure that we are still talking about the same cluster
|
||||
() => (this.cluster?.disconnected ?? true) && this.clusterId === clusterId,
|
||||
() => {
|
||||
this.props.navigateToCatalog();
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.navigateToClusterDisposer = navigateToClusterDisposer;
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
navigateToClusterDisposer,
|
||||
]);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
|
||||
reaction(() => [this.cluster?.ready, this.cluster?.disconnected], ([, disconnected]) => {
|
||||
if (this.isViewLoaded.get() && disconnected) {
|
||||
this.props.navigateToCatalog(); // redirect to catalog when active cluster get disconnected/not available
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -114,6 +158,8 @@ export const ClusterView = withInjectables<Dependencies>(NonInjectedClusterView,
|
||||
navigateToCatalog: di.inject(navigateToCatalogInjectable),
|
||||
clusterFrames: di.inject(clusterFrameHandlerInjectable),
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
clusterConnectionStatusState: di.inject(clusterConnectionStatusStateInjectable),
|
||||
clusterStore: di.inject(clusterStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import type { CommandOverlay } from "./command-overlay.injectable";
|
||||
import commandOverlayInjectable from "./command-overlay.injectable";
|
||||
import type { ipcRendererOn } from "../../../common/ipc";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { AddWindowEventListener } from "../../window/event-listener.injectable";
|
||||
import windowAddEventListenerInjectable from "../../window/event-listener.injectable";
|
||||
@ -23,6 +24,7 @@ import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluste
|
||||
import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
||||
import legacyOnChannelListenInjectable from "../../ipc/legacy-channel-listen.injectable";
|
||||
import { onKeyboardShortcut } from "../../utils/on-keyboard-shortcut";
|
||||
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
addWindowEventListener: AddWindowEventListener;
|
||||
@ -31,6 +33,7 @@ interface Dependencies {
|
||||
matchedClusterId: IComputedValue<ClusterId | undefined>;
|
||||
isMac: boolean;
|
||||
legacyOnChannelListen: typeof ipcRendererOn;
|
||||
entityRegistry: CatalogEntityRegistry;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -97,5 +100,6 @@ export const CommandContainer = withInjectables<Dependencies>(NonInjectedCommand
|
||||
matchedClusterId: di.inject(matchedClusterIdInjectable),
|
||||
isMac: di.inject(isMacInjectable),
|
||||
legacyOnChannelListen: di.inject(legacyOnChannelListenInjectable),
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -2,32 +2,31 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed } from "mobx";
|
||||
import { matchPath } from "react-router";
|
||||
import currentPathInjectable from "./current-path.injectable";
|
||||
import type { Route } from "../../common/front-end-routing/front-end-route-injection-token";
|
||||
import { getOrInsertWith } from "../utils";
|
||||
|
||||
const routePathParametersInjectable = getInjectable({
|
||||
id: "route-path-parameters",
|
||||
|
||||
instantiate: (di, route: Route<unknown>) => {
|
||||
instantiate: (di) => {
|
||||
const currentPath = di.inject(currentPathInjectable);
|
||||
const pathParametersCache = new Map<Route<unknown>, IComputedValue<Partial<Record<string, string>>>>();
|
||||
|
||||
// TODO: Reuse typing from route for accuracy
|
||||
return computed((): Record<string, string> => {
|
||||
const match = matchPath(currentPath.get(), {
|
||||
return <Param>(route: Route<Param>): IComputedValue<Partial<Param>> => (
|
||||
getOrInsertWith(pathParametersCache, route, () => computed(() => (
|
||||
console.log(currentPath.get()),
|
||||
matchPath(currentPath.get(), {
|
||||
path: route.path,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
return match ? match.params : {};
|
||||
});
|
||||
})?.params ?? {}
|
||||
)))
|
||||
);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, route: Route<unknown>) => route,
|
||||
}),
|
||||
});
|
||||
|
||||
export default routePathParametersInjectable;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user