diff --git a/package-lock.json b/package-lock.json index 2fad4344c0..687ed39373 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4634,6 +4634,10 @@ "resolved": "packages/node-fetch", "link": true }, + "node_modules/@k8slens/react-application-root": { + "resolved": "packages/technical-features/react-application-root", + "link": true + }, "node_modules/@k8slens/react-testing-library-discovery": { "resolved": "packages/utility-features/react-testing-library-discovery", "link": true @@ -34327,6 +34331,57 @@ "integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==", "dev": true }, + "packages/business-features/dock": { + "name": "@k8slens/dock", + "version": "1.0.0-alpha.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1", + "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0" + }, + "peerDependencies": { + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "lodash": "^4.17.21" + } + }, + "packages/business-features/dock/agnostic": { + "version": "1.0.0-alpha.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1" + }, + "peerDependencies": { + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2" + } + }, + "packages/business-features/keyboard-shortcuts": { + "name": "@k8slens/keyboard-shortcuts", + "version": "1.0.0-alpha.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1", + "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0" + }, + "peerDependencies": { + "@k8slens/application": "6.5.0-alpha.2", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "lodash": "^4.17.21" + } + }, "packages/cluster-settings": { "name": "@k8slens/cluster-settings", "version": "6.5.0-alpha.1", @@ -39143,7 +39198,32 @@ "lodash": "^4.17.21" } }, + "packages/technical-features/react-application-root": { + "name": "@k8slens/react-application-root", + "version": "1.0.0-alpha.0", + "license": "MIT", + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1", + "@k8slens/react-testing-library-discovery": "*", + "@testing-library/react": "^12.1.5" + }, + "peerDependencies": { + "@k8slens/application": "^6.5.0-alpha.2", + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "@ogre-tools/injectable-react": "^15.1.2", + "lodash": "^4.17.15", + "mobx": "^6.8.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + } + }, "packages/utility-features/react-testing-library-discovery": { + "name": "@k8slens/react-testing-library-discovery", "version": "1.0.0-alpha.0", "license": "MIT", "dependencies": { diff --git a/packages/technical-features/react-application-root/.eslintrc.json b/packages/technical-features/react-application-root/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/technical-features/react-application-root/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/technical-features/react-application-root/.prettierrc b/packages/technical-features/react-application-root/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/technical-features/react-application-root/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/technical-features/react-application-root/README.md b/packages/technical-features/react-application-root/README.md new file mode 100644 index 0000000000..3e5f581636 --- /dev/null +++ b/packages/technical-features/react-application-root/README.md @@ -0,0 +1,19 @@ +# @k8slens/react-application-root + +# Usage + +```bash +$ npm install @k8slens/react-application-root +``` + +```typescript +import { reactApplicationRootFeature } from "@k8slens/application"; +import { registerFeature } from "@k8slens/feature-core"; +import { createContainer } from "@ogre-tools/injectable"; + +const di = createContainer("some-container"); + +registerFeature(di, reactApplicationRootFeature); +``` + +## Extendability diff --git a/packages/technical-features/react-application-root/index.ts b/packages/technical-features/react-application-root/index.ts new file mode 100644 index 0000000000..02b56ab9d8 --- /dev/null +++ b/packages/technical-features/react-application-root/index.ts @@ -0,0 +1,10 @@ +export { renderInjectionToken } from "./src/render-application/render.injectable"; +export type { Render } from "./src/render-application/render.injectable"; + +export { reactApplicationChildrenInjectionToken } from "./src/react-application/react-application-children-injection-token"; +export type { ReactApplicationChildren } from "./src/react-application/react-application-children-injection-token"; + +export { reactApplicationWrapperInjectionToken } from "./src/react-application/react-application-wrapper-injection-token"; +export type { ReactApplicationWrapper } from "./src/react-application/react-application-wrapper-injection-token"; + +export { reactApplicationRootFeature } from "./src/feature"; diff --git a/packages/technical-features/react-application-root/jest.config.js b/packages/technical-features/react-application-root/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/technical-features/react-application-root/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/technical-features/react-application-root/package.json b/packages/technical-features/react-application-root/package.json new file mode 100644 index 0000000000..c9370ae5da --- /dev/null +++ b/packages/technical-features/react-application-root/package.json @@ -0,0 +1,52 @@ +{ + "name": "@k8slens/react-application-root", + "private": false, + "version": "1.0.0-alpha.0", + "description": "Package for Application Root in React", + "type": "commonjs", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/application": "^6.5.0-alpha.2", + "@ogre-tools/fp": "^15.1.2", + "@ogre-tools/injectable": "^15.1.2", + "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", + "@ogre-tools/injectable-extension-for-mobx": "^15.1.2", + "@ogre-tools/injectable-react": "^15.1.2", + "lodash": "^4.17.15", + "mobx": "^6.8.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "6.5.0-alpha.1", + "@testing-library/react": "^12.1.5", + "@k8slens/react-testing-library-discovery": "*" + } +} diff --git a/packages/technical-features/react-application-root/src/__snapshots__/react-application.test.tsx.snap b/packages/technical-features/react-application-root/src/__snapshots__/react-application.test.tsx.snap new file mode 100644 index 0000000000..6b1b448294 --- /dev/null +++ b/packages/technical-features/react-application-root/src/__snapshots__/react-application.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`react-application renders 1`] = ` + +
+ +`; + +exports[`react-application when children is registered and enabled renders 1`] = ` + +
+
+ Some children +
+
+ +`; + +exports[`react-application when children is registered and enabled when children is enabled renders 1`] = ` + +
+ +`; + +exports[`react-application when children is registered and enabled when wrapper is registered renders 1`] = ` + +
+
+
+ Some children +
+
+
+ +`; diff --git a/packages/technical-features/react-application-root/src/feature.ts b/packages/technical-features/react-application-root/src/feature.ts new file mode 100644 index 0000000000..4fb7034006 --- /dev/null +++ b/packages/technical-features/react-application-root/src/feature.ts @@ -0,0 +1,17 @@ +import { getFeature } from "@k8slens/feature-core"; +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { applicationFeature } from "@k8slens/application"; + +export const reactApplicationRootFeature = getFeature({ + id: "react-application-root", + + register: (di) => { + autoRegister({ + di, + targetModule: module, + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, + + dependencies: [applicationFeature], +}); diff --git a/packages/technical-features/react-application-root/src/react-application.test.tsx b/packages/technical-features/react-application-root/src/react-application.test.tsx new file mode 100644 index 0000000000..50b1d0614b --- /dev/null +++ b/packages/technical-features/react-application-root/src/react-application.test.tsx @@ -0,0 +1,135 @@ +import { registerFeature } from "@k8slens/feature-core"; +import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable"; +import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; +import { registerInjectableReact } from "@ogre-tools/injectable-react"; +import { reactApplicationRootFeature } from "./feature"; +import { runInAction, computed, observable, IObservableValue } from "mobx"; +import { startApplicationInjectionToken } from "@k8slens/application"; +import type { RenderResult } from "@testing-library/react"; +import { render, act } from "@testing-library/react"; +import renderInjectable from "./render-application/render.injectable"; +import { reactApplicationChildrenInjectionToken } from "./react-application/react-application-children-injection-token"; +import React from "react"; +import { Discover, discoverFor } from "@k8slens/react-testing-library-discovery"; +import { reactApplicationWrapperInjectionToken } from "./react-application/react-application-wrapper-injection-token"; + +const SomeChildren = () =>
Some children
; + +describe("react-application", () => { + let rendered: RenderResult; + let di: DiContainer; + let discover: Discover; + + beforeEach(async () => { + di = createContainer("some-container"); + + registerInjectableReact(di); + + registerMobX(di); + + runInAction(() => { + registerFeature(di, reactApplicationRootFeature); + }); + + di.override(renderInjectable, () => (application) => { + rendered = render(application); + }); + + const startApplication = di.inject(startApplicationInjectionToken); + + await startApplication(); + + discover = discoverFor(() => rendered); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when children is registered and enabled", () => { + let someObservable: IObservableValue; + + beforeEach(() => { + someObservable = observable.box(true); + + const someChildrenInjectable = getInjectable({ + id: "some-children", + + instantiate: () => ({ + id: "some-children", + Component: SomeChildren, + enabled: computed(() => someObservable.get()), + }), + + injectionToken: reactApplicationChildrenInjectionToken, + }); + + runInAction(() => { + di.register(someChildrenInjectable); + }); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("renders the children", () => { + const { discovered } = discover.getSingleElement("some-children"); + + expect(discovered).not.toBeNull(); + }); + + describe("when wrapper is registered", () => { + beforeEach(() => { + const someWrapperInjectable = getInjectable({ + id: "some-wrapper", + + instantiate: () => (Component) => () => + ( +
+ +
+ ), + + injectionToken: reactApplicationWrapperInjectionToken, + }); + + runInAction(() => { + di.register(someWrapperInjectable); + }); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("renders the children inside the wrapper", () => { + const { discovered } = discover + .getSingleElement("some-wrapper") + .getSingleElement("some-children"); + + expect(discovered).not.toBeNull(); + }); + }); + + describe("when children is enabled", () => { + beforeEach(() => { + act(() => { + runInAction(() => { + someObservable.set(false); + }); + }); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not render the children", () => { + const { discovered } = discover.querySingleElement("some-children"); + + expect(discovered).toBeNull(); + }); + }); + }); +}); diff --git a/packages/technical-features/react-application-root/src/react-application/react-application-children-injection-token.ts b/packages/technical-features/react-application-root/src/react-application/react-application-children-injection-token.ts new file mode 100644 index 0000000000..398f55d89a --- /dev/null +++ b/packages/technical-features/react-application-root/src/react-application/react-application-children-injection-token.ts @@ -0,0 +1,13 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type React from "react"; +import type { IComputedValue } from "mobx"; + +export interface ReactApplicationChildren { + id: string; + Component: React.ComponentType; + enabled: IComputedValue; +} + +export const reactApplicationChildrenInjectionToken = getInjectionToken({ + id: "react-application-children-injection-token", +}); diff --git a/packages/technical-features/react-application-root/src/react-application/react-application-content.tsx b/packages/technical-features/react-application-root/src/react-application/react-application-content.tsx new file mode 100644 index 0000000000..a7b2400553 --- /dev/null +++ b/packages/technical-features/react-application-root/src/react-application/react-application-content.tsx @@ -0,0 +1,29 @@ +import { withInjectables } from "@ogre-tools/injectable-react"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import React from "react"; +import { + ReactApplicationChildren, + reactApplicationChildrenInjectionToken, +} from "./react-application-children-injection-token"; +import type { IComputedValue } from "mobx"; +import { observer, Observer } from "mobx-react"; + +type Dependencies = { children: IComputedValue }; + +const NonInjectedContent = observer(({ children }: Dependencies) => ( + <> + {children.get().map((child) => ( + {() => (child.enabled.get() ? : null)} + ))} + +)); + +export const ReactApplicationContent = withInjectables( + NonInjectedContent, + + { + getProps: (di) => ({ + children: di.inject(computedInjectManyInjectable)(reactApplicationChildrenInjectionToken), + }), + }, +); diff --git a/packages/technical-features/react-application-root/src/react-application/react-application-wrapper-injection-token.ts b/packages/technical-features/react-application-root/src/react-application/react-application-wrapper-injection-token.ts new file mode 100644 index 0000000000..629c9b279d --- /dev/null +++ b/packages/technical-features/react-application-root/src/react-application/react-application-wrapper-injection-token.ts @@ -0,0 +1,8 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type React from "react"; + +export type ReactApplicationWrapper = (Component: React.ComponentType) => React.ComponentType; + +export const reactApplicationWrapperInjectionToken = getInjectionToken({ + id: "react-application-wrapper-injection-token", +}); diff --git a/packages/technical-features/react-application-root/src/react-application/react-application.tsx b/packages/technical-features/react-application-root/src/react-application/react-application.tsx new file mode 100644 index 0000000000..0d016d0f28 --- /dev/null +++ b/packages/technical-features/react-application-root/src/react-application/react-application.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainerForInjection } from "@ogre-tools/injectable"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { DiContextProvider } from "@ogre-tools/injectable-react"; +import { flow, identity } from "lodash/fp"; +import { observer } from "mobx-react"; +import React from "react"; +import { reactApplicationWrapperInjectionToken } from "./react-application-wrapper-injection-token"; + +import { ReactApplicationContent } from "./react-application-content"; + +interface ReactApplicationProps { + di: DiContainerForInjection; +} + +export const ReactApplication = observer(({ di }: ReactApplicationProps) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + + const wrappers = computedInjectMany(reactApplicationWrapperInjectionToken); + + const ContentWithWrappers = flow(identity, ...wrappers.get())(ReactApplicationContent); + + return ( + + + + ); +}); diff --git a/packages/technical-features/react-application-root/src/render-application/render-application-when-application-is-ready.injectable.tsx b/packages/technical-features/react-application-root/src/render-application/render-application-when-application-is-ready.injectable.tsx new file mode 100644 index 0000000000..c18f18fc5f --- /dev/null +++ b/packages/technical-features/react-application-root/src/render-application/render-application-when-application-is-ready.injectable.tsx @@ -0,0 +1,21 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { afterApplicationIsLoadedInjectionToken } from "@k8slens/application"; +import renderInjectable from "./render.injectable"; +import { ReactApplication } from "../react-application/react-application"; +import React from "react"; + +export const renderApplicationWhenApplicationIsReadyInjectable = getInjectable({ + id: "render-application-when-application-is-ready", + + instantiate: (di) => { + const render = di.inject(renderInjectable); + + return { + run: () => { + render(); + }, + }; + }, + + injectionToken: afterApplicationIsLoadedInjectionToken, +}); diff --git a/packages/technical-features/react-application-root/src/render-application/render.injectable.tsx b/packages/technical-features/react-application-root/src/render-application/render.injectable.tsx new file mode 100644 index 0000000000..944823f10a --- /dev/null +++ b/packages/technical-features/react-application-root/src/render-application/render.injectable.tsx @@ -0,0 +1,22 @@ +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { render } from "react-dom"; +import type React from "react"; + +export type Render = (application: React.ReactElement) => void; + +export const renderInjectionToken = getInjectionToken({ + id: "render-injection-token", +}); + +const renderInjectable = getInjectable({ + id: "render", + + /* c8 ignore next */ + instantiate: () => (application) => render(application, document.getElementById("app")), + + causesSideEffects: true, + + injectionToken: renderInjectionToken, +}); + +export default renderInjectable; diff --git a/packages/technical-features/react-application-root/tsconfig.json b/packages/technical-features/react-application-root/tsconfig.json new file mode 100644 index 0000000000..ec29a8f75f --- /dev/null +++ b/packages/technical-features/react-application-root/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/packages/technical-features/react-application-root/webpack.config.js b/packages/technical-features/react-application-root/webpack.config.js new file mode 100644 index 0000000000..1cda407f5a --- /dev/null +++ b/packages/technical-features/react-application-root/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForReact;