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`] = `
+
+
+
+`;
+
+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`] = `
+
+
+
+`;
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;