diff --git a/src/jest.setup.ts b/src/jest.setup.ts index a234960386..872639f891 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -38,6 +38,6 @@ fetchMock.enableMocks(); // Mock __non_webpack_require__ for tests globalThis.__non_webpack_require__ = jest.fn(); -process.on("unhandledRejection", (err) => { +process.on("unhandledRejection", (err: any) => { fail(err); }); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index b84939a5d3..4ac4074ca5 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -35,6 +35,7 @@ import plimit from "p-limit"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types"; import { storedKubeConfigFolder, toJS } from "../common/utils"; +import type { Response } from "request"; /** * Cluster @@ -642,8 +643,6 @@ export class Cluster implements ClusterModel, ClusterState { }; } - protected getAllowedNamespacesErrorCount = 0; - protected async getAllowedNamespaces() { if (this.accessibleNamespaces.length) { return this.accessibleNamespaces; @@ -655,24 +654,16 @@ export class Cluster implements ClusterModel, ClusterState { const { body: { items } } = await api.listNamespace(); const namespaces = items.map(ns => ns.metadata.name); - this.getAllowedNamespacesErrorCount = 0; // reset on success - return namespaces; } catch (error) { const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName); const namespaceList = [ctx.namespace].filter(Boolean); if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { - this.getAllowedNamespacesErrorCount += 1; + const { response } = error as HttpError & { response: Response }; - if (this.getAllowedNamespacesErrorCount > 3) { - // reset on send - this.getAllowedNamespacesErrorCount = 0; - - // then broadcast, make sure it is 3 successive attempts - logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error }); - broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); - } + logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body }); + broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); } return namespaceList; diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx index bcb5895413..ef4bc480d0 100644 --- a/src/renderer/components/+entity-settings/entity-settings.tsx +++ b/src/renderer/components/+entity-settings/entity-settings.tsx @@ -34,6 +34,7 @@ import type { EntitySettingsRouteParams } from "../../../common/routes"; import { groupBy } from "lodash"; import { SettingLayout } from "../layout/setting-layout"; import { HotbarIcon } from "../hotbar/hotbar-icon"; +import logger from "../../../common/logger"; interface Props extends RouteComponentProps { } @@ -45,6 +46,17 @@ export class EntitySettings extends React.Component { constructor(props: Props) { super(props); makeObservable(this); + + const { hash } = navigation.location; + + if (hash) { + const menuId = hash.slice(1); + const item = this.menuItems.find((item) => item.id === menuId); + + if (item) { + this.activeTab = item.id; + } + } } get entityId() { @@ -61,18 +73,10 @@ export class EntitySettings extends React.Component { return EntitySettingRegistry.getInstance().getItemsForKind(this.entity.kind, this.entity.apiVersion, this.entity.metadata.source); } - async componentDidMount() { - const { hash } = navigation.location; + get activeSetting() { + this.activeTab ||= this.menuItems[0]?.id; - if (hash) { - const item = this.menuItems.find((item) => item.title === hash.slice(1)); - - if (item) { - this.activeTab = item.id; - } - } - - this.ensureActiveTab(); + return this.menuItems.find((setting) => setting.id === this.activeTab); } onTabChange = (tabId: string) => { @@ -122,33 +126,31 @@ export class EntitySettings extends React.Component { ); } - ensureActiveTab() { - if (!this.activeTab) { - this.activeTab = this.menuItems[0]?.id; - } - } - render() { if (!this.entity) { - console.error("entity not found", this.entityId); + logger.error("[ENTITY-SETTINGS]: entity not found", this.entityId); return null; } - this.ensureActiveTab(); - const activeSetting = this.menuItems.find((setting) => setting.id === this.activeTab); + const { activeSetting } = this; + return ( -
-

{activeSetting.title}

-
- -
-
+ { + activeSetting && ( +
+

{activeSetting.title}

+
+ +
+
+ ) + }
); } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index a3c3f96b5f..7c7cdd2714 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -90,7 +90,7 @@ export class ClusterStatus extends React.Component { params: { entityId: this.props.clusterId, }, - fragment: "Proxy", + fragment: "proxy", })); }; diff --git a/src/renderer/components/layout/setting-layout.tsx b/src/renderer/components/layout/setting-layout.tsx index 45f665df1c..9f458658e8 100644 --- a/src/renderer/components/layout/setting-layout.tsx +++ b/src/renderer/components/layout/setting-layout.tsx @@ -23,7 +23,7 @@ import "./setting-layout.scss"; import React from "react"; import { observer } from "mobx-react"; -import { boundMethod, cssNames, IClassName } from "../../utils"; +import { cssNames, IClassName } from "../../utils"; import { navigation } from "../../navigation"; import { Icon } from "../icon"; @@ -36,17 +36,10 @@ export interface SettingLayoutProps extends React.DOMAttributes { back?: (evt: React.MouseEvent | KeyboardEvent) => void; } -function scrollToAnchor() { - const { hash } = window.location; - - if (hash) { - document.querySelector(`${hash}`).scrollIntoView(); - } -} - const defaultProps: Partial = { provideBackButtonNavigation: true, contentGaps: true, + back: () => navigation.goBack(), }; /** @@ -56,19 +49,14 @@ const defaultProps: Partial = { export class SettingLayout extends React.Component { static defaultProps = defaultProps as object; - @boundMethod - back(evt?: React.MouseEvent | KeyboardEvent) { - if (this.props.back) { - this.props.back(evt); - } else { - navigation.goBack(); - } - } - async componentDidMount() { - window.addEventListener("keydown", this.onEscapeKey); + const { hash } = window.location; - scrollToAnchor(); + if (hash) { + document.querySelector(hash)?.scrollIntoView(); + } + + window.addEventListener("keydown", this.onEscapeKey); } componentWillUnmount() { @@ -82,7 +70,7 @@ export class SettingLayout extends React.Component { if (evt.code === "Escape") { evt.stopPropagation(); - this.back(evt); + this.props.back(evt); } }; @@ -107,17 +95,18 @@ export class SettingLayout extends React.Component { {children}
- { this.props.provideBackButtonNavigation && ( -
-
- + { + this.props.provideBackButtonNavigation && ( +
+
+ +
+
- - -
- )} + ) + }
diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx index 62dd96a264..f99cee5823 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/index.tsx @@ -77,16 +77,15 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update ); } -const listNamespacesForbiddenHandlerDisplayedAt = new Map(); +const notificationLastDisplayedAt = new Map(); const intervalBetweenNotifications = 1000 * 60; // 60s function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void { - const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId); - const wasDisplayed = Boolean(lastDisplayedAt); + const lastDisplayedAt = notificationLastDisplayedAt.get(clusterId); const now = Date.now(); - if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) { - listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now); + if (!notificationLastDisplayedAt.has(clusterId) || (now - lastDisplayedAt) > intervalBetweenNotifications) { + notificationLastDisplayedAt.set(clusterId, now); } else { // don't bother the user too often return; @@ -94,21 +93,39 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: const notificationId = `list-namespaces-forbidden:${clusterId}`; + if (notificationsStore.getById(notificationId)) { + // notification is still visible + return; + } + Notifications.info( (
Add Accessible Namespaces -

Cluster {ClusterStore.getInstance().getById(clusterId).name} does not have permissions to list namespaces. Please add the namespaces you have access to.

+

+ Cluster {ClusterStore.getInstance().getById(clusterId).name} does not have permissions to list namespaces.{" "} + Please add the namespaces you have access to. +

-
), { id: notificationId, + /** + * Set the time when the notification is closed as well so that there is at + * least a minute between closing the notification as seeing it again + */ + onClose: () => notificationLastDisplayedAt.set(clusterId, Date.now()), } ); }