From c4c4bc58452d10a985179706e1667589cb373e0d Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Wed, 10 Nov 2021 14:01:02 +0200 Subject: [PATCH] Introduce a way to use injectables for UI Signed-off-by: Janne Savolainen --- package.json | 2 + src/renderer/cluster-frame.tsx | 73 ++++++++++--------- src/renderer/components/getDi.tsx | 31 ++++++++ .../components/getDiForUnitTesting.tsx | 53 ++++++++++++++ types/ogre-tools-injectable-react.d.ts | 45 ++++++++++++ types/ogre-tools-injectable.d.ts | 69 ++++++++++++++++++ yarn.lock | 24 ++++++ 7 files changed, 263 insertions(+), 34 deletions(-) create mode 100644 src/renderer/components/getDi.tsx create mode 100644 src/renderer/components/getDiForUnitTesting.tsx create mode 100644 types/ogre-tools-injectable-react.d.ts create mode 100644 types/ogre-tools-injectable.d.ts diff --git a/package.json b/package.json index 8bc2f1e99e..10e4d8acae 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,8 @@ "@kubernetes/client-node": "^0.15.1", "@sentry/electron": "^2.5.4", "@sentry/integrations": "^6.15.0", + "@ogre-tools/injectable": "^1.0.2", + "@ogre-tools/injectable-react": "^1.0.3", "abort-controller": "^3.0.0", "auto-bind": "^4.0.0", "autobind-decorator": "^2.4.0", diff --git a/src/renderer/cluster-frame.tsx b/src/renderer/cluster-frame.tsx index 0786ca18a5..e4e2b148d6 100755 --- a/src/renderer/cluster-frame.tsx +++ b/src/renderer/cluster-frame.tsx @@ -22,6 +22,7 @@ import React from "react"; import { observable, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { Redirect, Route, Router, Switch } from "react-router"; +import { DiContextProvider } from "@ogre-tools/injectable-react"; import { history } from "./navigation"; import { NotFound } from "./components/+404"; import { UserManagement } from "./components/+user-management/user-management"; @@ -73,12 +74,14 @@ import { watchHistoryState } from "./remote-helpers/history-updater"; import { unmountComponentAtNode } from "react-dom"; import { PortForwardDialog } from "./port-forward"; import { DeleteClusterDialog } from "./components/delete-cluster-dialog"; +import { getDi } from "./components/getDi"; @observer export class ClusterFrame extends React.Component { static clusterId: ClusterId; static readonly logPrefix = "[CLUSTER-FRAME]:"; static displayName = "ClusterFrame"; + diContainer = getDi(); constructor(props: {}) { super(props); @@ -193,40 +196,42 @@ export class ClusterFrame extends React.Component { render() { return ( - - - } footer={}> - - - - - - - - - - - - - {this.renderExtensionTabLayoutRoutes()} - {this.renderExtensionRoutes()} - - - - - - - - - - - - - - - - - + + + + } footer={}> + + + + + + + + + + + + + {this.renderExtensionTabLayoutRoutes()} + {this.renderExtensionRoutes()} + + + + + + + + + + + + + + + + + + ); } } diff --git a/src/renderer/components/getDi.tsx b/src/renderer/components/getDi.tsx new file mode 100644 index 0000000000..745573e6ea --- /dev/null +++ b/src/renderer/components/getDi.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { createContainer } from "@ogre-tools/injectable"; +import type { IConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; + +export const getDi = () => { + const di: IConfigurableDependencyInjectionContainer = createContainer( + () => require.context("./", true, /\.injectable\.(ts|tsx)$/), + ); + + return di; +}; diff --git a/src/renderer/components/getDiForUnitTesting.tsx b/src/renderer/components/getDiForUnitTesting.tsx new file mode 100644 index 0000000000..0ce9b6c9b0 --- /dev/null +++ b/src/renderer/components/getDiForUnitTesting.tsx @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import glob from "glob"; +import { memoize } from "lodash/fp"; + +import { + createContainer, + IConfigurableDependencyInjectionContainer, +} from "@ogre-tools/injectable"; + +export const getDiForUnitTesting = () => { + const di: IConfigurableDependencyInjectionContainer = createContainer(); + + getInjectableFilePaths() + .map(key => { + const injectable = require(key).default; + + return { + id: key, + ...injectable, + aliases: [injectable, ...(injectable.aliases || [])], + }; + }) + + .forEach(injectable => di.register(injectable)); + + di.preventSideEffects(); + + return di; +}; + +const getInjectableFilePaths = memoize(() => + glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), +); diff --git a/types/ogre-tools-injectable-react.d.ts b/types/ogre-tools-injectable-react.d.ts new file mode 100644 index 0000000000..3624dec706 --- /dev/null +++ b/types/ogre-tools-injectable-react.d.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +/// +declare module "@ogre-tools/injectable-react" { + import type { IDependencyInjectionContainer } from "@ogre-tools/injectable"; + + interface IDependencyInjectionContainerProviderProps { + di: IDependencyInjectionContainer; + } + + export const DiContextProvider: React.Provider; + + export const Inject: < + TComponentInjectable extends IComponentInjectable, + >({ + Component, + injectableKey, + getPlaceholder, + ...props + }: Omit< + React.ComponentProps, + keyof ReturnType + > & { + injectableKey: TComponentInjectable; + getPlaceholder?: () => JSX.Element | null; + }) => JSX.Element; +} diff --git a/types/ogre-tools-injectable.d.ts b/types/ogre-tools-injectable.d.ts new file mode 100644 index 0000000000..08c5b3f286 --- /dev/null +++ b/types/ogre-tools-injectable.d.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +declare module "@ogre-tools/injectable" { + export interface IDependencyInjectionContainer { + inject: < + TInjectable extends IInjectable, + TInstance, + TDependencies, + >( + injectableKey: TInjectable, + ) => ReturnType; + } + + export interface IConfigurableDependencyInjectionContainer + extends IDependencyInjectionContainer { + register: ( + injectable: IInjectable | IComponentInjectable, + ) => void; + preventSideEffects: () => void; + + override: , TInstance>( + injectable: TInjectable, + overrider: ReturnType, + ) => void; + } + + interface ICommonInjectable { + id?: string; + getDependencies: (di?: IDependencyInjectionContainer) => TDependencies; + lifecycle?: lifecycleEnum; + } + + export interface IInjectable + extends ICommonInjectable { + instantiate: (dependencies: TDependencies) => TInstance; + } + + export interface IComponentInjectable + extends ICommonInjectable { + instantiate: TInstance; + } + + export enum lifecycleEnum { + singleton, + transient, + scopedTransient, + } + + export const createContainer = (...getRequireContexts: any[]) => + IConfigurableDependencyInjectionContainer; +} diff --git a/yarn.lock b/yarn.lock index a427e9f745..c78e8fed10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -946,6 +946,30 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@ogre-tools/fp@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-1.0.2.tgz#26c2c5cf60aa01cc94763cc68beba7052fdadfd9" + integrity sha512-ftvi/aoi5PaojWnuhHzp0YiecUd22HzW5gErsSiKyO2bps90WI4WjgY6d9hWdlzM9eukVmwM+dC6rGNlltNHNw== + dependencies: + lodash "^4.17.21" + +"@ogre-tools/injectable-react@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-1.0.3.tgz#e30ea455cc4ccf24fbad831b460023492832c2f9" + integrity sha512-CFJeuezdJIZD0o0rp1MnahzItzZ7eSixSo0v0+002kq8T7HZb1ycD8d/zvXOBnkEF34K6GGibXicEw1q46SZzw== + dependencies: + "@ogre-tools/fp" "^1.0.2" + "@ogre-tools/injectable" "^1.0.2" + lodash "^4.17.21" + +"@ogre-tools/injectable@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-1.0.2.tgz#b081fefb2026c57fe47e27c268efa73d43934546" + integrity sha512-NZ7FHxKLfr+8o4aL51UQ212w1wD3QIsEN/JNtjtEq0TgLjd2Qo3zSniL4hCEj5M49K2qe3CxEb6ezyf2vFTb5g== + dependencies: + "@ogre-tools/fp" "^1.0.2" + lodash "^4.17.21" + "@panva/asn1.js@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6"