diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md index 84f1cbdffa..44c40430d8 100644 --- a/docs/extensions/capabilities/common-capabilities.md +++ b/docs/extensions/capabilities/common-capabilities.md @@ -100,6 +100,8 @@ import { ExamplePage } from "./src/example-page" export default class ExampleRendererExtension extends LensRendererExtension { globalPages = [ { + id: "example", + routePath: "/example", components: { Page: ExamplePage, } @@ -109,6 +111,7 @@ export default class ExampleRendererExtension extends LensRendererExtension { globalPageMenus = [ { title: "Example page", // used in icon's tooltip + target: { pageId: "example" } components: { Icon: () => , } diff --git a/extensions/example-extension/renderer.tsx b/extensions/example-extension/renderer.tsx index a1fb546df7..7733ec6e47 100644 --- a/extensions/example-extension/renderer.tsx +++ b/extensions/example-extension/renderer.tsx @@ -5,11 +5,21 @@ import React from "react" export default class ExampleExtension extends LensRendererExtension { clusterPages = [ { - path: "/extension-example", + id: "example", + routePath: "/extension-example", title: "Example Extension", components: { Page: () => , - MenuIcon: ExampleIcon, + } + } + ] + + clusterPageMenus = [ + { + target: { pageId: "example", params: {} }, + title: "Example Extension", + components: { + Icon: ExampleIcon, } } ] diff --git a/extensions/support-page/renderer.tsx b/extensions/support-page/renderer.tsx index 0137ea7771..92d460fdfc 100644 --- a/extensions/support-page/renderer.tsx +++ b/extensions/support-page/renderer.tsx @@ -5,6 +5,8 @@ import { SupportPage } from "./src/support"; export default class SupportPageRendererExtension extends LensRendererExtension { globalPages: Interface.PageRegistration[] = [ { + id: "support", + routePath: "/support", components: { Page: SupportPage, } @@ -14,7 +16,7 @@ export default class SupportPageRendererExtension extends LensRendererExtension statusBarItems: Interface.StatusBarRegistration[] = [ { item: ( -
this.navigate()}> +
this.navigate("/support")}>
) diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts new file mode 100644 index 0000000000..c7e9ed5513 --- /dev/null +++ b/src/extensions/__tests__/lens-extension.test.ts @@ -0,0 +1,23 @@ +import { LensExtension } from "../lens-extension" + +let ext: LensExtension = null + +describe("lens extension", () => { + beforeEach(async () => { + ext = new LensExtension({ + manifest: { + name: "foo-bar", + version: "0.1.1" + }, + manifestPath: "/this/is/fake/package.json", + isBundled: false, + isEnabled: true + }) + }) + + describe("name", () => { + it("returns name", () => { + expect(ext.name).toBe("foo-bar") + }) + }) +}) diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index b75fd5147d..969cff33b1 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -57,29 +57,29 @@ export class ExtensionLoader { loadOnMain() { logger.info('[EXTENSIONS-LOADER]: load on main') this.autoInitExtensions((ext: LensMainExtension) => [ - registries.menuRegistry.add(ext.appMenus, { key: ext }) + registries.menuRegistry.add(ext.appMenus) ]); } loadOnClusterManagerRenderer() { logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') this.autoInitExtensions((ext: LensRendererExtension) => [ - registries.globalPageRegistry.add(ext.globalPages, { key: ext }), - registries.globalPageMenuRegistry.add(ext.globalPageMenus, { key: ext }), - registries.appPreferenceRegistry.add(ext.appPreferences, { key: ext }), - registries.clusterFeatureRegistry.add(ext.clusterFeatures, { key: ext }), - registries.statusBarRegistry.add(ext.statusBarItems, { key: ext }), + registries.globalPageRegistry.add(ext.globalPages, ext), + registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext), + registries.appPreferenceRegistry.add(ext.appPreferences), + registries.clusterFeatureRegistry.add(ext.clusterFeatures), + registries.statusBarRegistry.add(ext.statusBarItems), ]); } loadOnClusterRenderer() { logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') this.autoInitExtensions((ext: LensRendererExtension) => [ - registries.clusterPageRegistry.add(ext.clusterPages, { key: ext }), - registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, { key: ext }), - registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems, { key: ext }), - registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems, { key: ext }), - registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts, { key: ext }) + registries.clusterPageRegistry.add(ext.clusterPages, ext), + registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext), + registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems), + registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems), + registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts) ]) } diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index ca135a0b39..f1ffb9184b 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,6 +1,5 @@ import type { InstalledExtension } from "./extension-manager"; import { action, observable, reaction } from "mobx"; -import { compile } from "path-to-regexp" import logger from "../main/logger"; export type LensExtensionId = string; // path to manifest (package.json) @@ -15,7 +14,6 @@ export interface LensExtensionManifest { } export class LensExtension { - readonly routePrefix = "/extension/:name" readonly manifest: LensExtensionManifest; readonly manifestPath: string; readonly isBundled: boolean; @@ -44,14 +42,6 @@ export class LensExtension { return this.manifest.description } - getPageUrl(baseUrl = "") { - return compile(this.routePrefix)({ name: this.name }) + baseUrl; - } - - getPageRoute(baseRoute = "") { - return this.routePrefix + baseRoute; - } - @action async enable() { if (this.isEnabled) return; diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 0055344a66..de9aae40ec 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -2,13 +2,14 @@ import type { MenuRegistration } from "./registries/menu-registry"; import { observable } from "mobx"; import { LensExtension } from "./lens-extension" import { WindowManager } from "../main/window-manager"; +import { getPageUrl } from "./registries/page-registry" export class LensMainExtension extends LensExtension { @observable.shallow appMenus: MenuRegistration[] = [] async navigate(location?: string, frameId?: number) { const windowManager = WindowManager.getInstance(); - const url = this.getPageUrl(location); // get full path to extension's page + const url = getPageUrl(this, location); // get full path to extension's page await windowManager.navigate(url, frameId); } } diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 87ca214805..5968124cbc 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,6 +1,7 @@ import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries" import { observable } from "mobx"; import { LensExtension } from "./lens-extension" +import { getPageUrl } from "./registries/page-registry" export class LensRendererExtension extends LensExtension { @observable.shallow globalPages: PageRegistration[] = [] @@ -16,6 +17,6 @@ export class LensRendererExtension extends LensExtension { async navigate(location?: string) { const { navigate } = await import("../renderer/navigation"); - navigate(this.getPageUrl(location)); + navigate(getPageUrl(this, location)); } } diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts new file mode 100644 index 0000000000..b293b598dc --- /dev/null +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -0,0 +1,31 @@ +import { getPageUrl } from "../page-registry" +import { LensExtension } from "../../lens-extension" + +let ext: LensExtension = null + +describe("getPageUrl", () => { + beforeEach(async () => { + ext = new LensExtension({ + manifest: { + name: "foo-bar", + version: "0.1.1" + }, + manifestPath: "/this/is/fake/package.json", + isBundled: false, + isEnabled: true + }) + }) + + it("returns a page url for extension", () => { + expect(getPageUrl(ext)).toBe("/extension/foo-bar") + }) + + it("allows to pass base url as parameter", () => { + expect(getPageUrl(ext, "/test")).toBe("/extension/foo-bar/test") + }) + + it("removes @", () => { + ext.manifest.name = "@foo/bar" + expect(getPageUrl(ext)).toBe("/extension/foo-bar") + }) +}) diff --git a/src/extensions/registries/app-preference-registry.ts b/src/extensions/registries/app-preference-registry.ts index dc15ec3e20..6c54911f82 100644 --- a/src/extensions/registries/app-preference-registry.ts +++ b/src/extensions/registries/app-preference-registry.ts @@ -1,12 +1,12 @@ import type React from "react" -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; export interface AppPreferenceComponents { Hint: React.ComponentType; Input: React.ComponentType; } -export interface AppPreferenceRegistration extends BaseRegistryItem { +export interface AppPreferenceRegistration { title: string; components: AppPreferenceComponents; } diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts index 76678791f9..bc23c29f5d 100644 --- a/src/extensions/registries/base-registry.ts +++ b/src/extensions/registries/base-registry.ts @@ -1,65 +1,24 @@ // Base class for extensions-api registries import { action, observable } from "mobx"; -import { LensExtension } from "../lens-extension"; -import { getRandId } from "../../common/utils"; -export type BaseRegistryKey = LensExtension | null; -export type BaseRegistryItemId = string | symbol; +export class BaseRegistry { + private items = observable([], { deep: false }); -export interface BaseRegistryItem { - id?: BaseRegistryItemId; // uniq id, generated automatically when not provided -} - -export interface BaseRegistryAddMeta { - key?: BaseRegistryKey; - merge?: boolean -} - -export class BaseRegistry { - private items = observable.map([], { deep: false }); - - getItems(): (T & { extension?: LensExtension | null })[] { - return Array.from(this.items).map(([ext, items]) => { - return items.map(item => ({ - ...item, - extension: ext, - })) - }).flat() - } - - getById(itemId: BaseRegistryItemId, key?: BaseRegistryKey): T { - const byId = (item: BaseRegistryItem) => item.id === itemId; - if (key) { - return this.items.get(key)?.find(byId) - } - return this.getItems().find(byId); + getItems(): T[] { + return this.items.toJS(); } @action - add(items: T | T[], { key = null, merge = true }: BaseRegistryAddMeta = {}) { - const normalizedItems = (Array.isArray(items) ? items : [items]).map((item: T) => { - item.id = item.id || getRandId(); - return item; - }); - if (merge && this.items.has(key)) { - const newItems = new Set(this.items.get(key)); - normalizedItems.forEach(item => newItems.add(item)) - this.items.set(key, [...newItems]); - } else { - this.items.set(key, normalizedItems); - } - return () => this.remove(normalizedItems, key) + add(items: T | T[]) { + const normalizedItems = (Array.isArray(items) ? items : [items]) + this.items.push(...normalizedItems); + return () => this.remove(...normalizedItems); } @action - remove(items: T[], key: BaseRegistryKey = null) { - const storedItems = this.items.get(key); - if (!storedItems) return; - const newItems = storedItems.filter(item => !items.includes(item)); // works because of {deep: false}; - if (newItems.length > 0) { - this.items.set(key, newItems) - } else { - this.items.delete(key); - } + remove(...items: T[]) { + items.forEach(item => { + this.items.remove(item); // works because of {deep: false}; + }) } } diff --git a/src/extensions/registries/cluster-feature-registry.ts b/src/extensions/registries/cluster-feature-registry.ts index 17a4ef1eef..0e3363d0f0 100644 --- a/src/extensions/registries/cluster-feature-registry.ts +++ b/src/extensions/registries/cluster-feature-registry.ts @@ -1,12 +1,12 @@ import type React from "react" -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; import { ClusterFeature } from "../cluster-feature"; export interface ClusterFeatureComponents { Description: React.ComponentType; } -export interface ClusterFeatureRegistration extends BaseRegistryItem { +export interface ClusterFeatureRegistration { title: string; components: ClusterFeatureComponents feature: ClusterFeature diff --git a/src/extensions/registries/kube-object-detail-registry.ts b/src/extensions/registries/kube-object-detail-registry.ts index 63d9cfa30d..32fea66b81 100644 --- a/src/extensions/registries/kube-object-detail-registry.ts +++ b/src/extensions/registries/kube-object-detail-registry.ts @@ -1,11 +1,11 @@ import React from "react" -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; export interface KubeObjectDetailComponents { Details: React.ComponentType; } -export interface KubeObjectDetailRegistration extends BaseRegistryItem { +export interface KubeObjectDetailRegistration { kind: string; apiVersions: string[]; components: KubeObjectDetailComponents; diff --git a/src/extensions/registries/kube-object-menu-registry.ts b/src/extensions/registries/kube-object-menu-registry.ts index 3e0cfb0251..8f527d6a3d 100644 --- a/src/extensions/registries/kube-object-menu-registry.ts +++ b/src/extensions/registries/kube-object-menu-registry.ts @@ -1,11 +1,11 @@ import React from "react" -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; export interface KubeObjectMenuComponents { MenuItem: React.ComponentType; } -export interface KubeObjectMenuRegistration extends BaseRegistryItem { +export interface KubeObjectMenuRegistration { kind: string; apiVersions: string[]; components: KubeObjectMenuComponents; diff --git a/src/extensions/registries/kube-object-status-registry.ts b/src/extensions/registries/kube-object-status-registry.ts index 3a516a82e1..74fd8145d2 100644 --- a/src/extensions/registries/kube-object-status-registry.ts +++ b/src/extensions/registries/kube-object-status-registry.ts @@ -1,7 +1,7 @@ import { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api"; -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; -export interface KubeObjectStatusRegistration extends BaseRegistryItem { +export interface KubeObjectStatusRegistration { kind: string; apiVersions: string[]; resolve: (object: KubeObject) => KubeObjectStatus; diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts index 6bd161ccb2..dc1ef6669c 100644 --- a/src/extensions/registries/page-menu-registry.ts +++ b/src/extensions/registries/page-menu-registry.ts @@ -1,15 +1,21 @@ // Extensions-api -> Register page menu items import type React from "react"; +import { action } from "mobx"; import type { IconProps } from "../../renderer/components/icon"; -import { BaseRegistry, BaseRegistryItem, BaseRegistryItemId } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; +import { LensExtension } from "../lens-extension"; -export interface PageMenuRegistration extends BaseRegistryItem { - id: BaseRegistryItemId; // required id from page-registry item to match with - url?: string; // when not provided initial extension's path used, e.g. "/extension/lens-extension-name" +export interface PageMenuTarget { + pageId: string; + extensionId?: string; + params?: object; +} + +export interface PageMenuRegistration { + target?: PageMenuTarget; title: React.ReactNode; components: PageMenuComponents; - subMenus?: PageSubMenuRegistration[]; } export interface PageSubMenuRegistration { @@ -22,13 +28,18 @@ export interface PageMenuComponents { } export class PageMenuRegistry extends BaseRegistry { - getItems() { - return super.getItems().map(item => { - item.url = item.extension.getPageUrl(item.url) - return item - }); + + @action + add(items: T[], ext?: LensExtension) { + const normalizedItems = items.map((menu) => { + if (menu.target && !menu.target.extensionId) { + menu.target.extensionId = ext.name + } + return menu + }) + return super.add(normalizedItems); } } -export const globalPageMenuRegistry = new PageMenuRegistry>(); -export const clusterPageMenuRegistry = new PageMenuRegistry(); +export const globalPageMenuRegistry = new PageMenuRegistry(); +export const clusterPageMenuRegistry = new PageMenuRegistry(); diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index 886ab8a584..7c34a7b9c4 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -1,13 +1,17 @@ // Extensions-api -> Custom page registration import React from "react"; -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { action } from "mobx"; +import { compile } from "path-to-regexp"; +import { BaseRegistry } from "./base-registry"; +import { LensExtension } from "../lens-extension" +import type { PageMenuTarget } from "./page-menu-registry"; -export interface PageRegistration extends BaseRegistryItem { +export interface PageRegistration { + id: string; // will be automatically prefixed with extension name routePath?: string; // additional (suffix) route path to base extension's route: "/extension/:name" exact?: boolean; // route matching flag, see: https://reactrouter.com/web/api/NavLink/exact-bool components: PageComponents; - subPages?: SubPageRegistration[]; } export interface SubPageRegistration { @@ -20,14 +24,31 @@ export interface PageComponents { Page: React.ComponentType; } +const routePrefix = "/extension/:name" + +export function getPageUrl(ext: LensExtension, baseUrl = "") { + const validUrlName = ext.name.replace("@", "").replace("/", "-"); + return compile(routePrefix)({ name: validUrlName }) + baseUrl; +} + export class PageRegistry extends BaseRegistry { - getItems() { - return super.getItems().map(item => { - item.routePath = item.extension.getPageRoute(item.routePath) - return item - }); + + @action + add(items: T[], ext?: LensExtension) { + const normalizedItems = items.map((i) => { + i.routePath = getPageUrl(ext, i.routePath) + return i + }) + return super.add(normalizedItems); + } + + getByPageMenuTarget(target: PageMenuTarget) { + if (!target) { + return null + } + return this.getItems().find((page) => page.routePath.startsWith(`/extension/${target.extensionId}/`) && page.id === target.pageId) } } -export const globalPageRegistry = new PageRegistry>(); -export const clusterPageRegistry = new PageRegistry(); +export const globalPageRegistry = new PageRegistry(); +export const clusterPageRegistry = new PageRegistry(); diff --git a/src/extensions/registries/status-bar-registry.ts b/src/extensions/registries/status-bar-registry.ts index c1029e6d68..88c4132d30 100644 --- a/src/extensions/registries/status-bar-registry.ts +++ b/src/extensions/registries/status-bar-registry.ts @@ -1,9 +1,9 @@ // Extensions API -> Status bar customizations import React from "react"; -import { BaseRegistry, BaseRegistryItem } from "./base-registry"; +import { BaseRegistry } from "./base-registry"; -export interface StatusBarRegistration extends BaseRegistryItem { +export interface StatusBarRegistration { item?: React.ReactNode; } diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 45b7d565f2..cb8d02172b 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -74,27 +74,8 @@ export class App extends React.Component { } renderExtensionRoutes() { - return clusterPageRegistry.getItems().map(({ id: pageId, components: { Page }, exact, routePath, subPages }) => { + return clusterPageRegistry.getItems().map(({ components: { Page }, exact, routePath }) => { const Component = () => { - if (subPages) { - const tabs: TabLayoutRoute[] = subPages.map(({ exact, routePath, components: { Page } }) => { - const menuItem = clusterPageMenuRegistry.getById(pageId); - if (!menuItem) return; - return { - routePath, exact, - component: Page, - url: menuItem.url, - title: menuItem.title, - } - }).filter(Boolean); - if (tabs.length > 0) { - return ( - - - - ) - } - } return }; return diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 7844399227..302e4e76d2 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -5,7 +5,6 @@ import { remote } from "electron" import type { Cluster } from "../../../main/cluster"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { observer } from "mobx-react"; -import { matchPath } from "react-router"; import { _i18n } from "../../i18n"; import { t, Trans } from "@lingui/macro"; import { userStore } from "../../../common/user-store"; @@ -15,7 +14,7 @@ import { ClusterIcon } from "../cluster-icon"; import { Icon } from "../icon"; import { autobind, cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; -import { navigate, navigation } from "../../navigation"; +import { isActiveRoute, navigate } from "../../navigation"; import { addClusterURL } from "../+add-cluster"; import { clusterSettingsURL } from "../+cluster-settings"; import { landingURL } from "../+landing-page"; @@ -24,6 +23,7 @@ import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL } from "./cluster-view.route"; import { globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; +import { compile } from "path-to-regexp"; interface Props { className?: IClassName; @@ -149,17 +149,16 @@ export class ClustersMenu extends React.Component { )}
- {globalPageMenuRegistry.getItems().map(({ id: menuItemId, title, url, components: { Icon } }) => { - const registeredPage = globalPageRegistry.getById(menuItemId); + {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { + const registeredPage = globalPageRegistry.getByPageMenuTarget(target); if (!registeredPage) return; const { routePath, exact } = registeredPage; - const isActive = !!matchPath(navigation.location.pathname, { path: routePath, exact }); return ( navigate(url)} + active={isActiveRoute({ path: routePath, exact })} + onClick={() => navigate(compile(routePath)(target.params))} /> ) })} diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index b70d7a3e62..8485f1e134 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -30,6 +30,7 @@ import { isActiveRoute } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac" import { Spinner } from "../spinner"; import { clusterPageMenuRegistry, clusterPageRegistry } from "../../../extensions/registries"; +import { compile } from "path-to-regexp"; const SidebarContext = React.createContext({ pinned: false }); type SidebarContextValue = { @@ -191,10 +192,11 @@ export class Sidebar extends React.Component { > {this.renderCustomResources()} - {clusterPageMenuRegistry.getItems().map(({ id: menuItemId, title, url, components: { Icon } }) => { - const registeredPage = clusterPageRegistry.getById(menuItemId); + {clusterPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { + const registeredPage = clusterPageRegistry.getByPageMenuTarget(target); if (!registeredPage) return; const { routePath, exact } = registeredPage; + const url = compile(routePath)(target.params) return (