From 4c5cd2b7370051149cec1e4815dd7f895ce50ff8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:42:12 -0400 Subject: [PATCH 1/9] Bump concurrently from 5.2.0 to 5.3.0 (#4053) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index be99cf1890..6f7bbc58e9 100644 --- a/package.json +++ b/package.json @@ -323,7 +323,7 @@ "chart.js": "^2.9.4", "circular-dependency-plugin": "^5.2.2", "color": "^3.1.2", - "concurrently": "^5.2.0", + "concurrently": "^5.3.0", "css-loader": "^5.2.7", "deepdash": "^5.3.9", "dompurify": "^2.3.3", diff --git a/yarn.lock b/yarn.lock index 38ce254f43..3b67b121f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4136,10 +4136,10 @@ concat-stream@^1.5.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -concurrently@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.2.0.tgz#ead55121d08a0fc817085584c123cedec2e08975" - integrity sha512-XxcDbQ4/43d6CxR7+iV8IZXhur4KbmEJk1CetVMUqCy34z9l0DkszbY+/9wvmSnToTej0SYomc2WSRH+L0zVJw== +concurrently@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.3.0.tgz#7500de6410d043c912b2da27de3202cb489b1e7b" + integrity sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ== dependencies: chalk "^2.4.2" date-fns "^2.0.1" From e227702d9161e50d8dacdae25d7dfa21bdecc878 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:43:08 -0400 Subject: [PATCH 2/9] Bump sass from 1.41.1 to 1.43.2 (#4050) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6f7bbc58e9..ca08f1e91c 100644 --- a/package.json +++ b/package.json @@ -368,7 +368,7 @@ "react-select-event": "^5.1.0", "react-table": "^7.7.0", "react-window": "^1.8.5", - "sass": "^1.41.1", + "sass": "^1.43.2", "sass-loader": "^8.0.2", "sharp": "^0.29.1", "style-loader": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 3b67b121f6..43316cc677 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12339,10 +12339,10 @@ sass-loader@^8.0.2: schema-utils "^2.6.1" semver "^6.3.0" -sass@^1.32.13, sass@^1.41.1: - version "1.41.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.41.1.tgz#bca5bed2154192779c29f48fca9c644c60c38d98" - integrity sha512-vIjX7izRxw3Wsiez7SX7D+j76v7tenfO18P59nonjr/nzCkZuoHuF7I/Fo0ZRZPKr88v29ivIdE9BqGDgQD/Nw== +sass@^1.32.13, sass@^1.43.2: + version "1.43.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.2.tgz#c02501520c624ad6622529a8b3724eb08da82d65" + integrity sha512-DncYhjl3wBaPMMJR0kIUaH3sF536rVrOcqqVGmTZHQRRzj7LQlyGV7Mb8aCKFyILMr5VsPHwRYtyKpnKYlmQSQ== dependencies: chokidar ">=3.0.0 <4.0.0" From f1ce97ecc842eb94ff2559808f2ccce65fca8cb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:43:40 -0400 Subject: [PATCH 3/9] Bump @types/url-parse from 1.4.3 to 1.4.4 (#4052) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ca08f1e91c..958dee2fff 100644 --- a/package.json +++ b/package.json @@ -310,7 +310,7 @@ "@types/tcp-port-used": "^1.0.0", "@types/tempy": "^0.3.0", "@types/triple-beam": "^1.3.2", - "@types/url-parse": "^1.4.3", + "@types/url-parse": "^1.4.4", "@types/uuid": "^8.3.1", "@types/webdriverio": "^4.13.0", "@types/webpack": "^4.41.31", diff --git a/yarn.lock b/yarn.lock index 43316cc677..db6bc169f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2078,10 +2078,10 @@ resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.0.tgz#5cb0dff2a5f616fc8e0c61b482bf01fa20a03cec" integrity sha512-ZAbqul7QAKpM2h1PFGa5ETN27ulmqtj0QviYHasw9LffvXZvVHuraOx/FOsIPPDNGZN0Qo1nASxxSfMYOtSoCw== -"@types/url-parse@^1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.3.tgz#fba49d90f834951cb000a674efee3d6f20968329" - integrity sha512-4kHAkbV/OfW2kb5BLVUuUMoumB3CP8rHqlw48aHvFy5tf9ER0AfOonBlX29l/DD68G70DmyhRlSYfQPSYpC5Vw== +"@types/url-parse@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.4.tgz#ebeb0ec8b581318739cf73e9f9b186f610764255" + integrity sha512-KtQLad12+4T/NfSxpoDhmr22+fig3T7/08QCgmutYA6QSznSRmEtuL95GrhVV40/0otTEdFc+etRcCTqhh1q5Q== "@types/uuid@^8.3.1": version "8.3.1" From c5de0b1e007a668501a5fede431ebfe7f4c8b8c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:46:53 -0400 Subject: [PATCH 4/9] Bump playwright from 1.14.0 to 1.15.2 (#4051) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 24 ++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 958dee2fff..ab927fd245 100644 --- a/package.json +++ b/package.json @@ -353,7 +353,7 @@ "node-gyp": "7.1.2", "node-loader": "^1.0.3", "nodemon": "^2.0.13", - "playwright": "^1.14.0", + "playwright": "^1.15.2", "postcss": "^8.3.6", "postcss-loader": "4.3.0", "postinstall-postinstall": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index db6bc169f7..32ae056719 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9504,12 +9504,7 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.4: - version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== - -mime@^2.4.6, mime@^2.5.2: +mime@^2.4.4, mime@^2.4.6, mime@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== @@ -11002,10 +10997,10 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.14.0.tgz#18301b11f5278a446d36b5cf96f67db36ce2cd20" - integrity sha512-aR5oZ1iVsjQkGfYCjgYAmyMAVu0MQ0i8MgdnfdqDu9EVLfbnpuuFmTv/Rb7/Yjno1kOrDUP9+RyNC+zfG3wozA== +playwright@^1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.15.2.tgz#b350056d1fffbe5de5b1bdaca6ab73e35758baad" + integrity sha512-+Z+7ckihyxR6rK5q8DWC6eUbKARfXpyxpjNcoJfgwSr64lAOzjhyFQiPC/JkdIqhsLgZjxpWfl1S7fLb+wPkgA== dependencies: commander "^6.1.0" debug "^4.1.1" @@ -12956,14 +12951,7 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= -stack-utils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" - integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg== - dependencies: - escape-string-regexp "^2.0.0" - -stack-utils@^2.0.3: +stack-utils@^2.0.2, stack-utils@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== From 246305cd63c3c96a612f7f314e75cf4fe344defb Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 15 Oct 2021 09:06:24 -0400 Subject: [PATCH 5/9] Fix accessible namespaces notification not navigating to correct settings (#4048) --- src/jest.setup.ts | 2 +- src/main/cluster.ts | 17 ++---- .../+entity-settings/entity-settings.tsx | 54 ++++++++++--------- .../cluster-manager/cluster-status.tsx | 2 +- .../components/layout/setting-layout.tsx | 51 +++++++----------- src/renderer/ipc/index.tsx | 37 +++++++++---- 6 files changed, 81 insertions(+), 82 deletions(-) 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()), } ); } From dabfce3609ceaa42ee55d3d2cdb7413aa898c26b Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 15 Oct 2021 18:17:08 +0300 Subject: [PATCH 6/9] Update immer to version 9.x (#3882) --- package.json | 2 +- .../utils/__tests__/storageHelper.test.ts | 148 +++++++----------- src/renderer/utils/storageHelper.ts | 24 +-- yarn.lock | 8 +- 4 files changed, 74 insertions(+), 108 deletions(-) diff --git a/package.json b/package.json index ab927fd245..5a452a19d3 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,7 @@ "grapheme-splitter": "^1.0.4", "handlebars": "^4.7.7", "http-proxy": "^1.18.1", - "immer": "^8.0.4", + "immer": "^9.0.6", "joi": "^17.4.2", "js-yaml": "^3.14.0", "jsdom": "^16.7.0", diff --git a/src/renderer/utils/__tests__/storageHelper.test.ts b/src/renderer/utils/__tests__/storageHelper.test.ts index 11127ee3c4..57141450c1 100644 --- a/src/renderer/utils/__tests__/storageHelper.test.ts +++ b/src/renderer/utils/__tests__/storageHelper.test.ts @@ -19,149 +19,109 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { reaction } from "mobx"; -import { StorageAdapter, StorageHelper } from "../storageHelper"; +import { observable, reaction } from "mobx"; +import { StorageHelper } from "../storageHelper"; import { delay } from "../../../common/utils/delay"; -describe("renderer/utils/StorageHelper", () => { - describe("window.localStorage might be used as StorageAdapter", () => { - type StorageModel = string; +type StorageModel = { + [prop: string]: any /*json-serializable*/; + message?: string; + description?: any; +}; +describe("renderer/utils/StorageHelper", () => { + describe("Using custom StorageAdapter", () => { const storageKey = "ui-settings"; + const remoteStorageMock = observable.map(); let storageHelper: StorageHelper; + let storageHelperAsync: StorageHelper; beforeEach(() => { - localStorage.clear(); + remoteStorageMock.set(storageKey, { + message: "saved-before", // pretending as previously saved data + }); storageHelper = new StorageHelper(storageKey, { autoInit: false, - storage: localStorage, - defaultValue: "test", - }); - }); - - it("initialized with default value", async () => { - localStorage.setItem(storageKey, "saved"); // pretending it was saved previously - - expect(storageHelper.key).toBe(storageKey); - expect(storageHelper.defaultValue).toBe("test"); - expect(storageHelper.get()).toBe("test"); - - storageHelper.init(); - - expect(storageHelper.key).toBe(storageKey); - expect(storageHelper.defaultValue).toBe("test"); - expect(storageHelper.get()).toBe("saved"); - }); - - it("updates storage", async () => { - storageHelper.init(); - - storageHelper.set("test2"); - expect(localStorage.getItem(storageKey)).toBe("test2"); - - localStorage.setItem(storageKey, "test3"); - storageHelper.init({ force: true }); // reload from underlying storage and merge - expect(storageHelper.get()).toBe("test3"); - }); - }); - - describe("Using custom StorageAdapter", () => { - type SettingsStorageModel = { - [key: string]: any; - message: string; - }; - - const storageKey = "mySettings"; - const storageMock: Record = {}; - let storageHelper: StorageHelper; - let storageHelperAsync: StorageHelper; - let storageAdapter: StorageAdapter; - - const storageHelperDefaultValue: SettingsStorageModel = { - message: "hello-world", - anyOtherStorableData: 123, - }; - - beforeEach(() => { - storageMock[storageKey] = { - message: "saved-before", - } as SettingsStorageModel; - - storageAdapter = { - onChange: jest.fn(), - getItem: jest.fn((key: string) => { - return storageMock[key]; - }), - setItem: jest.fn((key: string, value: any) => { - storageMock[key] = value; - }), - removeItem: jest.fn((key: string) => { - delete storageMock[key]; - }), - }; - - storageHelper = new StorageHelper(storageKey, { - autoInit: false, - defaultValue: storageHelperDefaultValue, - storage: storageAdapter, + defaultValue: { + message: "blabla", + description: "default" + }, + storage: { + getItem(key: string): StorageModel { + return Object.assign( + storageHelper.defaultValue, + remoteStorageMock.get(key), + ); + }, + setItem(key: string, value: StorageModel) { + remoteStorageMock.set(key, value); + }, + removeItem(key: string) { + remoteStorageMock.delete(key); + } + }, }); storageHelperAsync = new StorageHelper(storageKey, { autoInit: false, - defaultValue: storageHelperDefaultValue, + defaultValue: storageHelper.defaultValue, storage: { - ...storageAdapter, - async getItem(key: string): Promise { + ...storageHelper.storage, + async getItem(key: string): Promise { await delay(500); // fake loading timeout - return storageAdapter.getItem(key); + return storageHelper.storage.getItem(key); } }, }); }); - it("loads data from storage with fallback to default-value", () => { - expect(storageHelper.get()).toEqual(storageHelperDefaultValue); + it("initialized with default value", async () => { storageHelper.init(); - - expect(storageHelper.get().message).toBe("saved-before"); - expect(storageAdapter.getItem).toHaveBeenCalledWith(storageHelper.key); + expect(storageHelper.key).toBe(storageKey); + expect(storageHelper.get()).toEqual(storageHelper.defaultValue); }); it("async loading from storage supported too", async () => { expect(storageHelperAsync.initialized).toBeFalsy(); storageHelperAsync.init(); await delay(300); - expect(storageHelperAsync.get()).toEqual(storageHelperDefaultValue); + expect(storageHelperAsync.get()).toEqual(storageHelper.defaultValue); await delay(200); expect(storageHelperAsync.get().message).toBe("saved-before"); }); it("set() fully replaces data in storage", () => { storageHelper.init(); - storageHelper.set({ message: "test2" }); - expect(storageHelper.get().message).toBe("test2"); - expect(storageMock[storageKey]).toEqual({ message: "test2" }); - expect(storageAdapter.setItem).toHaveBeenCalledWith(storageHelper.key, { message: "test2" }); + storageHelper.set({ message: "msg" }); + storageHelper.get().description = "desc"; + expect(storageHelper.get().message).toBe("msg"); + expect(storageHelper.get().description).toBe("desc"); + expect(remoteStorageMock.get(storageKey)).toEqual({ + message: "msg", + description: "desc", + } as StorageModel); }); it("merge() does partial data tree updates", () => { storageHelper.init(); storageHelper.merge({ message: "updated" }); - expect(storageHelper.get()).toEqual({ ...storageHelperDefaultValue, message: "updated" }); - expect(storageAdapter.setItem).toHaveBeenCalledWith(storageHelper.key, { ...storageHelperDefaultValue, message: "updated" }); + expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" }); + expect(remoteStorageMock.get(storageKey)).toEqual({ ...storageHelper.defaultValue, message: "updated" }); + // `draft` modified inside, returning `void` is expected storageHelper.merge(draft => { draft.message = "updated2"; }); - expect(storageHelper.get()).toEqual({ ...storageHelperDefaultValue, message: "updated2" }); + expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated2" }); + // returning object modifies `draft` as well storageHelper.merge(draft => ({ message: draft.message.replace("2", "3") })); - expect(storageHelper.get()).toEqual({ ...storageHelperDefaultValue, message: "updated3" }); + expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated3" }); }); }); diff --git a/src/renderer/utils/storageHelper.ts b/src/renderer/utils/storageHelper.ts index a7931ae41e..80c5df8fba 100755 --- a/src/renderer/utils/storageHelper.ts +++ b/src/renderer/utils/storageHelper.ts @@ -20,10 +20,9 @@ */ // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) - import { action, comparer, makeObservable, observable, toJS, when, } from "mobx"; -import produce, { Draft } from "immer"; -import { isEqual, isFunction, isPlainObject } from "lodash"; +import produce, { Draft, isDraft } from "immer"; +import { isEqual } from "lodash"; import logger from "../../main/logger"; export interface StorageAdapter { @@ -150,15 +149,22 @@ export class StorageHelper { @action merge(value: Partial | ((draft: Draft) => Partial | void)) { - const nextValue = produce(this.toJSON(), (state: Draft) => { - const newValue = isFunction(value) ? value(state) : value; + const nextValue = produce(this.toJSON(), (draft: Draft) => { - return isPlainObject(newValue) - ? Object.assign(state, newValue) // partial updates for returned plain objects - : newValue; + if (typeof value == "function") { + const newValue = value(draft); + + // merge returned plain objects from `value-as-callback` usage + // otherwise `draft` can be just modified inside a callback without returning any value (void) + if (newValue && !isDraft(newValue)) { + Object.assign(draft, newValue); + } + } else { + Object.assign(draft, value); + } }); - this.set(nextValue as T); + this.set(nextValue); } toJSON(): T { diff --git a/yarn.lock b/yarn.lock index 32ae056719..b6a5f1bbde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7278,10 +7278,10 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@^8.0.4: - version "8.0.4" - resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a" - integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ== +immer@^9.0.6: + version "9.0.6" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" + integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ== import-cwd@^3.0.0: version "3.0.0" From 052d12fc197f48c479705f9cfa8dc1a10e182f61 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 15 Oct 2021 12:11:46 -0400 Subject: [PATCH 7/9] Check object instanceof on all detail panels (#4054) * Check object instanceof on all detail panels Signed-off-by: Sebastian Malton * Fix unit tests - Remove prettier as snapShot testing is too tight of a bound Signed-off-by: Sebastian Malton --- package.json | 1 - .../+catalog/catalog-entity-item.tsx | 8 +- .../components/+catalog/catalog.test.tsx | 112 ++++++------------ .../+config-autoscalers/hpa-details.tsx | 26 ++-- .../limit-range-details.tsx | 20 +++- .../+config-maps/config-map-details.tsx | 42 ++++--- .../pod-disruption-budgets-details.tsx | 14 ++- .../resource-quota-details.tsx | 13 +- .../+config-secrets/secret-details.tsx | 13 +- .../+custom-resources/crd-details.tsx | 14 ++- .../crd-resource-details.tsx | 17 ++- .../components/+events/event-details.tsx | 14 ++- .../components/+events/kube-event-details.tsx | 14 ++- .../+namespaces/namespace-details.tsx | 12 +- .../+network-endpoints/endpoint-details.tsx | 13 +- .../+network-ingresses/ingress-details.tsx | 9 +- .../network-policy-details.tsx | 10 +- .../+network-services/service-details.tsx | 14 ++- .../components/+nodes/node-details.tsx | 12 +- .../pod-security-policy-details.tsx | 35 ++++-- .../storage-class-details.tsx | 16 ++- .../volume-claim-details.tsx | 8 ++ .../+storage-volumes/volume-details.tsx | 8 ++ .../+workloads-cronjobs/cronjob-details.tsx | 24 ++-- .../daemonset-details.tsx | 12 +- .../deployment-details.tsx | 12 +- .../+workloads-jobs/job-details.tsx | 12 +- .../+workloads-pods/pod-details.tsx | 12 +- .../replicaset-details.tsx | 12 +- .../statefulset-details.tsx | 12 +- .../kube-object-meta/kube-object-meta.tsx | 14 ++- yarn.lock | 5 - 32 files changed, 392 insertions(+), 168 deletions(-) diff --git a/package.json b/package.json index 5a452a19d3..bb426467d9 100644 --- a/package.json +++ b/package.json @@ -357,7 +357,6 @@ "postcss": "^8.3.6", "postcss-loader": "4.3.0", "postinstall-postinstall": "^2.1.0", - "prettier": "^2.4.1", "progress-bar-webpack-plugin": "^2.1.0", "randomcolor": "^0.6.2", "raw-loader": "^4.0.2", diff --git a/src/renderer/components/+catalog/catalog-entity-item.tsx b/src/renderer/components/+catalog/catalog-entity-item.tsx index b16406758f..1489ef9efc 100644 --- a/src/renderer/components/+catalog/catalog-entity-item.tsx +++ b/src/renderer/components/+catalog/catalog-entity-item.tsx @@ -21,7 +21,7 @@ import styles from "./catalog.module.css"; import React from "react"; import { action, computed } from "mobx"; -import type { CatalogEntity } from "../../api/catalog-entity"; +import { CatalogEntity } from "../../api/catalog-entity"; import type { ItemObject } from "../../../common/item.store"; import { Badge } from "../badge"; import { navigation } from "../../navigation"; @@ -33,7 +33,11 @@ import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry"; const css = makeCss(styles); export class CatalogEntityItem implements ItemObject { - constructor(public entity: T, private registry: CatalogEntityRegistry) {} + constructor(public entity: T, private registry: CatalogEntityRegistry) { + if (!(entity instanceof CatalogEntity)) { + throw Object.assign(new TypeError("CatalogEntityItem cannot wrap a non-CatalogEntity type"), { typeof: typeof entity, prototype: Object.getPrototypeOf(entity) }); + } + } get kind() { return this.entity.kind; diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index bb1ce35a5b..417496a038 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -26,7 +26,7 @@ import { Catalog } from "./catalog"; import { createMemoryHistory } from "history"; import { mockWindow } from "../../../../__mocks__/windowMock"; import { kubernetesClusterCategory } from "../../../common/catalog-entities/kubernetes-cluster"; -import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../../common/catalog"; +import { catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { CatalogEntityItem } from "./catalog-entity-item"; @@ -46,6 +46,18 @@ jest.mock("@electron/remote", () => { }; }); +class MockCatalogEntity extends CatalogEntity { + public apiVersion = "api"; + public kind = "kind"; + + constructor(data: CatalogEntityData, public onRun: (context: CatalogEntityActionContext) => void | Promise) { + super(data); + } + + public onContextMenuOpen(): void | Promise {} + public onSettingsOpen(): void | Promise {} +} + describe("", () => { const history = createMemoryHistory(); const mockLocation = { @@ -66,30 +78,21 @@ describe("", () => { url: "", }; - const catalogEntityUid = "a_catalogEntity_uid"; - const catalogEntity = { - enabled: true, - apiVersion: "api", - kind: "kind", - metadata: { - uid: catalogEntityUid, - name: "a catalog entity", - labels: { - test: "label", + function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise) { + return new MockCatalogEntity({ + metadata: { + uid: "a_catalogEntity_uid", + name: "a catalog entity", + labels: { + test: "label", + }, }, - }, - status: { - phase: "", - }, - spec: {}, - }; - const catalogEntityItemMethods = { - getId: () => catalogEntity.metadata.uid, - getName: () => catalogEntity.metadata.name, - onContextMenuOpen: () => {}, - onSettingsOpen: () => {}, - onRun: () => {}, - }; + status: { + phase: "", + }, + spec: {}, + }, onRun); + } beforeEach(() => { CatalogEntityDetailRegistry.createInstance(); @@ -117,11 +120,7 @@ describe("", () => { const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); - const catalogEntityItem = new CatalogEntityItem({ - ...catalogEntity, - ...catalogEntityItemMethods, - onRun, - }, catalogEntityRegistry); + const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); // mock as if there is a selected item > the detail panel opens jest @@ -130,29 +129,8 @@ describe("", () => { catalogEntityRegistry.addOnBeforeRun( (event) => { - expect(event.target).toMatchInlineSnapshot(` - Object { - "apiVersion": "api", - "enabled": true, - "getId": [Function], - "getName": [Function], - "kind": "kind", - "metadata": Object { - "labels": Object { - "test": "label", - }, - "name": "a catalog entity", - "uid": "a_catalogEntity_uid", - }, - "onContextMenuOpen": [Function], - "onRun": [MockFunction], - "onSettingsOpen": [Function], - "spec": Object {}, - "status": Object { - "phase": "", - }, - } - `); + expect(event.target.getId()).toBe("a_catalogEntity_uid"); + expect(event.target.getName()).toBe("a catalog entity"); setTimeout(() => { expect(onRun).toHaveBeenCalled(); @@ -178,11 +156,7 @@ describe("", () => { const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); - const catalogEntityItem = new CatalogEntityItem({ - ...catalogEntity, - ...catalogEntityItemMethods, - onRun, - }, catalogEntityRegistry); + const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); // mock as if there is a selected item > the detail panel opens jest @@ -216,11 +190,7 @@ describe("", () => { const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); - const catalogEntityItem = new CatalogEntityItem({ - ...catalogEntity, - ...catalogEntityItemMethods, - onRun, - }, catalogEntityRegistry); + const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); // mock as if there is a selected item > the detail panel opens jest @@ -255,11 +225,7 @@ describe("", () => { const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(() => done()); - const catalogEntityItem = new CatalogEntityItem({ - ...catalogEntity, - ...catalogEntityItemMethods, - onRun, - }, catalogEntityRegistry); + const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); // mock as if there is a selected item > the detail panel opens jest @@ -289,11 +255,7 @@ describe("", () => { const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); - const catalogEntityItem = new CatalogEntityItem({ - ...catalogEntity, - ...catalogEntityItemMethods, - onRun, - }, catalogEntityRegistry); + const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); // mock as if there is a selected item > the detail panel opens jest @@ -330,11 +292,7 @@ describe("", () => { const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); - const catalogEntityItem = new CatalogEntityItem({ - ...catalogEntity, - ...catalogEntityItemMethods, - onRun, - }, catalogEntityRegistry); + const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); // mock as if there is a selected item > the detail panel opens jest diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx index 4dd544e5b1..3e6c74b9bb 100644 --- a/src/renderer/components/+config-autoscalers/hpa-details.tsx +++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx @@ -33,6 +33,7 @@ import { Table, TableCell, TableHead, TableRow } from "../table"; import { apiManager } from "../../../common/k8s-api/api-manager"; import { KubeObjectMeta } from "../kube-object-meta"; import { getDetailsUrl } from "../kube-detail-params"; +import logger from "../../../common/logger"; export interface HpaDetailsProps extends KubeObjectDetailsProps { } @@ -80,17 +81,13 @@ export class HpaDetails extends React.Component { Current / Target { - hpa.getMetrics().map((metric, index) => { - const name = renderName(metric); - const values = hpa.getMetricValues(metric); - - return ( + hpa.getMetrics() + .map((metric, index) => ( - {name} - {values} + {renderName(metric)} + {hpa.getMetricValues(metric)} - ); - }) + )) } ); @@ -99,7 +96,16 @@ export class HpaDetails extends React.Component { render() { const { object: hpa } = this.props; - if (!hpa) return null; + if (!hpa) { + return null; + } + + if (!(hpa instanceof HorizontalPodAutoscaler)) { + logger.error("[HpaDetails]: passed object that is not an instanceof HorizontalPodAutoscaler", hpa); + + return null; + } + const { scaleTargetRef } = hpa.spec; return ( diff --git a/src/renderer/components/+config-limit-ranges/limit-range-details.tsx b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx index e9acbd0a22..14ecbec7ed 100644 --- a/src/renderer/components/+config-limit-ranges/limit-range-details.tsx +++ b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx @@ -28,6 +28,7 @@ import { LimitPart, LimitRange, LimitRangeItem, Resource } from "../../../common import { KubeObjectMeta } from "../kube-object-meta"; import { DrawerItem } from "../drawer/drawer-item"; import { Badge } from "../badge"; +import logger from "../../../common/logger"; interface Props extends KubeObjectDetailsProps { } @@ -57,15 +58,13 @@ function renderResourceLimits(limit: LimitRangeItem, resource: Resource) { function renderLimitDetails(limits: LimitRangeItem[], resources: Resource[]) { - return resources.map(resource => + return resources.map(resource => ( { - limits.map(limit => - renderResourceLimits(limit, resource) - ) + limits.map(limit => renderResourceLimits(limit, resource)) } - ); + )); } @observer @@ -73,7 +72,16 @@ export class LimitRangeDetails extends React.Component { render() { const { object: limitRange } = this.props; - if (!limitRange) return null; + if (!limitRange) { + return null; + } + + if (!(limitRange instanceof LimitRange)) { + logger.error("[LimitRangeDetails]: passed object that is not an instanceof LimitRange", limitRange); + + return null; + } + const containerLimits = limitRange.getContainerLimits(); const podLimits = limitRange.getPodLimits(); const pvcLimits = limitRange.getPVCLimits(); diff --git a/src/renderer/components/+config-maps/config-map-details.tsx b/src/renderer/components/+config-maps/config-map-details.tsx index 6150aa1ad9..082a340a88 100644 --- a/src/renderer/components/+config-maps/config-map-details.tsx +++ b/src/renderer/components/+config-maps/config-map-details.tsx @@ -30,8 +30,9 @@ import { Input } from "../input"; import { Button } from "../button"; import { configMapsStore } from "./config-maps.store"; import type { KubeObjectDetailsProps } from "../kube-object-details"; -import type { ConfigMap } from "../../../common/k8s-api/endpoints"; +import { ConfigMap } from "../../../common/k8s-api/endpoints"; import { KubeObjectMeta } from "../kube-object-meta"; +import logger from "../../../common/logger"; interface Props extends KubeObjectDetailsProps { } @@ -82,7 +83,16 @@ export class ConfigMapDetails extends React.Component { render() { const { object: configMap } = this.props; - if (!configMap) return null; + if (!configMap) { + return null; + } + + if (!(configMap instanceof ConfigMap)) { + logger.error("[ConfigMapDetails]: passed object that is not an instanceof ConfigMap", configMap); + + return null; + } + const data = Array.from(this.data.entries()); return ( @@ -93,22 +103,20 @@ export class ConfigMapDetails extends React.Component { <> { - data.map(([name, value]) => { - return ( -
-
{name}
-
- this.data.set(name, v)} - /> -
+ data.map(([name, value]) => ( +
+
{name}
+
+ this.data.set(name, v)} + />
- ); - }) +
+ )) }