From a0fb8b593313cb5721e8fb931c0acf8006f67c71 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 5 May 2021 22:06:49 +0300 Subject: [PATCH] mobx-6 migration -- part 1 Signed-off-by: Roman --- docs/extensions/guides/stores.md | 2 - package.json | 7 +- src/common/cluster-store.ts | 4 +- src/common/configure-packages.ts | 38 ++++ src/common/hotbar-store.ts | 4 +- src/common/ipc/ipc.ts | 2 +- src/common/user-store.ts | 4 +- src/common/utils/autobind.ts | 49 +---- src/extensions/extension-discovery.ts | 2 - src/extensions/extension-loader.ts | 15 +- src/extensions/extensions-store.ts | 4 +- .../__tests__/page-registry.test.ts | 2 +- src/extensions/registries/page-registry.ts | 66 ++++--- src/extensions/renderer-api/navigation.ts | 8 +- src/jest.setup.ts | 5 + src/main/catalog-pusher.ts | 4 +- src/main/catalog-sources/kubeconfig-sync.ts | 2 +- src/main/cluster-manager.ts | 2 +- src/main/cluster.ts | 12 +- src/main/extension-filesystem.ts | 4 +- src/main/index.ts | 2 + src/renderer/bootstrap.tsx | 3 + .../+apps-releases/release-details.tsx | 2 +- .../+apps-releases/release.store.ts | 2 +- .../+config-maps/config-map-details.tsx | 4 +- .../components/+custom-resources/crd-list.tsx | 2 - .../components/+custom-resources/crd.store.ts | 2 +- .../components/+namespaces/namespace.store.ts | 2 - src/renderer/components/app.tsx | 2 +- src/renderer/components/dialog/dialog.tsx | 2 +- .../components/dock/dock-tab.store.ts | 2 +- .../error-boundary/error-boundary.tsx | 2 +- .../components/input/search-input-url.tsx | 1 - .../kube-object/kube-object-details.tsx | 8 +- src/renderer/components/table/table.tsx | 2 - src/renderer/hooks/useStorage.ts | 2 +- src/renderer/item.store.ts | 2 +- src/renderer/kube-object.store.ts | 6 +- src/renderer/navigation/helpers.ts | 4 +- src/renderer/navigation/history.ts | 9 +- src/renderer/navigation/index.ts | 1 + src/renderer/navigation/page-param.ts | 172 ++++++++---------- .../utils/__tests__/storageHelper.test.ts | 2 +- src/renderer/utils/createStorage.ts | 4 +- src/renderer/utils/storageHelper.ts | 25 +-- tsconfig.json | 1 + yarn.lock | 165 +++++++++++------ 47 files changed, 350 insertions(+), 317 deletions(-) create mode 100644 src/common/configure-packages.ts diff --git a/docs/extensions/guides/stores.md b/docs/extensions/guides/stores.md index c8a5ec270d..704ca8ee18 100644 --- a/docs/extensions/guides/stores.md +++ b/docs/extensions/guides/stores.md @@ -51,8 +51,6 @@ export class ExamplePreferencesStore extends Store.ExtensionStore { @action protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { - const currentClusters = this.clusters.toJS(); + const currentClusters = toJS(this.clusters); const newClusters = new Map(); const removedClusters = new Map(); @@ -324,8 +324,6 @@ export class ClusterStore extends BaseStore { return toJS({ activeCluster: this.activeCluster, clusters: this.clustersList.map(cluster => cluster.toJSON()), - }, { - recurseEverything: true }); } } diff --git a/src/common/configure-packages.ts b/src/common/configure-packages.ts new file mode 100644 index 0000000000..2483e79b92 --- /dev/null +++ b/src/common/configure-packages.ts @@ -0,0 +1,38 @@ +// Setup default configuration for external npm-packages. +import * as Mobx from "mobx"; +import * as Immer from "immer"; + +const { isObservable, toJS, observable } = Mobx; + +/** + * Patch-fixing mobx@6.toJS() to support partially observable objects as data-input. + * Otherwise it won't be recursively converted to corresponding non-observable plain JS-structure. + * @example + * data = {one: 1, two: observable.array([2])}; // "data" itself is non-observable + */ +Object.defineProperty(Mobx, "toJS", { + value(data: any) { + if (typeof data === "object" && !isObservable(data)) { + return toJS(observable.box(data).get()); + } + return toJS(data); + } +}); + +export default function configurePackages() { + // Docs: https://mobx.js.org/configuration.html + Mobx.configure({ + enforceActions: "never", + isolateGlobalState: true, + + // TODO: enable later (read more: https://mobx.js.org/migrating-from-4-or-5.html) + // computedRequiresReaction: true, + // reactionRequiresObservable: true, + // observableRequiresReaction: true, + }); + + // Docs: https://immerjs.github.io/immer/ + // Required for `storage-helper.ts` + Immer.setAutoFreeze(false); // allow to merge mobx observables + Immer.enableMapSet(); // allow to merge maps and sets +} diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index e4a871f093..9a9bbd9594 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -219,8 +219,6 @@ export class HotbarStore extends BaseStore { activeHotbarId: this.activeHotbarId }; - return toJS(model, { - recurseEverything: true, - }); + return toJS(model); } } diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index ebb3520fa3..390f4b22fb 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -18,7 +18,7 @@ export async function requestMain(channel: string, ...args: any[]) { } function getSubFrames(): ClusterFrameInfo[] { - return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); + return toJS(Array.from(clusterFrameMap.values())); } export async function broadcastMessage(channel: string, ...args: any[]) { diff --git a/src/common/user-store.ts b/src/common/user-store.ts index ab07838f9b..2c1f800469 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -285,9 +285,7 @@ export class UserStore extends BaseStore { }, }; - return toJS(model, { - recurseEverything: true, - }); + return toJS(model); } } diff --git a/src/common/utils/autobind.ts b/src/common/utils/autobind.ts index b5c706e362..3c7d1a7871 100644 --- a/src/common/utils/autobind.ts +++ b/src/common/utils/autobind.ts @@ -1,45 +1,10 @@ -// Decorator for binding class methods -// Can be applied to class or single method as @autobind() -type Constructor = new (...args: any[]) => T; +// Decorator for binding class methods to proper "this"-context. +// API: https://github.com/andreypopp/autobind-decorator +import bindMethodOrClass from "autobind-decorator" -export function autobind() { - return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) { - if (target instanceof Function) return bindClass(target); - else return bindMethod(target, prop, descriptor); +// TODO: unwrap, replace usages of @autobind() to @autobind +export function autobind() { + return function (target: { new(...args: any[]): T } | object, prop?: string, descriptor?: PropertyDescriptor) { + return bindMethodOrClass(target, prop, descriptor) as any; }; } - -function bindClass(constructor: T) { - const proto = constructor.prototype; - const descriptors = Object.getOwnPropertyDescriptors(proto); - const skipMethod = (methodName: string) => { - return methodName === "constructor" - || typeof descriptors[methodName].value !== "function"; - }; - - Object.keys(descriptors).forEach(prop => { - if (skipMethod(prop)) return; - const boundDescriptor = bindMethod(proto, prop, descriptors[prop]); - - Object.defineProperty(proto, prop, boundDescriptor); - }); -} - -function bindMethod(target: object, prop?: string, descriptor?: PropertyDescriptor) { - if (!descriptor || typeof descriptor.value !== "function") { - throw new Error(`@autobind() must be used on class or method only`); - } - const { value: func, enumerable, configurable } = descriptor; - const boundFunc = new WeakMap(); - - return Object.defineProperty(target, prop, { - enumerable, - configurable, - get() { - if (this === target) return func; // direct access from prototype - if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this)); - - return boundFunc.get(this); - } - }); -} diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 925a2e5138..afdf1aebf1 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -444,8 +444,6 @@ export class ExtensionDiscovery extends Singleton { toJSON(): ExtensionDiscoveryChannelMessage { return toJS({ isLoaded: this.isLoaded - }, { - recurseEverything: true }); } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index e7ec6d9529..6687efd1a8 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -1,7 +1,7 @@ import { app, ipcRenderer, remote } from "electron"; import { EventEmitter } from "events"; import { isEqual } from "lodash"; -import { action, computed, observable, reaction, toJS, when } from "mobx"; +import { action, computed, observable, reaction, when } from "mobx"; import path from "path"; import { getHostedCluster } from "../common/cluster-store"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; @@ -40,7 +40,7 @@ export class ExtensionLoader extends Singleton { whenLoaded = when(() => this.isLoaded); @computed get userExtensions(): Map { - const extensions = this.extensions.toJS(); + const extensions = this.toJSON(); extensions.forEach((ext, extId) => { if (ext.isBundled) { @@ -54,7 +54,7 @@ export class ExtensionLoader extends Singleton { @computed get userExtensionsByName(): Map { const extensions = new Map(); - for (const [, val] of this.instances.toJS()) { + for (const [, val] of this.instances.toJSON()) { if (val.isBundled) { continue; } @@ -135,7 +135,7 @@ export class ExtensionLoader extends Singleton { } } - protected async initMain() { + protected async initMain() { this.isLoaded = true; this.loadOnMain(); @@ -152,7 +152,7 @@ export class ExtensionLoader extends Singleton { }); } - protected async initRenderer() { + protected async initRenderer() { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { this.isLoaded = true; this.syncExtensions(extensions); @@ -311,10 +311,7 @@ export class ExtensionLoader extends Singleton { } toJSON(): Map { - return toJS(this.extensions, { - exportMapsAsObjects: false, - recurseEverything: true, - }); + return new Map(this.extensions.toJSON()); } broadcastExtensions(main = true) { diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 60fcff3c7c..1c963833e1 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -47,9 +47,7 @@ export class ExtensionsStore extends BaseStore { toJSON(): LensExtensionsStoreModel { return toJS({ - extensions: this.state.toJSON(), - }, { - recurseEverything: true + extensions: Object.fromEntries(this.state.toJSON()), }); } } diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 1d8598e451..7b2160d6b7 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -54,7 +54,7 @@ describe("getPageUrl", () => { }); it("gets page url with custom params", () => { - const params: PageParams = { test1: "one", test2: "2" }; + const params: PageParams = { test1: "one", test2: "2" }; const searchParams = new URLSearchParams(params); const pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params }); diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index 0ec6f27da0..93d28f4fb0 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -3,7 +3,7 @@ import React from "react"; import { observer } from "mobx-react"; import { BaseRegistry } from "./base-registry"; -import { LensExtension, sanitizeExtensionName } from "../lens-extension"; +import { LensExtension, LensExtensionId, sanitizeExtensionName } from "../lens-extension"; import { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; import { createPageParam } from "../../renderer/navigation/helpers"; @@ -13,21 +13,18 @@ export interface PageRegistration { * When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension */ id?: string; - params?: PageParams; + params?: PageParams, "name" | "prefix">>; components: PageComponents; } -// exclude "name" field since provided as key in page.params -export type ExtensionPageParamInit = Omit; - export interface PageComponents { Page: React.ComponentType; } -export interface PageTarget

{ +export interface PageTarget { extensionId?: string; pageId?: string; - params?: P; + params?: PageParams; } export interface PageParams { @@ -62,13 +59,11 @@ export function getExtensionPageUrl(target: PageTarget): string { if (registeredPage?.params) { Object.entries(registeredPage.params).forEach(([name, param]) => { - const paramValue = param.stringify(targetParams[name]); + pageUrl.searchParams.delete(name); // first off, clear existing value(s) - if (param.init.skipEmpty && param.isEmpty(paramValue)) { - pageUrl.searchParams.delete(name); - } else { - pageUrl.searchParams.set(name, paramValue); - } + param.stringify(targetParams[name]).forEach(value => { + pageUrl.searchParams.append(name, value); + }); }); } @@ -79,7 +74,7 @@ export class PageRegistry extends BaseRegistry protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { const { id: pageId } = page; const extensionId = ext.name; - const params = this.normalizeParams(page.params); + const params = this.normalizeParams(extensionId, page.params); const components = this.normalizeComponents(page.components, params); const url = getExtensionPageUrl({ extensionId, pageId }); @@ -92,25 +87,48 @@ export class PageRegistry extends BaseRegistry if (params) { const { Page } = components; + // inject extension's page component props.params components.Page = observer((props: object) => React.createElement(Page, { params, ...props })); } return components; } - protected normalizeParams(params?: PageParams): PageParams { - if (!params) { - return; - } - Object.entries(params).forEach(([name, value]) => { - const paramInit: PageParamInit = typeof value === "object" - ? { name, ...value } - : { name, defaultValue: value }; + protected normalizeParams(extensionId: LensExtensionId, params?: PageParams>): PageParams { + if (!params) return; + const normalizedParams: PageParams = {}; - params[paramInit.name] = createPageParam(paramInit); + Object.entries(params).forEach(([paramName, paramValue]) => { + const paramInit: PageParamInit = { + name: paramName, + prefix: `${extensionId}:`, + defaultValue: paramValue, + }; + + // handle non-string params + if (typeof paramValue !== "string") { + const { defaultValue: value, parse, stringify } = paramValue; + + const notAStringValue = typeof value !== "string" || ( + Array.isArray(value) && !value.every(value => typeof value === "string") + ); + + if (notAStringValue && !(parse || stringify)) { + throw new Error( + `PageRegistry: param's "${paramName}" initialization has failed: + paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"` + ) + } + + paramInit.defaultValue = value; + paramInit.parse = parse; + paramInit.stringify = stringify; + } + + normalizedParams[paramName] = createPageParam(paramInit); }); - return params as PageParams; + return normalizedParams; } getByPageTarget(target: PageTarget): RegisteredPage | null { diff --git a/src/extensions/renderer-api/navigation.ts b/src/extensions/renderer-api/navigation.ts index fd1f9196cc..87ba6e9f1d 100644 --- a/src/extensions/renderer-api/navigation.ts +++ b/src/extensions/renderer-api/navigation.ts @@ -1,12 +1,10 @@ -import { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; -import { navigation } from "../../renderer/navigation"; +import { navigation, PageParam, PageParamInit } from "../../renderer/navigation"; -export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param"; +export { PageParamInit, PageParam } from "../../renderer/navigation/page-param"; export { navigate, isActiveRoute } from "../../renderer/navigation/helpers"; export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details"; export { IURLParams } from "../../common/utils/buildUrl"; -// exporting to extensions-api version of helper without `isSystem` flag -export function createPageParam(init: PageParamInit) { +export function createPageParam(init: PageParamInit) { return new PageParam(init, navigation); } diff --git a/src/jest.setup.ts b/src/jest.setup.ts index d8c6ce9161..c77d78c9df 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -1,4 +1,9 @@ +import configurePackages from "./common/configure-packages"; import fetchMock from "jest-fetch-mock"; + +// setup default configuration for external npm-packages +configurePackages(); + // rewire global.fetch to call 'fetchMock' fetchMock.enableMocks(); diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index 532faf37e3..8522dea778 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -14,14 +14,14 @@ export class CatalogPusher { init() { const disposers: Disposer[] = []; - disposers.push(reaction(() => toJS(this.catalog.items, { recurseEverything: true }), (items) => { + disposers.push(reaction(() => toJS(this.catalog.items), (items) => { broadcastMessage("catalog:items", items); }, { fireImmediately: true, })); const listener = subscribeToBroadcast("catalog:broadcast", () => { - broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true })); + broadcastMessage("catalog:items", toJS(this.catalog.items)); }); disposers.push(() => unsubscribeFromBroadcast("catalog:broadcast", listener)); diff --git a/src/main/catalog-sources/kubeconfig-sync.ts b/src/main/catalog-sources/kubeconfig-sync.ts index f90fb85caf..44f46f0448 100644 --- a/src/main/catalog-sources/kubeconfig-sync.ts +++ b/src/main/catalog-sources/kubeconfig-sync.ts @@ -47,7 +47,7 @@ export class KubeconfigSyncManager extends Singleton { this.startNewSync(filePath); } - this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe(change => { + this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe_(change => { switch (change.type) { case "add": this.startNewSync(change.name); diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 3c4bcd0015..3c649ff579 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -14,7 +14,7 @@ export class ClusterManager extends Singleton { constructor() { super(); - reaction(() => toJS(ClusterStore.getInstance().clustersList, { recurseEverything: true }), () => { + reaction(() => toJS(ClusterStore.getInstance().clustersList), () => { this.updateCatalog(ClusterStore.getInstance().clustersList); }, { fireImmediately: true }); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 1b02e79a02..d757b36346 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -206,9 +206,7 @@ export class Cluster implements ClusterModel, ClusterState { @computed get prometheusPreferences(): ClusterPrometheusPreferences { const { prometheus, prometheusProvider } = this.preferences; - return toJS({ prometheus, prometheusProvider }, { - recurseEverything: true, - }); + return toJS({ prometheus, prometheusProvider }); } /** @@ -549,9 +547,7 @@ export class Cluster implements ClusterModel, ClusterState { accessibleNamespaces: this.accessibleNamespaces, }; - return toJS(model, { - recurseEverything: true - }); + return toJS(model); } /** @@ -571,9 +567,7 @@ export class Cluster implements ClusterModel, ClusterState { isGlobalWatchEnabled: this.isGlobalWatchEnabled, }; - return toJS(state, { - recurseEverything: true - }); + return toJS(state); } /** diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index 77bb51697a..9d807669ec 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -50,9 +50,7 @@ export class FilesystemProvisionerStore extends BaseStore { toJSON(): FSProvisionModel { return toJS({ - extensions: this.registeredExtensions.toJSON(), - }, { - recurseEverything: true + extensions: Object.fromEntries(this.registeredExtensions.toJSON()), }); } } diff --git a/src/main/index.ts b/src/main/index.ts index eec7c5006e..90e99d9ca9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -34,6 +34,7 @@ import { HotbarStore } from "../common/hotbar-store"; import { HelmRepoManager } from "./helm/helm-repo-manager"; import { KubeconfigSyncManager } from "./catalog-sources"; import { handleWsUpgrade } from "./proxy/ws-upgrade"; +import configurePackages from "../common/configure-packages"; const workingDir = path.join(app.getPath("appData"), appName); @@ -55,6 +56,7 @@ if (process.env.LENS_DISABLE_GPU) { app.disableHardwareAcceleration(); } +configurePackages(); mangleProxyEnv(); if (app.commandLine.getSwitchValue("proxy-server") !== "") { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 43b1c170eb..b20497c8b7 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -22,6 +22,9 @@ import { ThemeStore } from "./theme.store"; import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store"; import { DefaultProps } from "./mui-base-theme"; +import configurePackages from "../common/configure-packages"; + +configurePackages(); /** * If this is a development buid, wait a second to attach diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx index e326df2dc3..5bcca57c14 100644 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details.tsx @@ -51,7 +51,7 @@ export class ReleaseDetails extends Component { ); @disposeOnUnmount - secretWatcher = reaction(() => secretsStore.items.toJS(), () => { + secretWatcher = reaction(() => secretsStore.items.toJSON(), () => { if (!this.props.release) return; const { getReleaseSecret } = releaseStore; const { release } = this.props; diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index faeb509b71..3f8e23456c 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -20,7 +20,7 @@ export class ReleaseStore extends ItemStore { } watchAssociatedSecrets(): (() => void) { - return reaction(() => secretsStore.items.toJS(), () => { + return reaction(() => secretsStore.items.toJSON(), () => { if (this.isLoading) return; const newSecrets = this.getReleaseSecrets(); const amountChanged = newSecrets.length !== this.releaseSecrets.size; diff --git a/src/renderer/components/+config-maps/config-map-details.tsx b/src/renderer/components/+config-maps/config-map-details.tsx index 540a44c450..cc24904f04 100644 --- a/src/renderer/components/+config-maps/config-map-details.tsx +++ b/src/renderer/components/+config-maps/config-map-details.tsx @@ -39,7 +39,7 @@ export class ConfigMapDetails extends React.Component { try { this.isSaving = true; - await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() }); + await configMapsStore.update(configMap, { ...configMap, data: Object.fromEntries(this.data.toJSON()) }); Notifications.ok(

<>ConfigMap {configMap.getName()} successfully updated. @@ -54,7 +54,7 @@ export class ConfigMapDetails extends React.Component { const { object: configMap } = this.props; if (!configMap) return null; - const data = Object.entries(this.data.toJSON()); + const data = this.data.toJSON(); return (

diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index 57959fa1d0..4eba921448 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -14,8 +14,6 @@ import { Icon } from "../icon"; export const crdGroupsUrlParam = createPageParam({ name: "groups", - multiValues: true, - isSystem: true, defaultValue: [], }); diff --git a/src/renderer/components/+custom-resources/crd.store.ts b/src/renderer/components/+custom-resources/crd.store.ts index 64aefc1fe1..e4bd8c64bc 100644 --- a/src/renderer/components/+custom-resources/crd.store.ts +++ b/src/renderer/components/+custom-resources/crd.store.ts @@ -26,7 +26,7 @@ export class CRDStore extends KubeObjectStore { super(); // auto-init stores for crd-s - reaction(() => this.items.toJS(), items => items.forEach(initStore)); + reaction(() => this.items.toJSON(), items => items.forEach(initStore)); } protected sortItems(items: CustomResourceDefinition[]) { diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 4415f416b9..f9512a9031 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -9,8 +9,6 @@ const selectedNamespaces = createStorage("selected_namespa export const namespaceUrlParam = createPageParam({ name: "namespaces", - isSystem: true, - multiValues: true, defaultValue: [], }); diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 41fc63cf4d..8199da8c2e 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -78,7 +78,7 @@ export class App extends React.Component { whatInput.ask(); // Start to monitor user input device // Setup hosted cluster context - KubeObjectStore.defaultContext = clusterContext; + KubeObjectStore.defaultContext.set(clusterContext); kubeWatchApi.context = clusterContext; } diff --git a/src/renderer/components/dialog/dialog.tsx b/src/renderer/components/dialog/dialog.tsx index 81851616e8..c743ead09f 100644 --- a/src/renderer/components/dialog/dialog.tsx +++ b/src/renderer/components/dialog/dialog.tsx @@ -42,7 +42,7 @@ export class Dialog extends React.PureComponent { }; @disposeOnUnmount - closeOnNavigate = reaction(() => navigation.getPath(), () => this.close()); + closeOnNavigate = reaction(() => navigation.toString(), () => this.close()); public state: DialogState = { isOpen: this.props.isOpen, diff --git a/src/renderer/components/dock/dock-tab.store.ts b/src/renderer/components/dock/dock-tab.store.ts index 34a9b647bb..f3c083d234 100644 --- a/src/renderer/components/dock/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab.store.ts @@ -54,7 +54,7 @@ export class DockTabStore { } protected getStorableData(): DockTabStorageState { - const allTabsData = toJS(this.data, { recurseEverything: true }); + const allTabsData = toJS(this.data); return Object.fromEntries( Object.entries(allTabsData).map(([tabId, tabData]) => { diff --git a/src/renderer/components/error-boundary/error-boundary.tsx b/src/renderer/components/error-boundary/error-boundary.tsx index a628bfb3bc..3bc149e769 100644 --- a/src/renderer/components/error-boundary/error-boundary.tsx +++ b/src/renderer/components/error-boundary/error-boundary.tsx @@ -21,7 +21,7 @@ export class ErrorBoundary extends React.Component { @disposeOnUnmount resetOnNavigate = reaction( - () => navigation.getPath(), + () => navigation.toString(), () => this.setState({ error: null, errorInfo: null }) ); diff --git a/src/renderer/components/input/search-input-url.tsx b/src/renderer/components/input/search-input-url.tsx index c0e00d6e56..a3d2494bc1 100644 --- a/src/renderer/components/input/search-input-url.tsx +++ b/src/renderer/components/input/search-input-url.tsx @@ -8,7 +8,6 @@ import { createPageParam } from "../../navigation"; export const searchUrlParam = createPageParam({ name: "search", - isSystem: true, defaultValue: "", }); diff --git a/src/renderer/components/kube-object/kube-object-details.tsx b/src/renderer/components/kube-object/kube-object-details.tsx index b07b0753fb..dd63f9e3ff 100644 --- a/src/renderer/components/kube-object/kube-object-details.tsx +++ b/src/renderer/components/kube-object/kube-object-details.tsx @@ -18,7 +18,6 @@ import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry" */ export const kubeDetailsUrlParam = createPageParam({ name: "kube-details", - isSystem: true, }); /** @@ -30,7 +29,6 @@ export const kubeDetailsUrlParam = createPageParam({ */ export const kubeSelectedUrlParam = createPageParam({ name: "kube-selected", - isSystem: true, get defaultValue() { return kubeDetailsUrlParam.get(); } @@ -49,12 +47,12 @@ export function hideDetails() { export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) { const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : ""); - params.set(kubeDetailsUrlParam.urlName, selfLink); + params.set(kubeDetailsUrlParam.name, selfLink); if (resetSelected) { - params.delete(kubeSelectedUrlParam.urlName); + params.delete(kubeSelectedUrlParam.name); } else { - params.set(kubeSelectedUrlParam.urlName, kubeSelectedUrlParam.get()); + params.set(kubeSelectedUrlParam.name, kubeSelectedUrlParam.get()); } return `?${params}`; diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index e15bfabad2..5adbab2fee 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -46,12 +46,10 @@ export interface TableProps extends React.DOMAttributes { export const sortByUrlParam = createPageParam({ name: "sort", - isSystem: true, }); export const orderByUrlParam = createPageParam({ name: "order", - isSystem: true, }); @observer diff --git a/src/renderer/hooks/useStorage.ts b/src/renderer/hooks/useStorage.ts index febda7edb5..003b63bf86 100644 --- a/src/renderer/hooks/useStorage.ts +++ b/src/renderer/hooks/useStorage.ts @@ -1,6 +1,6 @@ import { useState } from "react"; import { createStorage } from "../utils"; -import { CreateObservableOptions } from "mobx/lib/api/observable"; +import { CreateObservableOptions } from "mobx"; export function useStorage(key: string, initialValue: T, options?: CreateObservableOptions) { const storage = createStorage(key, initialValue, options); diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index fb3b642f45..31990631ed 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -24,7 +24,7 @@ export abstract class ItemStore { } public getItems(): T[] { - return this.items.toJS(); + return this.items.toJSON(); } public getTotalCount(): number { diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 35f052d03f..ff64679ce7 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -18,7 +18,7 @@ export interface KubeObjectStoreLoadingParams { @autobind() export abstract class KubeObjectStore extends ItemStore { - @observable static defaultContext: ClusterContext; // TODO: support multiple cluster contexts + static defaultContext = observable.box(); // TODO: support multiple cluster contexts abstract api: KubeApi; public readonly limit?: number; @@ -34,7 +34,7 @@ export abstract class KubeObjectStore extends ItemSt } get context(): ClusterContext { - return KubeObjectStore.defaultContext; + return KubeObjectStore.defaultContext.get(); } @computed get contextItems(): T[] { @@ -350,7 +350,7 @@ export abstract class KubeObjectStore extends ItemSt @action protected updateFromEventsBuffer() { - const items = this.items.toJS(); + const items = this.items.toJSON(); for (const { type, object } of this.eventsBuffer.clear()) { const index = items.findIndex(item => item.getId() === object.metadata?.uid); diff --git a/src/renderer/navigation/helpers.ts b/src/renderer/navigation/helpers.ts index 399eee6325..3d0a0edc2a 100644 --- a/src/renderer/navigation/helpers.ts +++ b/src/renderer/navigation/helpers.ts @@ -1,6 +1,6 @@ import type { LocationDescriptor } from "history"; import { matchPath, RouteProps } from "react-router"; -import { PageParam, PageSystemParamInit } from "./page-param"; +import { PageParam, PageParamInit } from "./page-param"; import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route"; import { navigation } from "./history"; @@ -14,7 +14,7 @@ export function navigate(location: LocationDescriptor) { } } -export function createPageParam(init: PageSystemParamInit) { +export function createPageParam(init: PageParamInit) { return new PageParam(init, navigation); } diff --git a/src/renderer/navigation/history.ts b/src/renderer/navigation/history.ts index c1ce34625b..7187d63359 100644 --- a/src/renderer/navigation/history.ts +++ b/src/renderer/navigation/history.ts @@ -4,7 +4,14 @@ import { createObservableHistory } from "mobx-observable-history"; import logger from "../../main/logger"; export const history = ipcRenderer ? createBrowserHistory() : createMemoryHistory(); -export const navigation = createObservableHistory(history); + +export const navigation = createObservableHistory(history, { + searchParams: { + skipEmpty: true, // skip empty params, e.g. "?x=&y2=" will be "?y=2" + joinArrays: false, // join multiple params with same name, e.g. "?x=1&x=2" => "?x=1,2" + joinArraysWith: ",", // param values splitter, applicable only with {joinArrays:true} + } +}); navigation.listen((location, action) => { const isClusterView = !process.isMainFrame; diff --git a/src/renderer/navigation/index.ts b/src/renderer/navigation/index.ts index 94930fc994..6913e80672 100644 --- a/src/renderer/navigation/index.ts +++ b/src/renderer/navigation/index.ts @@ -4,5 +4,6 @@ import { bindEvents } from "./events"; export * from "./history"; export * from "./helpers"; +export * from "./page-param"; bindEvents(); diff --git a/src/renderer/navigation/page-param.ts b/src/renderer/navigation/page-param.ts index 50f6008738..005f0b0425 100644 --- a/src/renderer/navigation/page-param.ts +++ b/src/renderer/navigation/page-param.ts @@ -1,135 +1,113 @@ -// Manage observable URL-param from document.location.search -import { IObservableHistory } from "mobx-observable-history"; +// Manage observable param from document's location.search +import type { ObservableHistory } from "mobx-observable-history"; +import { action, computed } from "mobx"; export interface PageParamInit { name: string; defaultValue?: V; - defaultValueStringified?: string | string[]; // serialized version of "defaultValue" - multiValues?: boolean; // false == by default - multiValueSep?: string; // joining multiple values with separator, default: "," - skipEmpty?: boolean; // skip empty value(s), e.g. "?param=", default: true - parse?(value: string[]): V; // deserialize from URL - stringify?(value: V): string | string[]; // serialize params to URL -} - -export interface PageSystemParamInit extends PageParamInit { - isSystem?: boolean; + prefix?: string; // name prefix, for extensions it's `${extension.id}:` + parse?(value: string | string[]): V; // from URL + stringify?(value: V): string | string[]; // to URL } +// TODO: write tests export class PageParam { - static SYSTEM_PREFIX = "lens-"; - readonly name: string; - readonly urlName: string; + readonly isMulti: boolean; - constructor(readonly init: PageParamInit | PageSystemParamInit, protected history: IObservableHistory) { - const { isSystem, name } = init as PageSystemParamInit; + constructor(private init: PageParamInit, private history: ObservableHistory) { + const { prefix, name, defaultValue } = init; - this.name = name; - this.init.skipEmpty ??= true; - this.init.multiValueSep ??= ","; - - // prefixing to avoid collisions with extensions - this.urlName = `${isSystem ? PageParam.SYSTEM_PREFIX : ""}${name}`; + this.name = `${prefix ?? ""}${name}`; // actual prefixed URL-name + this.isMulti = Array.isArray(defaultValue); // multi-values param } - isEmpty(value: V | any) { - return [value].flat().every(value => value == "" || value == null); + // should be a getter since `init.defaultValue` could be a getter too + get defaultValue(): V | undefined { + return this.init.defaultValue; } - parse(values: string[]): V { - const { parse, multiValues } = this.init; + parse(values: string | string[]): V { + const { parse } = this.init; - if (!multiValues) values.splice(1); // reduce values to single item - const parsedValues = [parse ? parse(values) : values].flat(); - - return multiValues ? parsedValues : parsedValues[0] as any; - } - - stringify(value: V = this.get()): string { - const { stringify, multiValues, multiValueSep, skipEmpty } = this.init; - - if (skipEmpty && this.isEmpty(value)) { - return ""; + if (parse) { + return parse(values); } - if (multiValues) { - const values = [value].flat(); - const stringValues = [stringify ? stringify(value) : values.map(String)].flat(); + // return as-is ("string"-value based params) + return values as any as V; + } - return stringValues.join(multiValueSep); + stringify(value: V = this.get()): string[] { + const { stringify } = this.init; + + if (stringify) { + return [stringify(value)].flat(); } - return [stringify ? stringify(value) : String(value)].flat()[0]; + return [value].flat().map(String); } get(): V { - const value = this.parse(this.getRaw()); - - if (this.init.skipEmpty && this.isEmpty(value)) { - return this.getDefaultValue(); - } - - return value; + return this.parse(this.getRaw()) ?? this.defaultValue; } - set(value: V, { mergeGlobals = true, replaceHistory = false } = {}) { - const search = this.toSearchString({ mergeGlobals, value }); + @action + set(value: V, { mergeGlobals = true, replaceHistory = false } = {}): void { + const search = this.toString({ mergeGlobals, value }); this.history.merge({ search }, replaceHistory); } - setRaw(value: string | string[]) { - const { history, urlName } = this; - const { multiValues, multiValueSep, skipEmpty } = this.init; - const paramValue = multiValues ? [value].flat().join(multiValueSep) : String(value); + /** + * Set stringified raw value(s) and update `document.location.search` + * @param {string | string[]} value + */ + @action + setRaw(value: string | string[]): void { + const values: string[] = [value].flat(); - if (skipEmpty && this.isEmpty(paramValue)) { - history.searchParams.delete(urlName); - } else { - history.searchParams.set(urlName, paramValue); - } - } - - getRaw(): string[] { - const { history, urlName } = this; - const { multiValueSep } = this.init; - - return history.searchParams.getAsArray(urlName, multiValueSep); - } - - getDefaultValue() { - const { defaultValue, defaultValueStringified } = this.init; - - return defaultValueStringified ? this.parse([defaultValueStringified].flat()) : defaultValue; - } - - clear() { - this.history.searchParams.delete(this.urlName); - } - - toSearchString({ withPrefix = true, mergeGlobals = true, value = this.get() } = {}): string { - const { history, urlName, init: { skipEmpty } } = this; - const searchParams = new URLSearchParams(mergeGlobals ? history.location.search : ""); - - searchParams.set(urlName, this.stringify(value)); - - if (skipEmpty) { - searchParams.forEach((value: any, paramName) => { - if (this.isEmpty(value)) searchParams.delete(paramName); + if (this.isMulti) { + this.clear(); + values.forEach(value => { + this.history.searchParams.append(this.name, value); }); + } else { + this.history.searchParams.set(this.name, values[0]); } - - if (Array.from(searchParams).length > 0) { - return `${withPrefix ? "?" : ""}${searchParams}`; - } - - return ""; } - toObjectParam(value = this.get()): Record { + /** + * Get stringified raw value(s) from `document.location.search` + */ + @computed getRaw(): string | string[] { + const values: string[] = this.history.searchParams.getAll(this.name); + + return this.isMulti ? values : values[0]; + } + + @action + clear() { + this.history.searchParams.delete(this.name); + } + + toString({ withPrefix = true, mergeGlobals = true, value = this.get() } = {}): string { + let searchParams = new URLSearchParams(); + + if (mergeGlobals) { + searchParams = new URLSearchParams(this.history.searchParams); + searchParams.delete(this.name); + } + + this.stringify(value).forEach(value => { + searchParams.append(this.name, value); + }); + return `${withPrefix ? "?" : ""}${searchParams}`; + } + + toObjectParam(): Record { return { - [this.urlName]: value, + [this.name]: this.get(), }; } } diff --git a/src/renderer/utils/__tests__/storageHelper.test.ts b/src/renderer/utils/__tests__/storageHelper.test.ts index b788281cb3..a87356c3fa 100644 --- a/src/renderer/utils/__tests__/storageHelper.test.ts +++ b/src/renderer/utils/__tests__/storageHelper.test.ts @@ -175,7 +175,7 @@ describe("renderer/utils/StorageHelper", () => { it("storage.get() is observable", () => { expect(storageHelper.get()).toEqual(defaultValue); - reaction(() => storageHelper.toJS(), change => { + reaction(() => storageHelper.toJSON(), change => { observedChanges.push(change); }); diff --git a/src/renderer/utils/createStorage.ts b/src/renderer/utils/createStorage.ts index 79f6c13ba6..08d9d30ebf 100755 --- a/src/renderer/utils/createStorage.ts +++ b/src/renderer/utils/createStorage.ts @@ -1,10 +1,8 @@ // Keeps window.localStorage state in external JSON-files. // Because app creates random port between restarts => storage session wiped out each time. -import type { CreateObservableOptions } from "mobx/lib/api/observable"; - import path from "path"; import { app, remote } from "electron"; -import { observable, reaction, when } from "mobx"; +import { observable, reaction, when, CreateObservableOptions } from "mobx"; import fse from "fs-extra"; import { StorageHelper } from "./storageHelper"; import { ClusterStore, getHostedClusterId } from "../../common/cluster-store"; diff --git a/src/renderer/utils/storageHelper.ts b/src/renderer/utils/storageHelper.ts index 0c64b37623..182b0254e9 100755 --- a/src/renderer/utils/storageHelper.ts +++ b/src/renderer/utils/storageHelper.ts @@ -1,14 +1,10 @@ // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) -import type { CreateObservableOptions } from "mobx/lib/api/observable"; -import { action, comparer, observable, toJS, when, IObservableValue } from "mobx"; -import produce, { Draft, enableMapSet, setAutoFreeze } from "immer"; +import { action, comparer, CreateObservableOptions, IObservableValue, observable, toJS, when } from "mobx"; +import produce, { Draft } from "immer"; import { isEqual, isFunction, isPlainObject } from "lodash"; import logger from "../../main/logger"; -setAutoFreeze(false); // allow to merge observables -enableMapSet(); // allow merging maps and sets - export interface StorageAdapter { [metadata: string]: any; getItem(key: string): T | Promise; @@ -45,10 +41,8 @@ export class StorageHelper { ...StorageHelper.defaultOptions.observable, ...(options.observable ?? {}) }); - this.data.observe(change => { - const { newValue, oldValue } = toJS(change, { recurseEverything: true }); - - this.onChange(newValue, oldValue); + this.data.observe_(({ newValue, oldValue }) => { + this.onChange(newValue as T, oldValue as T); }); this.storage = options.storage; @@ -117,17 +111,18 @@ export class StorageHelper { return this.data.get(); } + @action set(value: T) { if (value == null) { - // This cannot use recursion because defaultValue might be null or undefined - this.data.set(this.defaultValue); + this.reset(); } else { this.data.set(value); } } + @action reset() { - this.set(this.defaultValue); + this.data.set(this.defaultValue); } merge(value: Partial | ((draft: Draft) => Partial | void)) { @@ -142,7 +137,7 @@ export class StorageHelper { this.set(nextValue as T); } - toJS() { - return toJS(this.get(), { recurseEverything: true }); + toJSON(): T { + return toJS(this.get()); } } diff --git a/tsconfig.json b/tsconfig.json index b2cb7376ef..b23c764f2b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "allowSyntheticDefaultImports": true, "traceResolution": false, "resolveJsonModule": true, + "useDefineForClassFields": false, "paths": { "*": [ "node_modules/*", diff --git a/yarn.lock b/yarn.lock index f344f16084..b0980140d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1269,11 +1269,16 @@ "@types/shot" "*" joi "^17.3.0" -"@types/history@*", "@types/history@^4.7.3": +"@types/history@*": version "4.7.6" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.6.tgz#ed8fc802c45b8e8f54419c2d054e55c9ea344356" integrity sha512-GRTZLeLJ8ia00ZH8mxMO8t0aC9M1N9bN461Z2eaRurJo6Fpa+utgCwLzI4jQHcrdzuzp5WPN9jRwpsCQ1VhJ5w== +"@types/history@^4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -2703,6 +2708,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +autobind-decorator@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c" + integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw== + await-lock@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.1.0.tgz#bc78c51d229a34d5d90965a1c94770e772c6145e" @@ -4390,13 +4400,20 @@ debug@4.1.0: dependencies: ms "^2.1.1" -debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: +debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -4411,7 +4428,7 @@ debug@^4.3.2: dependencies: ms "2.1.2" -debuglog@^1.0.1: +debuglog@*, debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -5893,6 +5910,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -6530,12 +6552,12 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -6739,11 +6761,16 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1, hosted-git-info@^2.8.8: +hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +hosted-git-info@^2.7.1, hosted-git-info@^2.8.9: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + hosted-git-info@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" @@ -7063,7 +7090,7 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -imurmurhash@^0.1.4: +imurmurhash@*, imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -7133,7 +7160,7 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@^1.3.5, ini@^1.3.8, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -8884,6 +8911,11 @@ lodash-es@^4.17.20: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash._baseindexof@*: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" + integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -8892,11 +8924,33 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" +lodash._bindcallback@*: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= + +lodash._cacheindexof@*: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" + integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= + +lodash._createcache@*: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" + integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= + dependencies: + lodash._getnative "^3.0.0" + lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= +lodash._getnative@*, lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= + lodash._root@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" @@ -8917,6 +8971,11 @@ lodash.memoize@4.x: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.restparam@*: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9151,9 +9210,9 @@ md5.js@^1.3.4: safe-buffer "^5.1.2" meant@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.2.tgz#5d0c78310a3d8ae1408a16be0fe0bd42a969f560" - integrity sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c" + integrity sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw== media-typer@0.3.0: version "0.3.0" @@ -9458,36 +9517,31 @@ mkdirp@1.x, mkdirp@^1.0.3: dependencies: minimist "^1.2.5" -mobx-observable-history@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mobx-observable-history/-/mobx-observable-history-1.0.3.tgz#7e34be76024dc09c71b17c3996b422f50a347c98" - integrity sha512-3JNF00AWbAXBSFJ8y/+xiPEpyySEDsNiWoT5q1G7V2w5BUQ+umqPSrmoJJlSzSs5d6Ey8HHGyPm0aGK444+hcA== +mobx-observable-history@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mobx-observable-history/-/mobx-observable-history-2.0.0.tgz#aec91929b8c0d363a19aac8dfd071a92c509caff" + integrity sha512-xX8w4lM5mKAO0S3yJ2M8FQ2pQ7Ns7zOvq7+goEnhHizO8HeHfC7O5SCy+BL5LN3Ahr5p4AW08KAj3edLBK5h4Q== dependencies: - "@types/history" "^4.7.3" + "@types/history" "^4.7.8" history "^4.10.1" - mobx "^5.15.4" + mobx "^6.3.0" -mobx-react-lite@>=2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-2.2.2.tgz#87c217dc72b4e47b22493daf155daf3759f868a6" - integrity sha512-2SlXALHIkyUPDsV4VTKVR9DW7K3Ksh1aaIv3NrNJygTbhXe2A9GrcKHZ2ovIiOp/BXilOcTYemfHHZubP431dg== +mobx-react-lite@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz#331d7365a6b053378dfe9c087315b4e41c5df69f" + integrity sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g== -mobx-react@^6.2.2: - version "6.3.0" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.3.0.tgz#7d11799f988bbdadc49e725081993b18baa20329" - integrity sha512-C14yya2nqEBRSEiJjPkhoWJLlV8pcCX3m2JRV7w1KivwANJqipoiPx9UMH4pm6QNMbqDdvJqoyl+LqNu9AhvEQ== +mobx-react@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-7.1.0.tgz#d947cada3cfad294b4b6f692e969c242b9c16eaf" + integrity sha512-DxvA6VXmnZ+N9f/UTtolWtdRnAAQY2iHWTSPLktfpj8NKlXUe4dabBAjuXrBcZUM8GjLWnxD1ZEjssXq1M0RAw== dependencies: - mobx-react-lite ">=2.2.0" + mobx-react-lite "^3.2.0" -mobx@^5.15.4: - version "5.15.4" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.4.tgz#9da1a84e97ba624622f4e55a0bf3300fb931c2ab" - integrity sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw== - -mobx@^5.15.7: - version "5.15.7" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665" - integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw== +mobx@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.0.tgz#a8fb693c3047bdfcb1eaff9aa48e36a7eb084f96" + integrity sha512-Aa1+VXsg4WxqJMTQfWoYuJi5UD10VZhiobSmcs5kcmI3BIT0aVtn7DysvCeDADCzl7dnbX+0BTHUj/v7gLlZpQ== mock-fs@^4.12.0: version "4.12.0" @@ -9921,9 +9975,9 @@ npm-audit-report@^1.3.3: console-control-strings "^1.1.0" npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== dependencies: npm-normalize-package-bin "^1.0.1" @@ -10040,15 +10094,15 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -npm-user-validate@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951" - integrity sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE= +npm-user-validate@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561" + integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw== npm@^6.14.8: - version "6.14.8" - resolved "https://registry.yarnpkg.com/npm/-/npm-6.14.8.tgz#64ef754345639bc035982ec3f609353c8539033c" - integrity sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A== + version "6.14.13" + resolved "https://registry.yarnpkg.com/npm/-/npm-6.14.13.tgz#e88bcb6c48209869c40b5cedad8a1508e58e6f30" + integrity sha512-SRl4jJi0EBHY2xKuu98FLRMo3VhYQSA6otyLnjSEiHoSG/9shXCFNJy9tivpUJvtkN9s6VDdItHa5Rn+fNBzag== dependencies: JSONStream "^1.3.5" abbrev "~1.1.1" @@ -10080,12 +10134,12 @@ npm@^6.14.8: glob "^7.1.6" graceful-fs "^4.2.4" has-unicode "~2.0.1" - hosted-git-info "^2.8.8" + hosted-git-info "^2.8.9" iferr "^1.0.2" infer-owner "^1.0.4" inflight "~1.0.6" inherits "^2.0.4" - ini "^1.3.5" + ini "^1.3.8" init-package-json "^1.10.3" is-cidr "^3.0.0" json-parse-better-errors "^1.0.2" @@ -10122,10 +10176,10 @@ npm@^6.14.8: npm-pick-manifest "^3.0.2" npm-profile "^4.0.4" npm-registry-fetch "^4.0.7" - npm-user-validate "~1.0.0" + npm-user-validate "^1.0.1" npmlog "~4.1.2" once "~1.4.0" - opener "^1.5.1" + opener "^1.5.2" osenv "^0.1.5" pacote "^9.5.12" path-is-inside "~1.0.2" @@ -10149,7 +10203,7 @@ npm@^6.14.8: slide "~1.1.6" sorted-object "~2.0.1" sorted-union-stream "~2.1.3" - ssri "^6.0.1" + ssri "^6.0.2" stringify-package "^1.0.1" tar "^4.4.13" text-table "~0.2.0" @@ -10377,7 +10431,7 @@ open@^7.3.1: is-docker "^2.0.0" is-wsl "^2.1.1" -opener@^1.5.1: +opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== @@ -11390,11 +11444,12 @@ query-string@^5.0.1: strict-uri-encode "^1.0.0" query-string@^6.8.2: - version "6.13.4" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.4.tgz#b35a9a3bd4955bce55f94feb0e819b3d0be6f66f" - integrity sha512-E2NPIeJoBEJGQNy3ib1k/Z/OkDBUKIo8IV2ZVwbKfoa65IS9unqWWUlLcbfU70Da0qNoxUZZA8CfKUjKLE641Q== + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== dependencies: decode-uri-component "^0.2.0" + filter-obj "^1.1.0" split-on-first "^1.0.0" strict-uri-encode "^2.0.0" @@ -12896,7 +12951,7 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^6.0.0, ssri@^6.0.1: +ssri@^6.0.0, ssri@^6.0.1, ssri@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==