diff --git a/package-lock.json b/package-lock.json index 3cccd40daa..d97222a6d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3825,6 +3825,10 @@ "resolved": "packages/utility-features/run-many", "link": true }, + "node_modules/@k8slens/runtime-features": { + "resolved": "packages/business-features/runtime-features", + "link": true + }, "node_modules/@k8slens/semver": { "resolved": "packages/semver", "link": true @@ -34177,6 +34181,26 @@ "react": "^17 || ^18" } }, + "packages/business-features/runtime-features": { + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "^6.5.0-alpha.3", + "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.4", + "@k8slens/webpack": "^6.5.0-alpha.9" + }, + "peerDependencies": { + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/react-application": "^1.0.0-alpha.0", + "@ogre-tools/fp": "^16.1.0", + "@ogre-tools/injectable": "^16.1.0", + "@ogre-tools/injectable-extension-for-auto-registration": "^16.1.0", + "@ogre-tools/injectable-react": "^16.1.0", + "lodash": "^4.17.21", + "react": "^17 || ^18" + } + }, "packages/cluster-settings": { "name": "@k8slens/cluster-settings", "version": "6.5.0-alpha.8", diff --git a/packages/business-features/runtime-features/.eslintrc.json b/packages/business-features/runtime-features/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/business-features/runtime-features/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/business-features/runtime-features/.prettierrc b/packages/business-features/runtime-features/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/business-features/runtime-features/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/business-features/runtime-features/README.md b/packages/business-features/runtime-features/README.md new file mode 100644 index 0000000000..b6ffdc7100 --- /dev/null +++ b/packages/business-features/runtime-features/README.md @@ -0,0 +1,19 @@ +# @k8slens/runtime-features + +# Usage + +```bash +$ npm install @k8slens/runtime-features +``` + +```typescript +import { runtimeFeaturesFeature } from "@k8slens/runtime-features"; +import { registerFeature } from "@k8slens/feature-core"; +import { createContainer } from "@ogre-tools/injectable"; + +const di = createContainer("some-container"); + +registerFeature(di, runtimeFeaturesFeature); +``` + +## Extendability diff --git a/packages/business-features/runtime-features/index.ts b/packages/business-features/runtime-features/index.ts new file mode 100644 index 0000000000..9940ee8199 --- /dev/null +++ b/packages/business-features/runtime-features/index.ts @@ -0,0 +1,8 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; +export { runtimeFeaturesFeature } from "./src/feature"; + +export { mikkoFeature } from "./src/mikko-feature"; + +export const requireInjectionToken = getInjectionToken({ + id: "require-injection-token", +}); diff --git a/packages/business-features/runtime-features/jest.config.js b/packages/business-features/runtime-features/jest.config.js new file mode 100644 index 0000000000..38d54ab7b6 --- /dev/null +++ b/packages/business-features/runtime-features/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; diff --git a/packages/business-features/runtime-features/package.json b/packages/business-features/runtime-features/package.json new file mode 100644 index 0000000000..1334a3c9c7 --- /dev/null +++ b/packages/business-features/runtime-features/package.json @@ -0,0 +1,52 @@ +{ + "name": "@k8slens/runtime-features", + "private": false, + "version": "1.0.0", + "description": "TBD", + "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": "lens-webpack-build", + "clean": "rimraf dist/", + "test:unit": "jest --coverage --runInBand", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "peerDependencies": { + "@k8slens/feature-core": "^6.5.0-alpha.0", + "@k8slens/react-application": "^1.0.0-alpha.0", + "@ogre-tools/fp": "^16.1.0", + "@ogre-tools/injectable": "^16.1.0", + "@ogre-tools/injectable-extension-for-auto-registration": "^16.1.0", + "@ogre-tools/injectable-extension-for-mobx": "^16.1.0", + "@ogre-tools/injectable-react": "^16.1.0", + "lodash": "^4.17.21", + "react": "^17 || ^18", + "mobx": "^6.9.0", + "mobx-react": "^7.6.0" + }, + "devDependencies": { + "@async-fn/jest": "^1.6.4", + "@k8slens/eslint-config": "^6.5.0-alpha.3", + "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.4", + "@k8slens/webpack": "^6.5.0-alpha.9" + } +} diff --git a/packages/business-features/runtime-features/src/__snapshots__/keyboard-shortcuts.test.tsx.snap b/packages/business-features/runtime-features/src/__snapshots__/keyboard-shortcuts.test.tsx.snap new file mode 100644 index 0000000000..b2cba6a543 --- /dev/null +++ b/packages/business-features/runtime-features/src/__snapshots__/keyboard-shortcuts.test.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`keyboard-shortcuts when application is started renders 1`] = ` + +
+
+
+
+
+ +`; diff --git a/packages/business-features/runtime-features/src/feature.ts b/packages/business-features/runtime-features/src/feature.ts new file mode 100644 index 0000000000..3d75b31ead --- /dev/null +++ b/packages/business-features/runtime-features/src/feature.ts @@ -0,0 +1,17 @@ +import { getFeature } from "@k8slens/feature-core"; +import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration"; +import { reactApplicationFeature } from "@k8slens/react-application"; + +export const runtimeFeaturesFeature = getFeature({ + id: "runtime-features", + + register: (di) => { + autoRegister({ + di, + targetModule: module, + getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)], + }); + }, + + dependencies: [reactApplicationFeature], +}); diff --git a/packages/business-features/runtime-features/src/install-features.injectable.ts b/packages/business-features/runtime-features/src/install-features.injectable.ts new file mode 100644 index 0000000000..8394a70b99 --- /dev/null +++ b/packages/business-features/runtime-features/src/install-features.injectable.ts @@ -0,0 +1,41 @@ +import { pipeline } from "@ogre-tools/fp"; +import { DiContainer, getInjectable } from "@ogre-tools/injectable"; +import { requireInjectionToken } from "../index"; +import { map } from "lodash/fp"; +import { Feature, registerFeature } from "@k8slens/feature-core"; +import type React from "react"; +import { runInAction } from "mobx"; + +export const installFeaturesInjectable = getInjectable({ + id: "install-features", + instantiate: (di) => { + const requireAsd = di.inject(requireInjectionToken); + + const getDynamicFeature = (featureJsString: string) => { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const sandbox = new Function("module", "require", featureJsString); + const moduleFake = {}; + + sandbox(moduleFake, requireAsd); + + console.log("mikko", { moduleFake }); + + // @ts-ignore + return moduleFake.exports.default; + }; + + return async (event: React.ChangeEvent) => { + await pipeline( + event.target.files, + map((file) => file.text()), + (x) => Promise.all(x), + map(getDynamicFeature), + (y) => { + runInAction(() => { + y.forEach((x) => registerFeature(di as unknown as DiContainer, x as Feature)); + }); + }, + ); + }; + }, +}); diff --git a/packages/business-features/runtime-features/src/mikko-feature.tsx b/packages/business-features/runtime-features/src/mikko-feature.tsx new file mode 100644 index 0000000000..037b759f84 --- /dev/null +++ b/packages/business-features/runtime-features/src/mikko-feature.tsx @@ -0,0 +1,45 @@ +import { getFeature } from "@k8slens/feature-core"; +import { reactApplicationFeature } from "@k8slens/react-application"; + +import { computed } from "mobx"; +import { getInjectable } from "@ogre-tools/injectable"; +import { reactApplicationChildrenInjectionToken } from "@k8slens/react-application"; +import React from "react"; + +export const mikkoInjectable = getInjectable({ + id: "mikko", + + instantiate: () => ({ + id: "mikkomikko", + Component: () => ( +
+ Mikko +
+ ), + enabled: computed(() => true), + }), + + injectionToken: reactApplicationChildrenInjectionToken, +}); + +export const mikkoFeature = getFeature({ + id: "mikko", + + register: (di) => { + di.register(mikkoInjectable); + }, + + dependencies: [reactApplicationFeature], +}); diff --git a/packages/business-features/runtime-features/src/runtime-features-preferences.injectable.tsx b/packages/business-features/runtime-features/src/runtime-features-preferences.injectable.tsx new file mode 100644 index 0000000000..3740387875 --- /dev/null +++ b/packages/business-features/runtime-features/src/runtime-features-preferences.injectable.tsx @@ -0,0 +1,73 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { reactApplicationChildrenInjectionToken } from "@k8slens/react-application"; +import { computed, IComputedValue } from "mobx"; +import React from "react"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { featureInjectionToken } from "@k8slens/feature-core"; +import { observer } from "mobx-react"; +import type { FeatureAsd } from "@k8slens/feature-core"; +import { installFeaturesInjectable } from "./install-features.injectable"; + +interface Dependencies { + features: IComputedValue; + installFeatures: (event: any) => Promise; +} + +const NonInjectedRuntimeFeaturesPreferences = observer(({ features, installFeatures }: Dependencies) => ( +
+

Features

+
    + {features.get().map((x) => ( +
  • + {x.id} +
  • + ))} +
+ +
+ +
+ Register new feature + +
+
+)); + +export const RuntimeFeaturesPreferences = withInjectables( + NonInjectedRuntimeFeaturesPreferences, + + { + getProps: (di) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + + return { + features: computedInjectMany(featureInjectionToken), + installFeatures: di.inject(installFeaturesInjectable), + }; + }, + }, +); + +export const runtimeFeaturesPreferencesInjectable = getInjectable({ + id: "runtime-features-preferences", + + instantiate: () => ({ + id: "runtime-feature-preferences", + Component: RuntimeFeaturesPreferences, + enabled: computed(() => true), + }), + + injectionToken: reactApplicationChildrenInjectionToken, +}); diff --git a/packages/business-features/runtime-features/tsconfig.json b/packages/business-features/runtime-features/tsconfig.json new file mode 100644 index 0000000000..9e140d79da --- /dev/null +++ b/packages/business-features/runtime-features/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@k8slens/typescript/config/base.json", + "include": ["**/*.ts", "**/*.tsx"], +} diff --git a/packages/business-features/runtime-features/webpack.config.js b/packages/business-features/runtime-features/webpack.config.js new file mode 100644 index 0000000000..1cda407f5a --- /dev/null +++ b/packages/business-features/runtime-features/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForReact; diff --git a/packages/technical-features/feature-core/index.ts b/packages/technical-features/feature-core/index.ts index 13f6306665..3aed1bebfe 100644 --- a/packages/technical-features/feature-core/index.ts +++ b/packages/technical-features/feature-core/index.ts @@ -1,3 +1,4 @@ export { getFeature } from "./src/feature"; -export { registerFeature } from "./src/register-feature"; +export { registerFeature, featureInjectionToken } from "./src/register-feature"; +export type { FeatureAsd } from "./src/register-feature"; export type { Feature, GetFeatureArgs } from "./src/feature"; diff --git a/packages/technical-features/feature-core/package.json b/packages/technical-features/feature-core/package.json index 7305899072..21aaf43aa8 100644 --- a/packages/technical-features/feature-core/package.json +++ b/packages/technical-features/feature-core/package.json @@ -31,7 +31,8 @@ "lint:fix": "lens-lint --fix" }, "peerDependencies": { - "@ogre-tools/injectable": "^16.1.0" + "@ogre-tools/injectable": "^16.1.0", + "mobx": "^6.9.0" }, "devDependencies": { "@k8slens/eslint-config": "^6.5.0-alpha.3", diff --git a/packages/technical-features/feature-core/src/register-feature.ts b/packages/technical-features/feature-core/src/register-feature.ts index 535a93bf18..28be7059b7 100644 --- a/packages/technical-features/feature-core/src/register-feature.ts +++ b/packages/technical-features/feature-core/src/register-feature.ts @@ -1,7 +1,19 @@ import type { DiContainer } from "@ogre-tools/injectable"; -import { getInjectable } from "@ogre-tools/injectable"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; import type { Feature } from "./feature"; import { featureContextMapInjectable, featureContextMapInjectionToken } from "./feature-context-map-injectable"; +import { action, IComputedValue } from "mobx"; +import { computed, observable } from "mobx"; + +export type FeatureAsd = { + id: string; + enabled: IComputedValue; + toggle: () => void; +}; + +export const featureInjectionToken = getInjectionToken({ + id: "feature-injection-token", +}); const createFeatureContext = (feature: Feature, di: DiContainer) => { const featureContextInjectable = getInjectable({ @@ -26,6 +38,40 @@ const createFeatureContext = (feature: Feature, di: DiContainer) => { di.register(featureContextInjectable); + const featureAsdInjectable = getInjectable({ + id: `${feature.id}-feature`, + + instantiate: (di) => { + const enabled = observable.box(true); + + const featureContext = di.inject(featureContextInjectable); + + return { + id: feature.id, + + enabled: computed(() => { + return enabled.get(); + }), + + toggle: action(() => { + console.log("mikko", enabled.get()); + + if (enabled.get()) { + enabled.set(false); + featureContext.deregister(); + } else { + enabled.set(true); + featureContext.register(); + } + }), + }; + }, + + injectionToken: featureInjectionToken, + }); + + di.register(featureAsdInjectable); + const featureContextMap = di.inject(featureContextMapInjectable); const featureContext = di.inject(featureContextInjectable);