diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index d93dd0cb18..53e7a52e4b 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -134,7 +134,7 @@ export class KubernetesCluster< } } -class KubernetesClusterCategory extends CatalogCategory { +export class KubernetesClusterCategory extends CatalogCategory { public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; public readonly kind = "CatalogCategory"; public metadata = { @@ -151,5 +151,3 @@ class KubernetesClusterCategory extends CatalogCategory { }, }; } - -export const kubernetesClusterCategory = new KubernetesClusterCategory(); diff --git a/src/common/catalog/categories/general.injectable.ts b/src/common/catalog/categories/general.injectable.ts new file mode 100644 index 0000000000..d2e3ba7a69 --- /dev/null +++ b/src/common/catalog/categories/general.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { GeneralCategory } from "../../catalog-entities"; +import { builtInCategoryInjectionToken } from "../category-registry.injectable"; + +const generalCategoryInjectable = getInjectable({ + id: "general-category", + instantiate: () => new GeneralCategory(), + injectionToken: builtInCategoryInjectionToken, +}); + +export default generalCategoryInjectable; diff --git a/src/common/catalog/categories/kubernetes-cluster.injectable.ts b/src/common/catalog/categories/kubernetes-cluster.injectable.ts new file mode 100644 index 0000000000..6dc3f9be8d --- /dev/null +++ b/src/common/catalog/categories/kubernetes-cluster.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { KubernetesClusterCategory } from "../../catalog-entities/kubernetes-cluster"; +import { builtInCategoryInjectionToken } from "../category-registry.injectable"; + +const kubernetesClusterCategoryInjectable = getInjectable({ + id: "kubernetes-cluster-category", + instantiate: () => new KubernetesClusterCategory(), + injectionToken: builtInCategoryInjectionToken, +}); + +export default kubernetesClusterCategoryInjectable; diff --git a/src/common/catalog/categories/weblink.injectable.ts b/src/common/catalog/categories/weblink.injectable.ts new file mode 100644 index 0000000000..339758efbf --- /dev/null +++ b/src/common/catalog/categories/weblink.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { WebLinkCategory } from "../../catalog-entities"; +import { builtInCategoryInjectionToken } from "../category-registry.injectable"; + +const weblinkCategoryInjectable = getInjectable({ + id: "weblink-category", + instantiate: () => new WebLinkCategory(), + injectionToken: builtInCategoryInjectionToken, +}); + +export default weblinkCategoryInjectable; diff --git a/src/common/catalog/category-registry.injectable.ts b/src/common/catalog/category-registry.injectable.ts index 0b9aa321cd..cf7de38826 100644 --- a/src/common/catalog/category-registry.injectable.ts +++ b/src/common/catalog/category-registry.injectable.ts @@ -2,18 +2,25 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getInjectable } from "@ogre-tools/injectable"; -import { GeneralCategory, kubernetesClusterCategory, WebLinkCategory } from "../catalog-entities"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { WebLinkCategory } from "../catalog-entities"; +import type { CatalogCategory } from "./catalog-entity"; import { CatalogCategoryRegistry } from "./category-registry"; +export const builtInCategoryInjectionToken = getInjectionToken({ + id: "built-in-category-token", +}); + const catalogCategoryRegistryInjectable = getInjectable({ id: "catalog-category-registry", - instantiate: () => { + instantiate: (di) => { const registry = new CatalogCategoryRegistry(); + const categories = di.injectMany(builtInCategoryInjectionToken); + + for (const category of categories) { + registry.add(category); + } - // TODO: move to different place - registry.add(new GeneralCategory()); - registry.add(kubernetesClusterCategory); registry.add(new WebLinkCategory()); return registry; diff --git a/src/extensions/common-api/catalog.ts b/src/extensions/common-api/catalog.ts index fae083e75a..b31059d2c1 100644 --- a/src/extensions/common-api/catalog.ts +++ b/src/extensions/common-api/catalog.ts @@ -3,13 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import kubernetesClusterCategoryInjectable from "../../common/catalog/categories/kubernetes-cluster.injectable"; +import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; + export { KubernetesCluster, - kubernetesClusterCategory, GeneralEntity, WebLink, } from "../../common/catalog-entities"; +export const kubernetesClusterCategory = asLegacyGlobalForExtensionApi(kubernetesClusterCategoryInjectable); + export type { KubernetesClusterPrometheusMetrics, KubernetesClusterSpec, diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index c4520b11e5..b698460a54 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -40,6 +40,6 @@ export interface RegisteredPage { id: string; extensionId: string; url: string; // registered extension's page URL (without page params) - params: PageParams; // normalized params + params: PageParams>; // normalized params components: PageComponents; // normalized components } diff --git a/src/extensions/renderer-api/navigation.ts b/src/extensions/renderer-api/navigation.ts index 122e7c2be7..b2ea235ecc 100644 --- a/src/extensions/renderer-api/navigation.ts +++ b/src/extensions/renderer-api/navigation.ts @@ -3,14 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { PageParamInit } from "../../renderer/navigation"; -import { navigation, PageParam } from "../../renderer/navigation"; +import getDetailsUrlInjectable from "../../renderer/components/kube-detail-params/get-details-url.injectable"; +import hideDetailsInjectable from "../../renderer/components/kube-detail-params/hide-details.injectable"; +import showDetailsInjectable from "../../renderer/components/kube-detail-params/show-details.injectable"; +import createPageParamInjectable from "../../renderer/navigation/create-page-param.injectable"; +import isActiveRouteInjectable from "../../renderer/navigation/is-route-active.injectable"; +import navigateInjectable from "../../renderer/navigation/navigate.injectable"; +import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; -export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param"; -export { navigate, isActiveRoute } from "../../renderer/navigation/helpers"; -export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-detail-params"; +export type { PageParamInit, PageParam } from "../../renderer/navigation"; export type { URLParams } from "../../common/utils/buildUrl"; -export function createPageParam(init: PageParamInit) { - return new PageParam(init, navigation); -} +export const getDetailsUrl = asLegacyGlobalFunctionForExtensionApi(getDetailsUrlInjectable); +export const showDetails = asLegacyGlobalFunctionForExtensionApi(showDetailsInjectable); +export const hideDetails = asLegacyGlobalFunctionForExtensionApi(hideDetailsInjectable); +export const createPageParam = asLegacyGlobalFunctionForExtensionApi(createPageParamInjectable); +export const isActiveRoute = asLegacyGlobalFunctionForExtensionApi(isActiveRouteInjectable); +export const navigate = asLegacyGlobalFunctionForExtensionApi(navigateInjectable); diff --git a/src/renderer/api/helpers/general-active-sync.ts b/src/renderer/api/helpers/general-active-sync.ts deleted file mode 100644 index bdc38c5eee..0000000000 --- a/src/renderer/api/helpers/general-active-sync.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { when } from "mobx"; -import { catalogCategoryRegistry } from "../../../common/catalog"; -import { catalogEntityRegistry } from "../catalog-entity-registry"; -import { isActiveRoute } from "../../navigation"; -import assert from "assert"; - -export async function setEntityOnRouteMatch() { - await when(() => catalogEntityRegistry.entities.size > 0); - - const generalCategory = catalogCategoryRegistry.getByName("General"); - - assert(generalCategory); - const entities = catalogEntityRegistry.getItemsForCategory(generalCategory); - const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path)); - - if (activeEntity) { - catalogEntityRegistry.activeEntity = activeEntity; - } -} diff --git a/src/renderer/api/helpers/watch-for-general-entity-navigation.injectable.ts b/src/renderer/api/helpers/watch-for-general-entity-navigation.injectable.ts new file mode 100644 index 0000000000..6c032a07ee --- /dev/null +++ b/src/renderer/api/helpers/watch-for-general-entity-navigation.injectable.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { reaction, when } from "mobx"; +import generalCategoryInjectable from "../../../common/catalog/categories/general.injectable"; +import isActiveRouteInjectable from "../../navigation/is-route-active.injectable"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; +import type { Disposer } from "../../utils"; +import { disposer } from "../../utils"; +import catalogEntityRegistryInjectable from "../catalog-entity-registry/catalog-entity-registry.injectable"; + +export type WatchForGeneralEntityNavigation = () => Disposer; + +const watchForGeneralEntityNavigationInjectable = getInjectable({ + id: "watch-for-general-entity-navigation", + instantiate: (di): WatchForGeneralEntityNavigation => { + const observableHistory = di.inject(observableHistoryInjectable); + const isActiveRoute = di.inject(isActiveRouteInjectable); + const entityRegistry = di.inject(catalogEntityRegistryInjectable); + const generalCategory = di.inject(generalCategoryInjectable); + + return () => { + const dispose = disposer(); + + dispose.push(when( + () => entityRegistry.entities.size > 0, + () => { + dispose.push(reaction( + () => observableHistory.location, + () => { + const entities = entityRegistry.getItemsForCategory(generalCategory); + const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path)); + + if (activeEntity) { + entityRegistry.activeEntity = activeEntity; + } + }, + { + fireImmediately: true, + }, + )); + }, + )); + + return dispose; + }; + }, +}); + +export default watchForGeneralEntityNavigationInjectable; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 54116f5f86..52c75c2252 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -45,6 +45,7 @@ import { bindEvents } from "./navigation/events"; import assert from "assert"; import openDeleteClusterDialogInjectable from "./components/delete-cluster-dialog/open.injectable"; import { init } from "@sentry/electron/renderer"; +import kubernetesClusterCategoryInjectable from "../common/catalog/categories/kubernetes-cluster.injectable"; configurePackages(); // global packages registerCustomThemes(); // monaco editor themes @@ -95,13 +96,11 @@ export async function bootstrap(di: DiContainer) { logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`); initializers.initCatalogEntityDetailRegistry(); - const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); - const addSyncEntries = di.inject(addSyncEntriesInjectable); - logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`); initializers.initCatalogCategoryRegistryEntries({ - navigateToAddCluster, - addSyncEntries, + navigateToAddCluster: di.inject(navigateToAddClusterInjectable), + addSyncEntries: di.inject(addSyncEntriesInjectable), + kubernetesClusterCategory: di.inject(kubernetesClusterCategoryInjectable), }); logger.info(`${logPrefix} initializing Catalog`); diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx index 15824a6cb6..3290f548cb 100644 --- a/src/renderer/components/+cluster/cluster-issues.tsx +++ b/src/renderer/components/+cluster/cluster-issues.tsx @@ -15,7 +15,6 @@ import { cssNames, prevDefault } from "../../utils"; import type { ItemObject } from "../../../common/item.store"; import { Spinner } from "../spinner"; import type { ThemeStore } from "../../themes/store"; -import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import type { ApiManager } from "../../../common/k8s-api/api-manager"; import { KubeObjectAge } from "../kube-object/age"; import { withInjectables } from "@ogre-tools/injectable-react"; @@ -25,6 +24,10 @@ import type { EventStore } from "../+events/store"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; import eventStoreInjectable from "../+events/store.injectable"; import nodeStoreInjectable from "../+nodes/store.injectable"; +import type { PageParam } from "../../navigation"; +import type { ToggleDetails } from "../kube-detail-params/toggle-details.injectable"; +import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable"; +import toggleDetailsInjectable from "../kube-detail-params/toggle-details.injectable"; export interface ClusterIssuesProps { className?: string; @@ -49,6 +52,8 @@ interface Dependencies { nodeStore: NodeStore; eventStore: EventStore; apiManager: ApiManager; + kubeSelectedUrlParam: PageParam; + toggleDetails: ToggleDetails; } @observer @@ -86,6 +91,7 @@ class NonInjectedClusterIssues extends React.Component { const { warnings } = this; + const { kubeSelectedUrlParam, toggleDetails } = this.props; const warning = warnings.find(warn => warn.getId() == uid); if (!warning) { @@ -189,5 +195,7 @@ export const ClusterIssues = withInjectables(N apiManager: di.inject(apiManagerInjectable), eventStore: di.inject(eventStoreInjectable), nodeStore: di.inject(nodeStoreInjectable), + kubeSelectedUrlParam: di.inject(kubeSelectedUrlParamInjectable), + toggleDetails: di.inject(toggleDetailsInjectable), }), }); diff --git a/src/renderer/components/+preferences/close-preferences.injectable.ts b/src/renderer/components/+preferences/close-preferences.injectable.ts index 61eb3f9042..b85c2ae52a 100644 --- a/src/renderer/components/+preferences/close-preferences.injectable.ts +++ b/src/renderer/components/+preferences/close-preferences.injectable.ts @@ -14,13 +14,11 @@ const closePreferencesInjectable = getInjectable({ const navigateToFrontPage = di.inject(navigateToFrontPageInjectable); return () => { - if (observableHistory.length === 1) { + if (observableHistory.length <= 1) { navigateToFrontPage(); - - return; + } else { + observableHistory.goBack(); } - - observableHistory.goBack(); }; }, }); diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 208899a957..85fb8a80c0 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -15,25 +15,25 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import { TopBar } from "../layout/top-bar/top-bar"; import catalogPreviousActiveTabStorageInjectable from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable"; import type { IComputedValue } from "mobx"; -import { reaction } from "mobx"; import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; -import { setEntityOnRouteMatch } from "../../api/helpers/general-active-sync"; -import { navigation } from "../../navigation"; import welcomeRouteInjectable from "../../../common/front-end-routing/routes/welcome/welcome-route.injectable"; import { buildURL } from "../../../common/utils/buildUrl"; import type { StorageLayer } from "../../utils"; +import type { WatchForGeneralEntityNavigation } from "../../api/helpers/watch-for-general-entity-navigation.injectable"; +import watchForGeneralEntityNavigationInjectable from "../../api/helpers/watch-for-general-entity-navigation.injectable"; interface Dependencies { catalogPreviousActiveTabStorage: StorageLayer; currentRouteComponent: IComputedValue; welcomeUrl: string; + watchForGeneralEntityNavigation: WatchForGeneralEntityNavigation; } @observer class NonInjectedClusterManager extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - reaction(() => navigation.location, () => setEntityOnRouteMatch(), { fireImmediately: true }), + this.props.watchForGeneralEntityNavigation(), ]); } @@ -64,5 +64,6 @@ export const ClusterManager = withInjectables(NonInjectedClusterMa catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable), currentRouteComponent: di.inject(currentRouteComponentInjectable), welcomeUrl: buildURL(di.inject(welcomeRouteInjectable).path), + watchForGeneralEntityNavigation: di.inject(watchForGeneralEntityNavigationInjectable), }), }); diff --git a/src/renderer/components/drawer/drawer.tsx b/src/renderer/components/drawer/drawer.tsx index 75c3d0808c..9adcb11cdb 100644 --- a/src/renderer/components/drawer/drawer.tsx +++ b/src/renderer/components/drawer/drawer.tsx @@ -18,7 +18,7 @@ import drawerStorageInjectable, { defaultDrawerWidth, } from "./drawer-storage/drawer-storage.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; -import historyInjectable from "../../navigation/history/history.injectable"; +import historyInjectable from "../../navigation/history.injectable"; import type { History } from "history"; export type DrawerPosition = "top" | "left" | "right" | "bottom"; diff --git a/src/renderer/components/kube-detail-params/get-details-url.injectable.ts b/src/renderer/components/kube-detail-params/get-details-url.injectable.ts new file mode 100644 index 0000000000..e11e9e5d05 --- /dev/null +++ b/src/renderer/components/kube-detail-params/get-details-url.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; +import kubeDetailsUrlParamInjectable from "./kube-details-url.injectable"; +import kubeSelectedUrlParamInjectable from "./kube-selected-url.injectable"; + +export type GetDetailsUrl = (selfLink: string, resetSelected?: boolean, mergeGlobals?: boolean) => string; + +const getDetailsUrlInjectable = getInjectable({ + id: "get-details-url", + instantiate: (di): GetDetailsUrl => { + const observableHistory = di.inject(observableHistoryInjectable); + const kubeDetailsUrlParam = di.inject(kubeDetailsUrlParamInjectable); + const kubeSelectedUrlParam = di.inject(kubeSelectedUrlParamInjectable); + + return (selfLink, resetSelected = false, mergeGlobals = true) => { + const params = new URLSearchParams(mergeGlobals ? observableHistory.searchParams : ""); + + params.set(kubeDetailsUrlParam.name, selfLink); + + if (resetSelected) { + params.delete(kubeSelectedUrlParam.name); + } else { + params.set(kubeSelectedUrlParam.name, kubeSelectedUrlParam.get()); + } + + return `?${params}`; + }; + }, +}); + +export default getDetailsUrlInjectable; diff --git a/src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts b/src/renderer/components/kube-detail-params/hide-details.injectable.ts similarity index 56% rename from src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts rename to src/renderer/components/kube-detail-params/hide-details.injectable.ts index ce399fb406..d1a3637d95 100644 --- a/src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts +++ b/src/renderer/components/kube-detail-params/hide-details.injectable.ts @@ -2,12 +2,18 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { hideDetails } from "../../kube-detail-params"; import { getInjectable } from "@ogre-tools/injectable"; +import showDetailsInjectable from "./show-details.injectable"; + +export type HideDetails = () => void; const hideDetailsInjectable = getInjectable({ id: "hide-details", - instantiate: () => hideDetails, + instantiate: (di): HideDetails => { + const showDetails = di.inject(showDetailsInjectable); + + return () => showDetails(""); + }, }); export default hideDetailsInjectable; diff --git a/src/renderer/components/kube-detail-params/index.ts b/src/renderer/components/kube-detail-params/index.ts index a781b6449b..beef30557a 100644 --- a/src/renderer/components/kube-detail-params/index.ts +++ b/src/renderer/components/kube-detail-params/index.ts @@ -3,4 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./params"; +import { asLegacyGlobalFunctionForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; +import getDetailsUrlInjectable from "./get-details-url.injectable"; +import showDetailsInjectable from "./show-details.injectable"; + +/** + * @deprecated use `di.inject(getDetailsUrlInjectable)` instead + */ +export const getDetailsUrl = asLegacyGlobalFunctionForExtensionApi(getDetailsUrlInjectable); + +/** + * @deprecated use `di.inject(showDetailsInjectable)` instead + */ +export const showDetails = asLegacyGlobalFunctionForExtensionApi(showDetailsInjectable); diff --git a/src/renderer/components/kube-detail-params/kube-details-url.injectable.ts b/src/renderer/components/kube-detail-params/kube-details-url.injectable.ts new file mode 100644 index 0000000000..3a8e29b7af --- /dev/null +++ b/src/renderer/components/kube-detail-params/kube-details-url.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import createPageParamInjectable from "../../navigation/create-page-param.injectable"; + +/** + * Used to store `object.selfLink` to show more info about resource in the details panel. + */ +const kubeDetailsUrlParamInjectable = getInjectable({ + id: "kube-details-url-param", + instantiate: (di) => { + const createPageParam = di.inject(createPageParamInjectable); + + return createPageParam({ + name: "kube-details", + }); + }, +}); + +export default kubeDetailsUrlParamInjectable; diff --git a/src/renderer/components/kube-detail-params/kube-selected-url.injectable.ts b/src/renderer/components/kube-detail-params/kube-selected-url.injectable.ts new file mode 100644 index 0000000000..e990ef4d72 --- /dev/null +++ b/src/renderer/components/kube-detail-params/kube-selected-url.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import createPageParamInjectable from "../../navigation/create-page-param.injectable"; +import kubeDetailsUrlParamInjectable from "./kube-details-url.injectable"; + +/** + * Used to highlight last active/selected table row with the resource. + * + * @example + * If we go to "Nodes (page) -> Node (details) -> Pod (details)", + * last clicked Node should be "active" while Pod details are shown). + */ +const kubeSelectedUrlParamInjectable = getInjectable({ + id: "kube-selected-url-param", + instantiate: (di) => { + const createPageParam = di.inject(createPageParamInjectable); + const kubeDetailsUrlParam = di.inject(kubeDetailsUrlParamInjectable); + + return createPageParam({ + name: "kube-selected", + get defaultValue() { + return kubeDetailsUrlParam.get(); + }, + }); + }, +}); + +export default kubeSelectedUrlParamInjectable; diff --git a/src/renderer/components/kube-detail-params/params.ts b/src/renderer/components/kube-detail-params/params.ts deleted file mode 100644 index 826cba7a64..0000000000 --- a/src/renderer/components/kube-detail-params/params.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { createPageParam, navigation } from "../../navigation"; - -/** - * Used to store `object.selfLink` to show more info about resource in the details panel. - */ -export const kubeDetailsUrlParam = createPageParam({ - name: "kube-details", -}); - -/** - * Used to highlight last active/selected table row with the resource. - * - * @example - * If we go to "Nodes (page) -> Node (details) -> Pod (details)", - * last clicked Node should be "active" while Pod details are shown). - */ -export const kubeSelectedUrlParam = createPageParam({ - name: "kube-selected", - get defaultValue() { - return kubeDetailsUrlParam.get(); - }, -}); - -export function toggleDetails(selfLink: string | undefined, resetSelected = true) { - const current = kubeSelectedUrlParam.get() === selfLink; - - if (current) { - hideDetails(); - } else { - showDetails(selfLink, resetSelected); - } -} - -export function showDetails(selfLink = "", resetSelected = true) { - const detailsUrl = getDetailsUrl(selfLink, resetSelected); - - navigation.merge({ search: detailsUrl }); -} - -export function hideDetails() { - showDetails(); -} - -export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) { - const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : ""); - - params.set(kubeDetailsUrlParam.name, selfLink); - - if (resetSelected) { - params.delete(kubeSelectedUrlParam.name); - } else { - params.set(kubeSelectedUrlParam.name, kubeSelectedUrlParam.get()); - } - - return `?${params}`; -} diff --git a/src/renderer/components/kube-detail-params/show-details.injectable.ts b/src/renderer/components/kube-detail-params/show-details.injectable.ts new file mode 100644 index 0000000000..af7a31fc2a --- /dev/null +++ b/src/renderer/components/kube-detail-params/show-details.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; +import getDetailsUrlInjectable from "./get-details-url.injectable"; + +/** + * @param selfLink The Kube selflink to show details for + * @param resetSelected If true then will reset the selected kube object (which object is highlighted generally) + * @default resetSelected true + */ +export type ShowDetails = (selfLink: string, resetSelected?: boolean) => void; + +const showDetailsInjectable = getInjectable({ + id: "show-details", + instantiate: (di): ShowDetails => { + const observableHistory = di.inject(observableHistoryInjectable); + const getDetailsUrl = di.inject(getDetailsUrlInjectable); + + return (selfLink, resetSelected = true) => { + observableHistory.merge({ + search: getDetailsUrl(selfLink, resetSelected), + }); + }; + }, +}); + +export default showDetailsInjectable; diff --git a/src/renderer/components/kube-detail-params/toggle-details.injectable.ts b/src/renderer/components/kube-detail-params/toggle-details.injectable.ts new file mode 100644 index 0000000000..fe0c97fd2b --- /dev/null +++ b/src/renderer/components/kube-detail-params/toggle-details.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import hideDetailsInjectable from "./hide-details.injectable"; +import kubeSelectedUrlParamInjectable from "./kube-selected-url.injectable"; +import showDetailsInjectable from "./show-details.injectable"; + +export interface ToggleDetails { + /** + * @param selfLink The selfLink of the kube object to toggle the details panal of + * @param resetSelected If true then the selected kube object (for the current list view) will be reset + * @default resetSelected true + */ + (selfLink: string, resetSelected?: boolean): void; + /** + * @deprecated Future versions will not support passing in `selfLink` that is `undefined` + */ + (selfLink: string | undefined, resetSelected?: boolean): void; +} + +const toggleDetailsInjectable = getInjectable({ + id: "toggle-details", + instantiate: (di): ToggleDetails => { + const showDetails = di.inject(showDetailsInjectable); + const hideDetails = di.inject(hideDetailsInjectable); + const kubeSelectedUrlParam = di.inject(kubeSelectedUrlParamInjectable); + + return (selfLink, resetSelected = true) => { + const current = kubeSelectedUrlParam.get() === selfLink; + + if (current || !selfLink) { + hideDetails(); + } else { + showDetails(selfLink, resetSelected); + } + }; + }, +}); + +export default toggleDetailsInjectable; diff --git a/src/renderer/components/kube-object-details/kube-object-details.tsx b/src/renderer/components/kube-object-details/kube-object-details.tsx index 49403d3998..c0d566d049 100644 --- a/src/renderer/components/kube-object-details/kube-object-details.tsx +++ b/src/renderer/components/kube-object-details/kube-object-details.tsx @@ -11,37 +11,50 @@ import { computed, observable, reaction, makeObservable } from "mobx"; import { Drawer } from "../drawer"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { Spinner } from "../spinner"; -import { apiManager } from "../../../common/k8s-api/api-manager"; -import { customResourceDefinitionStore } from "../+custom-resources/legacy-store"; +import type { ApiManager } from "../../../common/k8s-api/api-manager"; import { KubeObjectMenu } from "../kube-object-menu"; import { KubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { CustomResourceDetails } from "../+custom-resources"; import { KubeObjectMeta } from "../kube-object-meta"; -import { hideDetails, kubeDetailsUrlParam } from "../kube-detail-params"; +import type { PageParam } from "../../navigation"; +import type { HideDetails } from "../kube-detail-params/hide-details.injectable"; +import type { CustomResourceDefinitionStore } from "../+custom-resources/definition.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; +import customResourceDefinitionStoreInjectable from "../+custom-resources/definition.store.injectable"; +import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable"; +import kubeDetailsUrlParamInjectable from "../kube-detail-params/kube-details-url.injectable"; -export interface KubeObjectDetailsProps { +export interface KubeObjectDetailsProps { className?: string; - object: T; + object: Kube; +} + +interface Dependencies { + kubeDetailsUrlParam: PageParam; + apiManager: ApiManager; + hideDetails: HideDetails; + customResourceDefinitionStore: CustomResourceDefinitionStore; } @observer -export class KubeObjectDetails extends React.Component { +class NonInjectedKubeObjectDetails extends React.Component { @observable isLoading = false; @observable.ref loadingError: React.ReactNode; - constructor(props: {}) { + constructor(props: Dependencies) { super(props); makeObservable(this); } @computed get path() { - return kubeDetailsUrlParam.get(); + return this.props.kubeDetailsUrlParam.get(); } @computed get object() { try { - return apiManager + return this.props.apiManager .getStore(this.path) ?.getByPath(this.path); } catch (error) { @@ -56,13 +69,13 @@ export class KubeObjectDetails extends React.Component { reaction(() => [ this.path, this.object, // resource might be updated via watch-event or from already opened details - customResourceDefinitionStore.items.length, // crd stores initialized after loading + this.props.customResourceDefinitionStore.items.length, // crd stores initialized after loading ], async () => { this.loadingError = ""; const { path, object } = this; if (!object) { - const store = apiManager.getStore(path); + const store = this.props.apiManager.getStore(path); if (store) { this.isLoading = true; @@ -103,7 +116,7 @@ export class KubeObjectDetails extends React.Component { )); if (details.length === 0) { - const crd = customResourceDefinitionStore.getByObject(object); + const crd = this.props.customResourceDefinitionStore.getByObject(object); /** * This is a fallback so that if a custom resource object doesn't have @@ -141,10 +154,20 @@ export class KubeObjectDetails extends React.Component { open={!!(object || isLoading || loadingError)} title={this.renderTitle(object)} toolbar={object && } - onClose={hideDetails} + onClose={this.props.hideDetails} > {object && this.renderContents(object)} ); } } + +export const KubeObjectDetails = withInjectables(NonInjectedKubeObjectDetails, { + getProps: (di, props) => ({ + ...props, + apiManager: di.inject(apiManagerInjectable), + customResourceDefinitionStore: di.inject(customResourceDefinitionStoreInjectable), + hideDetails: di.inject(hideDetailsInjectable), + kubeDetailsUrlParam: di.inject(kubeDetailsUrlParamInjectable), + }), +}); diff --git a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx index 661624ff53..56b26135d7 100644 --- a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx @@ -17,7 +17,6 @@ import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store" import { KubeObjectMenu } from "../kube-object-menu"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { ResourceKindMap, ResourceNames } from "../../utils/rbac"; -import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import { Icon } from "../icon"; import { TooltipPosition } from "../tooltip"; import { withInjectables } from "@ogre-tools/injectable-react"; @@ -26,6 +25,10 @@ import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-f import type { SubscribableStore, SubscribeStores } from "../../kube-watch-api/kube-watch-api"; import type { KubeApi } from "../../../common/k8s-api/kube-api"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; +import type { PageParam } from "../../navigation"; +import type { ToggleDetails } from "../kube-detail-params/toggle-details.injectable"; +import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable"; +import toggleDetailsInjectable from "../kube-detail-params/toggle-details.injectable"; export interface KubeObjectListLayoutProps< K extends KubeObject, @@ -42,6 +45,8 @@ export interface KubeObjectListLayoutProps< interface Dependencies { clusterFrameContext: ClusterFrameContext; subscribeToStores: SubscribeStores; + kubeSelectedUrlParam: PageParam; + toggleDetails: ToggleDetails; } @observer @@ -51,14 +56,13 @@ class NonInjectedKubeObjectListLayout< D extends KubeJsonApiDataFor, > extends React.Component & Dependencies> { static defaultProps = { - onDetails: (item: KubeObject) => toggleDetails(item.selfLink), subscribeStores: true, }; private loadErrors = observable.array(); @computed get selectedItem() { - return this.props.store.getByPath(kubeSelectedUrlParam.get()); + return this.props.store.getByPath(this.props.kubeSelectedUrlParam.get()); } componentDidMount() { @@ -104,7 +108,16 @@ class NonInjectedKubeObjectListLayout< } render() { - const { className, customizeHeader, store, items, dependentStores, ...layoutProps } = this.props; + const { + className, + customizeHeader, + store, + items, + dependentStores, + toggleDetails, + onDetails, + ...layoutProps + } = this.props; const placeholderString = ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind; return ( @@ -137,13 +150,14 @@ class NonInjectedKubeObjectListLayout< ...[customizeHeader].filter(isDefined).flat(), ]} renderItemMenu={item => } + onDetails={onDetails ?? ((item) => toggleDetails(item.selfLink))} {...layoutProps} /> ); } } -const InjectedKubeObjectListLayout = withInjectables< +export const KubeObjectListLayout = withInjectables< Dependencies, KubeObjectListLayoutProps>, KubeJsonApiDataFor> >(NonInjectedKubeObjectListLayout, { @@ -151,13 +165,11 @@ const InjectedKubeObjectListLayout = withInjectables< ...props, clusterFrameContext: di.inject(clusterFrameContextInjectable), subscribeToStores: di.inject(subscribeStoresInjectable), + kubeSelectedUrlParam: di.inject(kubeSelectedUrlParamInjectable), + toggleDetails: di.inject(toggleDetailsInjectable), }), -}); - -export function KubeObjectListLayout< +}) as < K extends KubeObject, A extends KubeApi, D extends KubeJsonApiDataFor = KubeJsonApiDataFor, ->(props: KubeObjectListLayoutProps) { - return ; -} +>(props: KubeObjectListLayoutProps) => React.ReactElement; diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.tsx index 60324bc8e0..1639e2bb40 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.tsx @@ -13,9 +13,10 @@ import type { ApiManager } from "../../../common/k8s-api/api-manager"; import { withInjectables } from "@ogre-tools/injectable-react"; import clusterNameInjectable from "./dependencies/cluster-name.injectable"; import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; -import hideDetailsInjectable from "./dependencies/hide-details.injectable"; import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; +import type { HideDetails } from "../kube-detail-params/hide-details.injectable"; +import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable"; export interface KubeObjectMenuProps extends MenuActionsProps { object: TKubeObject; @@ -27,7 +28,7 @@ interface Dependencies { apiManager: ApiManager; kubeObjectMenuItems: React.ElementType[]; clusterName: string | undefined; - hideDetails: () => void; + hideDetails: HideDetails; createEditResourceTab: (kubeObject: KubeObject) => void; } diff --git a/src/renderer/components/layout/setting-layout.tsx b/src/renderer/components/layout/setting-layout.tsx index fa52d7a2ee..b6e0e58ec2 100644 --- a/src/renderer/components/layout/setting-layout.tsx +++ b/src/renderer/components/layout/setting-layout.tsx @@ -12,7 +12,7 @@ import { cssNames } from "../../utils"; import { CloseButton } from "./close-button"; import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; -import observableHistoryInjectable from "../../navigation/history/observable.injectable"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; export interface SettingLayoutProps extends React.DOMAttributes { className?: IClassName; diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 1c06215e6a..04c62bddf5 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -37,7 +37,7 @@ import { observable, toJS } from "mobx"; import type { Draft } from "immer"; import { produce, isDraft } from "immer"; import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing"; -import historyInjectable from "./navigation/history/history.injectable"; +import historyInjectable from "./navigation/history.injectable"; export interface GetRendererDiForUnitTestingOptions extends GetDiForUnitTestingOptions { overrideCreateStorage?: boolean; diff --git a/src/renderer/initializers/catalog-category-registry.tsx b/src/renderer/initializers/catalog-category-registry.tsx index bef9fd240e..5b93b32644 100644 --- a/src/renderer/initializers/catalog-category-registry.tsx +++ b/src/renderer/initializers/catalog-category-registry.tsx @@ -3,16 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { kubernetesClusterCategory } from "../../common/catalog-entities"; +import type { KubernetesClusterCategory } from "../../common/catalog-entities"; import { isLinux, isWindows } from "../../common/vars"; import { PathPicker } from "../components/path-picker"; interface Dependencies { navigateToAddCluster: () => void; addSyncEntries: (filePaths: string[]) => void; + kubernetesClusterCategory: KubernetesClusterCategory; } -export function initCatalogCategoryRegistryEntries({ navigateToAddCluster, addSyncEntries } : Dependencies) { +export function initCatalogCategoryRegistryEntries({ + navigateToAddCluster, + addSyncEntries, + kubernetesClusterCategory, +} : Dependencies) { kubernetesClusterCategory.on("catalogAddMenu", ctx => { ctx.menuItems.push( { diff --git a/src/renderer/navigation/create-page-param.injectable.ts b/src/renderer/navigation/create-page-param.injectable.ts new file mode 100644 index 0000000000..19b96d9b88 --- /dev/null +++ b/src/renderer/navigation/create-page-param.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import observableHistoryInjectable from "./observable-history.injectable"; +import type { PageParamDependencies, PageParamInit } from "./page-param"; +import { PageParam } from "./page-param"; + +export type CreatePageParam = (init: PageParamInit) => PageParam; + +const createPageParamInjectable = getInjectable({ + id: "create-page-param", + instantiate: (di): CreatePageParam => { + const deps: PageParamDependencies = { + history: di.inject(observableHistoryInjectable), + }; + + return (init) => new PageParam(deps, init); + }, +}); + +export default createPageParamInjectable; diff --git a/src/renderer/navigation/helpers.ts b/src/renderer/navigation/helpers.ts deleted file mode 100644 index a7738c397f..0000000000 --- a/src/renderer/navigation/helpers.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { LocationDescriptor } from "history"; -import { createPath } from "history"; -import type { RouteProps } from "react-router"; -import { matchPath } from "react-router"; -import { navigation } from "./history"; -import type { PageParamInit } from "./page-param"; -import { PageParam } from "./page-param"; - -export function navigate(location: LocationDescriptor) { - const currentLocation = createPath(navigation.location); - - navigation.push(location); - - const newLocation = createPath(navigation.location); - - if (currentLocation === newLocation) { - navigation.goBack(); // prevent sequences of same url in history - } -} - -export function createPageParam(init: PageParamInit) { - return new PageParam(init, navigation); -} - -export function matchRoute

(route: string | string[] | RouteProps) { - return matchPath

(navigation.location.pathname, route); -} - -export function isActiveRoute(route: string | string[] | RouteProps): boolean { - return !!matchRoute(route); -} diff --git a/src/renderer/navigation/history/history.injectable.ts b/src/renderer/navigation/history.injectable.ts similarity index 100% rename from src/renderer/navigation/history/history.injectable.ts rename to src/renderer/navigation/history.injectable.ts diff --git a/src/renderer/navigation/history.ts b/src/renderer/navigation/history.ts deleted file mode 100644 index 1322ae9fcd..0000000000 --- a/src/renderer/navigation/history.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; -import observableHistoryInjectable from "./history/observable.injectable"; - -export { searchParamsOptions } from "./history/search-params"; - -/** - * @deprecated: Switch to using di.inject(observableHistoryInjectable) - */ -export const navigation = asLegacyGlobalForExtensionApi(observableHistoryInjectable); diff --git a/src/renderer/navigation/index.ts b/src/renderer/navigation/index.ts index f49f7672ea..5403b75629 100644 --- a/src/renderer/navigation/index.ts +++ b/src/renderer/navigation/index.ts @@ -3,8 +3,33 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -// Navigation (renderer) +import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; +import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; +import createPageParamInjectable from "./create-page-param.injectable"; +import matchRouteInjectable from "./match-route.injectable"; +import navigateInjectable from "./navigate.injectable"; +import observableHistoryInjectable from "./observable-history.injectable"; + +export { searchParamsOptions } from "./search-params"; + +/** + * @deprecated use `di.inject(observableHistoryInjectable)` instead + */ +export const navigation = asLegacyGlobalForExtensionApi(observableHistoryInjectable); + +/** + * @deprecated use `di.inject(navigateInjectable)` instead + */ +export const navigate = asLegacyGlobalFunctionForExtensionApi(navigateInjectable); + +/** + * @deprecated use `di.inject(createPageParamInjectable)` instead + */ +export const createPageParam = asLegacyGlobalFunctionForExtensionApi(createPageParamInjectable); + +/** + * @deprecated use `di.inject(matchRouteInjectable)` instead + */ +export const matchRoute = asLegacyGlobalFunctionForExtensionApi(matchRouteInjectable); -export * from "./history"; -export * from "./helpers"; export * from "./page-param"; diff --git a/src/renderer/navigation/is-route-active.injectable.ts b/src/renderer/navigation/is-route-active.injectable.ts new file mode 100644 index 0000000000..33a2525dc4 --- /dev/null +++ b/src/renderer/navigation/is-route-active.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { RouteProps } from "react-router"; +import matchRouteInjectable from "./match-route.injectable"; + +export type IsRouteActive = (route: string | string[] | RouteProps) => boolean; + +const isActiveRouteInjectable = getInjectable({ + id: "is-active-route", + instantiate: (di): IsRouteActive => { + const matchRoute = di.inject(matchRouteInjectable); + + return (route) => Boolean(matchRoute(route)); + }, +}); + +export default isActiveRouteInjectable; diff --git a/src/renderer/navigation/match-route.injectable.ts b/src/renderer/navigation/match-route.injectable.ts new file mode 100644 index 0000000000..df079f335f --- /dev/null +++ b/src/renderer/navigation/match-route.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { match, RouteProps } from "react-router"; +import { matchPath } from "react-router"; +import observableHistoryInjectable from "./observable-history.injectable"; + +export type MatchRoute = (route: string | string[] | RouteProps) => match | null; + +const matchRouteInjectable = getInjectable({ + id: "match-route", + instantiate: (di): MatchRoute => { + const observableHistory = di.inject(observableHistoryInjectable); + + return (route) => matchPath(observableHistory.location.pathname, route); + }, +}); + +export default matchRouteInjectable; diff --git a/src/renderer/navigation/navigate.injectable.ts b/src/renderer/navigation/navigate.injectable.ts new file mode 100644 index 0000000000..01995b0b94 --- /dev/null +++ b/src/renderer/navigation/navigate.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { LocationDescriptor } from "history"; +import { action } from "mobx"; +import observableHistoryInjectable from "./observable-history.injectable"; +import { createPath } from "history"; + +export type Navigate = (location: LocationDescriptor) => void; + +const navigateInjectable = getInjectable({ + id: "navigate", + instantiate: (di): Navigate => { + const observableHistory = di.inject(observableHistoryInjectable); + + return action((location) => { + const currentLocation = createPath(observableHistory.location); + + observableHistory.push(location); + + const newLocation = createPath(observableHistory.location); + + if (currentLocation === newLocation) { + observableHistory.goBack(); // prevent sequences of same url in history + } + }); + }, +}); + +export default navigateInjectable; diff --git a/src/renderer/navigation/history/observable.injectable.ts b/src/renderer/navigation/observable-history.injectable.ts similarity index 93% rename from src/renderer/navigation/history/observable.injectable.ts rename to src/renderer/navigation/observable-history.injectable.ts index bd7804dc6f..d3fd5bffba 100644 --- a/src/renderer/navigation/history/observable.injectable.ts +++ b/src/renderer/navigation/observable-history.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { createObservableHistory } from "mobx-observable-history"; -import loggerInjectable from "../../../common/logger.injectable"; +import loggerInjectable from "../../common/logger.injectable"; import { searchParamsOptions } from "./search-params"; import historyInjectable from "./history.injectable"; diff --git a/src/renderer/navigation/page-param.ts b/src/renderer/navigation/page-param.ts index 32dfad947e..eb65f5a682 100644 --- a/src/renderer/navigation/page-param.ts +++ b/src/renderer/navigation/page-param.ts @@ -7,19 +7,23 @@ import { action, makeObservable } from "mobx"; import type { ObservableHistory } from "mobx-observable-history"; -export interface PageParamInit { +export interface PageParamInit { name: string; - defaultValue?: V; // multi-values param must be defined with array-value, e.g. [] - parse?(value: string | string[]): V; // from URL - stringify?(value: V): string | string[]; // to URL + defaultValue?: Value; // multi-values param must be defined with array-value, e.g. [] + parse?(value: string | string[]): Value; // from URL + stringify?(value: Value): string | string[]; // to URL +} + +export interface PageParamDependencies { + readonly history: ObservableHistory; } // TODO: write tests -export class PageParam { +export class PageParam { readonly name: string; readonly isMulti: boolean; - constructor(private init: PageParamInit, private history: ObservableHistory) { + constructor(protected readonly dependencies: PageParamDependencies, private init: PageParamInit) { makeObservable(this); const { name, defaultValue } = init; @@ -28,11 +32,11 @@ export class PageParam { } // should be a getter since `init.defaultValue` could be a getter too - get defaultValue(): V | undefined { + get defaultValue(): Value | undefined { return this.init.defaultValue; } - parse(values: string | string[]): V { + parse(values: string | string[]): Value { const { parse } = this.init; if (parse) { @@ -40,10 +44,10 @@ export class PageParam { } // return as-is ("string"-value based params) - return values as any as V; + return values as unknown as Value; } - stringify(value: V = this.get()): string[] { + stringify(value: Value = this.get()): string[] { const { stringify } = this.init; if (stringify) { @@ -53,17 +57,17 @@ export class PageParam { return [value].flat().map(String); } - get(): V { + get(): Value { // TODO: cleanup // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.parse(this.getRaw()) ?? this.defaultValue!; } @action - set(value: V, { mergeGlobals = true, replaceHistory = false } = {}): void { + set(value: Value, { mergeGlobals = true, replaceHistory = false } = {}): void { const search = this.toString({ mergeGlobals, value }); - this.history.merge({ search }, replaceHistory); + this.dependencies.history.merge({ search }, replaceHistory); } /** @@ -77,10 +81,10 @@ export class PageParam { if (this.isMulti) { this.clear(); values.forEach(value => { - this.history.searchParams.append(this.name, value); + this.dependencies.history.searchParams.append(this.name, value); }); } else { - this.history.searchParams.set(this.name, values[0]); + this.dependencies.history.searchParams.set(this.name, values[0]); } } @@ -88,21 +92,21 @@ export class PageParam { * Get stringified raw value(s) from `document.location.search` */ getRaw(): string | string[] { - const values: string[] = this.history.searchParams.getAll(this.name); + const values: string[] = this.dependencies.history.searchParams.getAll(this.name); return this.isMulti ? values : values[0]; } @action clear() { - this.history.searchParams.delete(this.name); + this.dependencies.history.searchParams.delete(this.name); } toString({ mergeGlobals = true, value = this.get() } = {}): string { let searchParams = new URLSearchParams(); if (mergeGlobals) { - searchParams = new URLSearchParams(this.history.searchParams); + searchParams = new URLSearchParams(this.dependencies.history.searchParams); searchParams.delete(this.name); } diff --git a/src/renderer/navigation/history/search-params.ts b/src/renderer/navigation/search-params.ts similarity index 100% rename from src/renderer/navigation/history/search-params.ts rename to src/renderer/navigation/search-params.ts diff --git a/src/renderer/routes/current-path.injectable.ts b/src/renderer/routes/current-path.injectable.ts index 4ee8def26e..4ca6d64e8f 100644 --- a/src/renderer/routes/current-path.injectable.ts +++ b/src/renderer/routes/current-path.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; -import observableHistoryInjectable from "../navigation/history/observable.injectable"; +import observableHistoryInjectable from "../navigation/observable-history.injectable"; const currentPathInjectable = getInjectable({ id: "current-path", diff --git a/src/renderer/routes/extension-page-parameters.injectable.ts b/src/renderer/routes/extension-page-parameters.injectable.ts index 31bb30c87c..7ca7774662 100644 --- a/src/renderer/routes/extension-page-parameters.injectable.ts +++ b/src/renderer/routes/extension-page-parameters.injectable.ts @@ -5,11 +5,10 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { pipeline } from "@ogre-tools/fp"; import type { PageParamInit } from "../navigation"; -import { PageParam } from "../navigation"; -import observableHistoryInjectable from "../navigation/history/observable.injectable"; import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; import type { PageRegistration } from "../../extensions/registries"; -import { fromPairs, map, toPairs } from "lodash/fp"; +import { fromPairs, map } from "lodash/fp"; +import createPageParamInjectable from "../navigation/create-page-param.injectable"; interface InstantiationParameter { extension: LensRendererExtension; @@ -20,18 +19,18 @@ const extensionPageParametersInjectable = getInjectable({ id: "extension-page-parameters", instantiate: (di, { registration }: InstantiationParameter) => { - const observableHistory = di.inject(observableHistoryInjectable); + const createPageParam = di.inject(createPageParamInjectable); return pipeline( registration.params ?? {}, - toPairs, - map(([key, value]): [string, PageParamInit] => [ + Object.entries, + map(([key, value]): [string, PageParamInit] => [ key, typeof value === "string" ? convertStringToPageParamInit(key, value) - : convertPartialPageParamInitToFull(key, value as PageParamInit), + : convertPartialPageParamInitToFull(key, value), ]), - map(([key, value]) => [key, new PageParam(value, observableHistory)]), + map(([key, value]) => [key, createPageParam(value)]), fromPairs, ); }, @@ -44,20 +43,20 @@ const extensionPageParametersInjectable = getInjectable({ }), }); -const convertPartialPageParamInitToFull = ( +const convertPartialPageParamInitToFull = ( key: string, - value: PageParamInit, -): PageParamInit => ({ - name: key, - defaultValue: value.defaultValue, - stringify: value.stringify, - parse: value.parse, -}); + value: PageParamInit, +): PageParamInit => ({ + name: key, + defaultValue: value.defaultValue, + stringify: value.stringify, + parse: value.parse, + }); const convertStringToPageParamInit = ( key: string, value: string, -): PageParamInit => ({ +): PageParamInit => ({ name: key, defaultValue: value, }); diff --git a/src/renderer/routes/extension-route-registrator.injectable.tsx b/src/renderer/routes/extension-route-registrator.injectable.tsx index 9746ab24df..5b9e847ec4 100644 --- a/src/renderer/routes/extension-route-registrator.injectable.tsx +++ b/src/renderer/routes/extension-route-registrator.injectable.tsx @@ -10,8 +10,6 @@ import { observer } from "mobx-react"; import React from "react"; import { isEmpty, matches } from "lodash/fp"; import type { PageRegistration } from "../../extensions/registries"; -import observableHistoryInjectable from "../navigation/observable-history.injectable"; -import type { ObservableHistory } from "mobx-observable-history"; import { extensionRegistratorInjectionToken } from "../../extensions/extension-loader/extension-registrator-injection-token"; import { SiblingsInTabLayout } from "../components/layout/siblings-in-tab-layout"; import extensionPageParametersInjectable from "./extension-page-parameters.injectable"; @@ -24,14 +22,11 @@ const extensionRouteRegistratorInjectable = getInjectable({ id: "extension-route-registrator", instantiate: (di) => { - const observableHistory = di.inject(observableHistoryInjectable); - return (ext, extensionInstallationCount) => { const extension = ext as LensRendererExtension; const toRouteInjectable = toRouteInjectableFor( di, extension, - observableHistory, extensionInstallationCount, ); @@ -53,7 +48,6 @@ const toRouteInjectableFor = ( di: DiContainerForInstantiate, extension: LensRendererExtension, - observableHistory: ObservableHistory, extensionInstallationCount: number, ) => (clusterFrame: boolean) => diff --git a/src/renderer/routes/navigate-to-url.injectable.ts b/src/renderer/routes/navigate-to-url.injectable.ts index 468543d100..71f39ed8b4 100644 --- a/src/renderer/routes/navigate-to-url.injectable.ts +++ b/src/renderer/routes/navigate-to-url.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import observableHistoryInjectable from "../navigation/observable-history.injectable"; import { runInAction } from "mobx"; +import type { NavigateToUrl } from "../../common/front-end-routing/navigate-to-url-injection-token"; import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; import { broadcastMessage } from "../../common/ipc"; import { IpcRendererNavigationEvents } from "../navigation/events"; @@ -12,24 +13,20 @@ import { IpcRendererNavigationEvents } from "../navigation/events"; const navigateToUrlInjectable = getInjectable({ id: "navigate-to-url", - instantiate: (di) => { + instantiate: (di): NavigateToUrl => { const observableHistory = di.inject(observableHistoryInjectable); - return (url, options = {}) => { + return (url, options = {}): void => { if (options.forceRootFrame) { - broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url); - - return; + return void broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url); } runInAction(() => { if (options.withoutAffectingBackButton) { observableHistory.replace(url); - - return; + } else { + observableHistory.push(url); } - - observableHistory.push(url); }); }; }, diff --git a/src/renderer/routes/query-parameters.injectable.ts b/src/renderer/routes/query-parameters.injectable.ts index d8a5c3fec5..2faf4b67b9 100644 --- a/src/renderer/routes/query-parameters.injectable.ts +++ b/src/renderer/routes/query-parameters.injectable.ts @@ -13,11 +13,7 @@ const queryParametersInjectable = getInjectable({ instantiate: (di) => { const observableHistory = di.inject(observableHistoryInjectable); - return computed(() => { - const queryString = observableHistory.location.search; - - return parseQueryString(queryString); - }); + return computed(() => parseQueryString(observableHistory.location.search)); }, });