1
0
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:
Sebastian Malton 2022-04-05 11:24:50 -04:00
parent a9f4bcecb2
commit 853573afcb
28 changed files with 361 additions and 197 deletions

View File

@ -106,12 +106,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.size > 0;
}
getById(id: ClusterId | undefined): Cluster | undefined {
if (id) {
return this.clusters.get(id);
}
return undefined;
getById(id: ClusterId): Cluster | undefined {
return this.clusters.get(id);
}
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {

View File

@ -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;

View File

@ -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) {

View File

@ -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,
});

View File

@ -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 });
});
});
});

View File

@ -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();

View File

@ -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);
};
}

View File

@ -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);
};
},
});

View File

@ -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);
},
});

View File

@ -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),

View File

@ -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;

View File

@ -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() {

View File

@ -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),

View File

@ -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),

View File

@ -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() {

View File

@ -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),

View File

@ -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;

View File

@ -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),

View File

@ -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 {
releases: di.inject(removableReleasesInjectable),
releasesArePending: di.inject(releasesInjectable).pending,
selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces,
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
namespace: routeParameters.namespace,
};
},
},
);
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),
...di.inject(helmReleasesRouteParametersInjectable),
}),
});

View File

@ -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),

View File

@ -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 {
portForwardStore: di.inject(portForwardStoreInjectable),
forwardport: routeParameters.forwardport,
navigateToPortForwards: di.inject(navigateToPortForwardsInjectable),
};
},
},
);
export const PortForwards = withInjectables<Dependencies>(NonInjectedPortForwards, {
getProps: (di) => ({
portForwardStore: di.inject(portForwardStoreInjectable),
navigateToPortForwards: di.inject(navigateToPortForwardsInjectable),
...di.inject(portForwardsRouteParametersInjectable),
}),
});

View File

@ -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;

View File

@ -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;

View File

@ -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,89 +31,58 @@ 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) => (
<p key={index} className={cssNames({ error: isError })}>
{message.trim()}
</p>
))
state.authOutput
.get()
.map(({ message, isError }, index) => (
<p key={index} className={cssNames({ error: isError })}>
{message.trim()}
</p>
))
}
</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"}
&hellip;
</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="flex items-center column gaps">
<h2>{this.entity?.getName() ?? this.cluster.name}</h2>
{this.renderStatusIcon()}
{this.renderAuthenticationOutput()}
{this.renderReconnectionHelp()}
</div>
return (
<div className={cssNames(styles.status, "flex column box center align-center justify-center", className)}>
<div className="flex items-center column gaps">
<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),
}),
});

View File

@ -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),

View File

@ -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;
}
return this.props.clusterStore.getById(clusterId);
}
private readonly isViewLoaded = computed(() => this.props.clusterFrames.hasLoadedView(this.clusterId), {
keepAlive: true,
requiresReaction: true,
});
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),
}),
});

View File

@ -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),
}),
});

View File

@ -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(), {
path: route.path,
exact: true,
});
return match ? match.params : {};
});
return <Param>(route: Route<Param>): IComputedValue<Partial<Param>> => (
getOrInsertWith(pathParametersCache, route, () => computed(() => (
console.log(currentPath.get()),
matchPath(currentPath.get(), {
path: route.path,
exact: true,
})?.params ?? {}
)))
);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, route: Route<unknown>) => route,
}),
});
export default routePathParametersInjectable;