From f1b03990ea64ebde5dad70acef4a61e6cd1d12a9 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 9 Sep 2020 13:00:25 +0300 Subject: [PATCH] Extensions loading (#795) Signed-off-by: Roman Co-authored-by: Alex Andreev Co-authored-by: Lauri Nevala --- .gitignore | 6 +- package.json | 23 ++- src/common/cluster-store.ts | 5 +- src/common/vars.ts | 8 + src/extensions/example-extension/README.md | 3 + .../example-extension/example-extension.ts | 17 ++ src/extensions/example-extension/package.json | 11 ++ .../example-extension/tsconfig.json | 13 ++ src/extensions/extension-api.ts | 22 +++ src/extensions/extension-store.ts | 174 ++++++++++++++++++ src/extensions/extension.ts | 89 +++++++++ src/extensions/lens-runtime.ts | 19 ++ src/extensions/tsconfig.json | 13 ++ src/main/menu.ts | 10 +- src/renderer/bootstrap.tsx | 2 + .../+extensions/extensions.route.ts | 11 ++ .../components/+extensions/extensions.scss | 4 + .../components/+extensions/extensions.tsx | 31 ++++ src/renderer/components/+extensions/index.ts | 2 + .../components/cluster-icon/cluster-icon.scss | 1 - .../cluster-manager/cluster-manager.tsx | 2 + .../cluster-manager/clusters-menu.scss | 16 +- .../cluster-manager/clusters-menu.tsx | 9 +- .../cluster-manager/register-page.ts | 28 +++ src/renderer/lens-app.tsx | 6 + static/RELEASE_NOTES.md | 19 +- tsconfig.json | 1 - types/mocks.d.ts | 1 + webpack.dll.ts | 34 ---- webpack.renderer.ts | 31 +++- yarn.lock | 19 +- 31 files changed, 564 insertions(+), 66 deletions(-) create mode 100644 src/extensions/example-extension/README.md create mode 100644 src/extensions/example-extension/example-extension.ts create mode 100644 src/extensions/example-extension/package.json create mode 100644 src/extensions/example-extension/tsconfig.json create mode 100644 src/extensions/extension-api.ts create mode 100644 src/extensions/extension-store.ts create mode 100644 src/extensions/extension.ts create mode 100644 src/extensions/lens-runtime.ts create mode 100644 src/extensions/tsconfig.json create mode 100644 src/renderer/components/+extensions/extensions.route.ts create mode 100644 src/renderer/components/+extensions/extensions.scss create mode 100644 src/renderer/components/+extensions/extensions.tsx create mode 100644 src/renderer/components/+extensions/index.ts create mode 100644 src/renderer/components/cluster-manager/register-page.ts delete mode 100755 webpack.dll.ts diff --git a/.gitignore b/.gitignore index d6efc880e7..6ddf3f489c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,14 @@ dist/ -out/ node_modules/ .DS_Store yarn-error.log coverage/ tmp/ -static/build/** +static/build +static/types binaries/client/ binaries/server/ +src/extensions/*/*.js +src/extensions/*/*.d.ts locales/**/**.js lens.log diff --git a/package.json b/package.json index 9a1b57733e..920d90afb5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "3.6.0-beta.2", + "version": "3.6.0-rc.1", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", @@ -11,14 +11,16 @@ "email": "info@k8slens.dev" }, "scripts": { - "dev": "concurrently -k \"yarn dev-run -C\" \"yarn dev:main\" \"yarn dev:renderer\"", - "dev-run": "cross-env DEBUG=true nodemon --watch static/build/main.js --exec \"electron --inspect .\"", - "dev:main": "cross-env DEBUG=true yarn compile:main --watch", - "dev:renderer": "cross-env DEBUG=true yarn compile:renderer --watch", + "dev": "concurrently -k \"yarn dev-run -C\" yarn:dev:*", + "dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"", + "dev:main": "yarn compile:main --watch", + "dev:renderer": "yarn compile:renderer --watch", + "dev:extensions": "tsc --project src/extensions/example-extension --watch", "compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile:main": "webpack --config webpack.main.ts", "compile:renderer": "webpack --config webpack.renderer.ts", "compile:i18n": "lingui compile", + "compile:extension-api.d.ts": "tsc --project src/extensions", "build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens", "build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens", "build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens", @@ -69,7 +71,8 @@ }, "moduleNameMapper": { "\\.(css|scss)$": "/__mocks__/styleMock.ts" - } + }, + "modulePathIgnorePatterns": ["/dist"] }, "build": { "afterSign": "build/notarize.js", @@ -89,6 +92,11 @@ "to": "static/", "filter": "!**/main.js" }, + { + "from": "src/extensions/", + "to": "./extensions/", + "filter": "**/*.js*" + }, "LICENSE" ], "linux": { @@ -167,6 +175,7 @@ "@types/lodash": "^4.14.155", "@types/marked": "^0.7.4", "@types/mock-fs": "^4.10.0", + "@types/module-alias": "^2.0.0", "@types/node": "^12.12.45", "@types/proper-lockfile": "^4.1.1", "@types/react-beautiful-dnd": "^13.0.0", @@ -193,6 +202,7 @@ "mobx": "^5.15.5", "mobx-observable-history": "^1.0.3", "mock-fs": "^4.12.0", + "module-alias": "^2.2.2", "node-machine-id": "^1.1.12", "node-pty": "^0.9.0", "openid-client": "^3.15.2", @@ -269,7 +279,6 @@ "circular-dependency-plugin": "^5.2.0", "color": "^3.1.2", "concurrently": "^5.2.0", - "cross-env": "^7.0.2", "css-element-queries": "^1.2.3", "css-loader": "^3.5.3", "dompurify": "^2.0.11", diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 45bcad4d6f..a30c4d7e94 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,4 +1,4 @@ -import { WorkspaceId, workspaceStore } from "./workspace-store"; +import type { WorkspaceId } from "./workspace-store"; import path from "path"; import { app, ipcRenderer, remote } from "electron"; import { unlink } from "fs-extra"; @@ -13,7 +13,6 @@ import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; import _ from "lodash"; import move from "array-move"; -import { is } from "immer/dist/internal"; export interface ClusterIconUpload { clusterId: string; @@ -64,7 +63,7 @@ export class ClusterStore extends BaseStore { static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string { const filePath = ClusterStore.getCustomKubeConfigPath(clusterId); const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig); - saveToAppFiles(filePath, fileContents, { mode: 0o600}); + saveToAppFiles(filePath, fileContents, { mode: 0o600 }); return filePath; } diff --git a/src/common/vars.ts b/src/common/vars.ts index ca28a2f99a..c17c54e118 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -2,6 +2,7 @@ import path from "path"; import packageInfo from "../../package.json" import { defineGlobal } from "./utils/defineGlobal"; +import { addAlias } from "module-alias"; export const isMac = process.platform === "darwin" export const isWindows = process.platform === "win32" @@ -21,6 +22,13 @@ export const rendererDir = path.join(contextDir, "src/renderer"); export const htmlTemplate = path.resolve(rendererDir, "template.html"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); +// Extensions +export const extensionsLibName = `${appName}-extensions.api` +export const extensionsDir = path.join(contextDir, "src/extensions"); + +// Special dynamic module aliases +addAlias("@lens/extensions", path.resolve(buildDir, `${extensionsLibName}.js`)); // fixme: provide path in prod + // Special runtime paths defineGlobal("__static", { get() { diff --git a/src/extensions/example-extension/README.md b/src/extensions/example-extension/README.md new file mode 100644 index 0000000000..eb8d01ac38 --- /dev/null +++ b/src/extensions/example-extension/README.md @@ -0,0 +1,3 @@ +# Lens Example Extension + +*TODO*: add more info \ No newline at end of file diff --git a/src/extensions/example-extension/example-extension.ts b/src/extensions/example-extension/example-extension.ts new file mode 100644 index 0000000000..28b4da5825 --- /dev/null +++ b/src/extensions/example-extension/example-extension.ts @@ -0,0 +1,17 @@ +import { LensExtension, Icon, LensRuntimeRendererEnv } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts" + +// todo: register custom icon in cluster-menu +// todo: register custom view by clicking the item + +export default class ExampleExtension extends LensExtension { + async enable(runtime: /*LensRuntimeRendererEnv*/ any): Promise { + try { + super.enable(runtime); + runtime.logger.info('EXAMPLE EXTENSION: ENABLE() override'); + } catch (err){ + console.error(err) + } + } +} + +// console.log("done")}/> \ No newline at end of file diff --git a/src/extensions/example-extension/package.json b/src/extensions/example-extension/package.json new file mode 100644 index 0000000000..d57a78aab5 --- /dev/null +++ b/src/extensions/example-extension/package.json @@ -0,0 +1,11 @@ +{ + "name": "extension-example", + "version": "1.0.0", + "description": "Example extension", + "main": "example-extension.ts", + "lens": { + "metadata": {} + }, + "dependencies": { + } +} diff --git a/src/extensions/example-extension/tsconfig.json b/src/extensions/example-extension/tsconfig.json new file mode 100644 index 0000000000..16081f6055 --- /dev/null +++ b/src/extensions/example-extension/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": ".", + "module": "CommonJS", + "sourceMap": false, + "declaration": false + }, + "include": [ + "../../../types", + "./example-extension.ts" + ] +} diff --git a/src/extensions/extension-api.ts b/src/extensions/extension-api.ts new file mode 100644 index 0000000000..3dff72d00c --- /dev/null +++ b/src/extensions/extension-api.ts @@ -0,0 +1,22 @@ +// Lens-extensions api developer's kit +export type { LensRuntimeRendererEnv } from "./lens-runtime"; + +// APIs +export * from "./extension" + +// Common UI components +export * from "../renderer/components/icon" +export * from "../renderer/components/badge" +export * from "../renderer/components/tooltip" +export * from "../renderer/components/button" +export * from "../renderer/components/input" +export * from "../renderer/components/select" +export * from "../renderer/components/checkbox" +export * from "../renderer/components/radio" +export * from "../renderer/components/slider" +export * from "../renderer/components/spinner" +export * from "../renderer/components/tabs" +export * from "../renderer/components/line-progress" + +// Utils +export * from "../main/logger"; diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts new file mode 100644 index 0000000000..655c4d6ca7 --- /dev/null +++ b/src/extensions/extension-store.ts @@ -0,0 +1,174 @@ +import type { LensRuntimeRendererEnv } from "./lens-runtime"; +import path from "path"; +import fs from "fs-extra"; +import { action, observable, reaction, toJS, } from "mobx"; +import { BaseStore } from "../common/base-store"; +import { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./extension"; +import { isDevelopment, isProduction, isTestEnv } from "../common/vars"; +import logger from "../main/logger"; + +export interface ExtensionStoreModel { + version: ExtensionVersion; + extensions: Record +} + +export interface ExtensionModel { + id?: ExtensionId; // available in lens-extension instance + version: ExtensionVersion; + name: string; + manifestPath: string; + description?: string; + enabled?: boolean; + updateUrl?: string; +} + +export interface InstalledExtension { + manifestPath: string; + manifest: ExtensionManifest; + extensionModule: { + [name: string]: any; + default: new (model: ExtensionModel, manifest?: ExtensionManifest) => LensExtension + } +} + +export class ExtensionStore extends BaseStore { + private constructor() { + super({ + configName: "lens-extension-store", + syncEnabled: false, + }); + } + + @observable version: ExtensionVersion = "0.0.0"; + @observable extensions = observable.map(); + @observable removed = observable.map(); + @observable installed = observable.map([], { deep: false }); + + get folderPath(): string { + if (isDevelopment) { + return path.resolve(__static, "../src/extensions"); + } + return path.resolve(__static, "../extensions"); //todo figure out prod + } + + async load() { + await this.loadExtensions(); + return super.load(); + } + + autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) { + logger.info('[EXTENSIONS-STORE]: auto-activation loaded extensions: ON'); + return reaction(() => this.installed.toJS(), installedExtensions => { + installedExtensions.forEach(({ extensionModule, manifest, manifestPath }) => { + let instance = this.getById(manifestPath); + if (!instance) { + const LensExtensionClass = extensionModule.default; + instance = new LensExtensionClass({ ...manifest, manifestPath, id: manifestPath }, manifest); + instance.enable(getLensRuntimeEnv()); + this.extensions.set(manifestPath, instance); // save + } + }) + }, { + fireImmediately: true, + delay: delay, + }) + } + + getExtensionByManifest(manifestPath: string): InstalledExtension { + let manifestJson: ExtensionManifest; + let mainJs: string; + try { + manifestJson = __non_webpack_require__(manifestPath); // "__non_webpack_require__" converts to native node's require()-call + mainJs = path.resolve(path.dirname(manifestPath), manifestJson.main); + mainJs = mainJs.replace(/\.ts$/i, ".js"); // todo: compile *.ts on the fly? + const extensionModule = __non_webpack_require__(mainJs); + return { + manifestPath: manifestPath, + manifest: manifestJson, + extensionModule: extensionModule, + } + } catch (err) { + console.error(`[EXTENSION-STORE]: can't load extension at ${manifestPath}: ${err}`, { manifestJson, mainJs }); + } + } + + @action + async loadExtensions() { + const extensions = await this.loadFromFolder(this.folderPath); + const extManifestMap = new Map(extensions.map(ext => [ext.manifestPath, ext])); + this.installed.replace(extManifestMap); + } + + async loadFromFolder(folderPath: string): Promise { + const paths = await fs.readdir(folderPath); + const manifestsLoading = paths.map(fileName => { + const absPath = path.resolve(folderPath, fileName); + const manifestPath = path.resolve(absPath, "package.json"); + return fs.access(manifestPath, fs.constants.F_OK) + .then(() => this.getExtensionByManifest(manifestPath)) + .catch(() => null) + }); + let extensions = await Promise.all(manifestsLoading); + extensions = extensions.filter(v => !!v); // filter out files and invalid folders (without manifest.json) + console.info(`[EXTENSION-STORE]: ${extensions.length} extensions loaded`, { folderPath, extensions }); + return extensions; + } + + getById(id: ExtensionId): LensExtension { + return this.extensions.get(id); + } + + async removeById(id: ExtensionId) { + const extension = this.getById(id); + if (extension) { + await extension.uninstall(); + this.extensions.delete(id); + } + } + + @action + protected fromStore({ extensions, version }: ExtensionStoreModel) { + if (version) { + this.version = version; + } + if (extensions) { + const currentExtensions = new Map(Object.entries(extensions)); + this.extensions.forEach(extension => { + if (!currentExtensions.has(extension.id)) { + this.removed.set(extension.id, extension); + } + }) + currentExtensions.forEach(model => { + const extensionId = model.id || model.manifestPath; + const manifest = this.installed.get(extensionId); + if (!manifest) { + console.error(`[EXTENSION-STORE]: can't load extension manifest at ${model.manifestPath}`, { model }) + return; + } + const extensionInstance = this.getById(extensionId) + if (!extensionInstance) { + try { + const { manifest: manifestJson, extensionModule } = manifest; + const LensExtensionClass = extensionModule.default; + this.extensions.set(model.id, new LensExtensionClass(model, manifestJson)); + } catch (err) { + console.error(`[EXTENSION-STORE]: init extension failed: ${err}`, { model, manifest }) + } + } else { + extensionInstance.importModel(model); + } + }) + } + } + + toJSON(): ExtensionStoreModel { + return toJS({ + version: this.version, + extensions: this.extensions.toJSON(), + }, { + recurseEverything: true, + }) + } +} + +export const extensionStore = ExtensionStore.getInstance() diff --git a/src/extensions/extension.ts b/src/extensions/extension.ts new file mode 100644 index 0000000000..698422fb54 --- /dev/null +++ b/src/extensions/extension.ts @@ -0,0 +1,89 @@ +import type { ExtensionModel } from "./extension-store"; +import type { LensRuntimeRendererEnv } from "./lens-runtime"; +import { readJsonSync } from "fs-extra"; +import { action, observable, toJS } from "mobx"; +import extensionManifest from "./example-extension/package.json" +import logger from "../main/logger"; + +export type ExtensionId = string; // instance-id or abs path to "%lens-extension/manifest.json" +export type ExtensionVersion = string | number; +export type ExtensionManifest = typeof extensionManifest & ExtensionModel; + +export class LensExtension implements ExtensionModel { + public id: ExtensionId; + public updateUrl: string; + + @observable name = ""; + @observable description = ""; + @observable version: ExtensionVersion = "0.0.0"; + @observable manifest: ExtensionManifest; + @observable manifestPath: string; + @observable isEnabled = false; + @observable.ref runtime: Partial = {}; + + constructor(model: ExtensionModel, manifest: ExtensionManifest) { + this.importModel(model, manifest); + } + + @action + async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) { + try { + this.manifest = manifest || await readJsonSync(manifestPath, { throws: true }) + this.manifestPath = manifestPath; + Object.assign(this, model); + } catch (err) { + logger.error(`[EXTENSION]: cannot read manifest at ${manifestPath}`, { ...model, err: String(err) }) + this.disable(); + } + } + + async enable(runtime: LensRuntimeRendererEnv) { + this.isEnabled = true; + this.runtime = runtime; + console.log(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta()); + } + + async disable() { + this.isEnabled = false; + this.runtime = {}; + console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta()); + } + + // todo + async install(downloadUrl?: string) { + return; + } + + // todo + async uninstall() { + return; + } + + async hasNewVersion(): Promise> { + return; + } + + getMeta() { + return toJS({ + id: this.id, + manifest: this.manifest, + manifestPath: this.manifestPath, + enabled: this.isEnabled, + runtime: this.runtime, + }, { + recurseEverything: true + }) + } + + toJSON(): ExtensionModel { + return { + id: this.id, + name: this.name, + version: this.version, + description: this.description, + manifestPath: this.manifestPath, + enabled: this.isEnabled, + updateUrl: this.updateUrl, + } + } +} diff --git a/src/extensions/lens-runtime.ts b/src/extensions/lens-runtime.ts new file mode 100644 index 0000000000..e456bb435f --- /dev/null +++ b/src/extensions/lens-runtime.ts @@ -0,0 +1,19 @@ +// Lens runtime for injecting to extension on activation +import { apiManager } from "../renderer/api/api-manager"; +import logger from "../main/logger"; +import { dynamicPages } from "../renderer/components/cluster-manager/register-page"; + +export interface LensRuntimeRendererEnv { + apiManager: typeof apiManager; + logger: typeof logger; + dynamicPages: typeof dynamicPages +} + +// todo: expose more public runtime apis: stores, managers, etc. +export function getLensRuntime(): LensRuntimeRendererEnv { + return { + apiManager, + logger, + dynamicPages, + } +} diff --git a/src/extensions/tsconfig.json b/src/extensions/tsconfig.json new file mode 100644 index 0000000000..bc688c7ebe --- /dev/null +++ b/src/extensions/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "AMD", + "declaration": true, + "emitDeclarationOnly": true, + "outFile": "./../../static/types/extension-api.d.ts" + }, + "include": [ + "../../types", + "./extension-api.ts" + ] +} diff --git a/src/main/menu.ts b/src/main/menu.ts index 1b0f3434d7..cbe8645e42 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -6,6 +6,7 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; +import { extensionsURL } from "../renderer/components/+extensions/extensions.route"; import logger from "./logger"; export function initMenu(windowManager: WindowManager) { @@ -67,11 +68,18 @@ export function buildMenu(windowManager: WindowManager) { { type: 'separator' }, { label: 'Preferences', - accelerator: 'Cmd+,', + accelerator: 'CmdOrCtrl+,', click() { navigate(preferencesURL()) } }, + { + label: 'Extensions', + accelerator: 'CmdOrCtrl+E', + click() { + navigate(extensionsURL()) + } + }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 0fcb216cd9..e4b24c3035 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -4,6 +4,7 @@ import { render } from "react-dom"; import { isMac } from "../common/vars"; import { userStore } from "../common/user-store"; import { workspaceStore } from "../common/workspace-store"; +import { extensionStore } from "../extensions/extension-store"; import { clusterStore, getHostedClusterId } from "../common/cluster-store"; import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; @@ -23,6 +24,7 @@ export async function bootstrap(App: AppComponent) { userStore.load(), workspaceStore.load(), clusterStore.load(), + extensionStore.load(), i18nStore.init(), themeStore.init(), ]); diff --git a/src/renderer/components/+extensions/extensions.route.ts b/src/renderer/components/+extensions/extensions.route.ts new file mode 100644 index 0000000000..5744aabb0e --- /dev/null +++ b/src/renderer/components/+extensions/extensions.route.ts @@ -0,0 +1,11 @@ +import { RouteProps } from "react-router"; +import { buildURL } from "../../navigation"; + +export const extensionsRoute: RouteProps = { + path: "/extensions" +} + +export interface IExtensionsRouteParams { +} + +export const extensionsURL = buildURL(extensionsRoute.path); diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss new file mode 100644 index 0000000000..496b03c161 --- /dev/null +++ b/src/renderer/components/+extensions/extensions.scss @@ -0,0 +1,4 @@ +.Extensions { + $spacing: $padding * 2; + padding: $spacing; +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx new file mode 100644 index 0000000000..18e1b2aeec --- /dev/null +++ b/src/renderer/components/+extensions/extensions.tsx @@ -0,0 +1,31 @@ +import "./extensions.scss" +import React from "react"; +import { observer } from "mobx-react"; +import { extensionStore } from "../../../extensions/extension-store"; +import { WizardLayout } from "../layout/wizard-layout"; +import { Icon } from "../icon"; + +@observer +export class Extensions extends React.Component { + // todo: add input-select to customize extensions loading folder(s) + renderInfoPanel() { + return ( +
+ +

Extensions available to install

+
+ ); + } + + render() { + const { installed: installedExtensions } = extensionStore; + return ( + +

Extensions

+
+          {JSON.stringify(installedExtensions.toJSON(), null, 2)}
+        
+
+ ); + } +} diff --git a/src/renderer/components/+extensions/index.ts b/src/renderer/components/+extensions/index.ts new file mode 100644 index 0000000000..8946a5f6fe --- /dev/null +++ b/src/renderer/components/+extensions/index.ts @@ -0,0 +1,2 @@ +export * from "./extensions.route" +export * from "./extensions" diff --git a/src/renderer/components/cluster-icon/cluster-icon.scss b/src/renderer/components/cluster-icon/cluster-icon.scss index c64e6e07ab..540cecf9eb 100644 --- a/src/renderer/components/cluster-icon/cluster-icon.scss +++ b/src/renderer/components/cluster-icon/cluster-icon.scss @@ -4,7 +4,6 @@ position: relative; border-radius: $radius; padding: $radius; - margin-bottom: $padding * 2; user-select: none; cursor: pointer; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 6011549e8c..987f22658d 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -11,6 +11,7 @@ import { Workspaces, workspacesRoute } from "../+workspaces"; import { AddCluster, addClusterRoute } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings"; +import { Extensions, extensionsRoute } from "../+extensions"; import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; @@ -60,6 +61,7 @@ export class ClusterManager extends React.Component { + diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss index db1a182b38..bc4a8ef75c 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ b/src/renderer/components/cluster-manager/clusters-menu.scss @@ -16,10 +16,10 @@ .clusters { @include hidden-scrollbar; padding: 0 $spacing; // extra spacing for cluster-icon's badge - margin-bottom: $spacing; + margin-bottom: $margin; - > :last-child { - margin-bottom: $margin; + .ClusterIcon { + margin-bottom: $margin * 1.5; } &:empty { @@ -67,4 +67,14 @@ pointer-events: none; } } + + > .dynamic-pages { + &:not(:empty) { + padding-top: $spacing; + } + + .Icon { + --size: 40px; + } + } } \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 07780830ff..495ce2176a 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -21,8 +21,7 @@ import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route"; import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd"; - -// fixme: allow to rearrange clusters with drag&drop +import { dynamicPages } from "./register-page"; interface Props { className?: IClassName; @@ -149,6 +148,12 @@ export class ClustersMenu extends React.Component { new} /> )} +
+ {Array.from(dynamicPages.all).map(([path, { MenuIcon }]) => { + if (!MenuIcon) return; + return navigate(path)}/> + })} +
); } diff --git a/src/renderer/components/cluster-manager/register-page.ts b/src/renderer/components/cluster-manager/register-page.ts new file mode 100644 index 0000000000..71a6345cae --- /dev/null +++ b/src/renderer/components/cluster-manager/register-page.ts @@ -0,0 +1,28 @@ +// Dynamic pages + +import React from "react"; +import { observable } from "mobx"; +import type { IconProps } from "../icon"; + +export interface PageComponents { + Main: React.ComponentType; + MenuIcon: React.ComponentType; +} + +export class PagesStore { + all = observable.map(); + + getComponents(path: string): PageComponents | null { + return this.all.get(path); + } + + register(path: string, components: PageComponents) { + this.all.set(path, components); + } + + unregister(path: string) { + this.all.delete(path); + } +} + +export const dynamicPages = new PagesStore(); diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 9b4fffc6a1..7c1f228fd3 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -11,9 +11,15 @@ import { ErrorBoundary } from "./components/error-boundary"; import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; import { Notifications } from "./components/notifications"; import { ConfirmDialog } from "./components/confirm-dialog"; +import { extensionStore } from "../extensions/extension-store"; +import { getLensRuntime } from "../extensions/lens-runtime"; @observer export class LensApp extends React.Component { + componentDidMount() { + extensionStore.autoEnableOnLoad(getLensRuntime); + } + render() { return ( diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 6e53eb990d..d0553e542a 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,24 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 3.6.0-beta.2 (current version) +## 3.6.0-rc.1 (current version) +- Allow user to configure directory where Kubectl binaries are downloaded +- Allow user to configure path to Kubectl binary, instead of using bundled Kubectl +- Log application logs also to log file +- Restrict file permissions to only the user for pasted kubeconfigs +- Close Preferences and Cluster Setting on Esc keypress +- Update Kubectl versions used with Lens +- Update Helm binary version +- Fix: Update CRD api to use preferred version and implement v1 differences +- Fix: Allow to drag and drop cluster icons +- Fix: Wider version select box for Helm chart installation +- Fix: Reload only active dashboard view, not the whole app window +- Fix cluster icon margins +- Fix: Reconnect non-accessible clusters on reconnect +- Fix: Bundle Kubectl and Helm binaries +- Fix: Remove double copyright + +## 3.6.0-beta.2 - Fix: too narrow sidebar without clusters - Fix app crash when iterating Events without 'kind' property defined - Detect non-functional bundled kubectl diff --git a/tsconfig.json b/tsconfig.json index 95f8861c59..5a61e2ca3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "baseUrl": ".", - "outDir": "./out", "jsx": "react", "target": "ES2017", "module": "ESNext", diff --git a/types/mocks.d.ts b/types/mocks.d.ts index 7ddd25267b..9c8bf60047 100644 --- a/types/mocks.d.ts +++ b/types/mocks.d.ts @@ -3,6 +3,7 @@ declare module "mac-ca" declare module "win-ca" declare module "@hapi/call" declare module "@hapi/subtext" +declare module "@lens/extensions" // fixme: provide generated types from "extension-api.ts" // Global path to static assets declare const __static: string; diff --git a/webpack.dll.ts b/webpack.dll.ts deleted file mode 100755 index 000ba0af21..0000000000 --- a/webpack.dll.ts +++ /dev/null @@ -1,34 +0,0 @@ -import path from "path"; -import webpack, { LibraryTarget } from "webpack"; -import { isDevelopment, buildDir } from "./src/common/vars"; - -export const library = "dll" -export const libraryTarget: LibraryTarget = "commonjs2" -export const manifestPath = path.resolve(buildDir, `${library}.manifest.json`); - -export const packages = [ - "react", "react-dom", - "ace-builds", "xterm", - "moment", -]; - -export default function (): webpack.Configuration { - return { - context: path.dirname(manifestPath), - mode: isDevelopment ? "development" : "production", - cache: isDevelopment, - entry: { - [library]: packages, - }, - output: { - library, - libraryTarget, - }, - plugins: [ - new webpack.DllPlugin({ - name: library, - path: manifestPath, - }) - ], - } -} diff --git a/webpack.renderer.ts b/webpack.renderer.ts index b6c35535ce..1d60e2afc4 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -1,4 +1,4 @@ -import { appName, htmlTemplate, isDevelopment, isProduction, buildDir, rendererDir, sassCommonVars, publicPath } from "./src/common/vars"; +import { appName, buildDir, extensionsDir, extensionsLibName, htmlTemplate, isDevelopment, isProduction, publicPath, rendererDir, sassCommonVars } from "./src/common/vars"; import path from "path"; import webpack from "webpack"; import HtmlWebpackPlugin from "html-webpack-plugin"; @@ -6,12 +6,32 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin"; import TerserPlugin from "terser-webpack-plugin"; import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin" -export default function (): webpack.Configuration { - console.info('WEBPACK:renderer', require("./src/common/vars")) +export default [ + webpackLensRenderer, + webpackExtensionsApi, +] + +// todo: use common chunks/externals for "react", "react-dom", etc. +export function webpackExtensionsApi(): webpack.Configuration { + const config = webpackLensRenderer({ showVars: false }); + config.name = "extensions-api" + config.entry = { + [extensionsLibName]: path.resolve(extensionsDir, "extension-api.ts") + }; + config.output.libraryTarget = "commonjs2" + delete config.devtool; + return config; +} + +export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration { + if (showVars) { + console.info('WEBPACK:renderer', require("./src/common/vars")); + } return { context: __dirname, target: "electron-renderer", devtool: "source-map", // todo: optimize in dev-mode with webpack.SourceMapDevToolPlugin + name: "lens-app", mode: isProduction ? "production" : "development", cache: isDevelopment, entry: { @@ -23,6 +43,11 @@ export default function (): webpack.Configuration { filename: '[name].js', chunkFilename: 'chunks/[name].js', }, + stats: { + warningsFilter: [ + /Critical dependency: the request of a dependency is an expression/ + ] + }, resolve: { extensions: [ '.js', '.jsx', '.json', diff --git a/yarn.lock b/yarn.lock index 44fc03bfd8..8e84da93d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1966,6 +1966,11 @@ dependencies: "@types/node" "*" +"@types/module-alias@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/module-alias/-/module-alias-2.0.0.tgz#882668f8b8cdbda44812c3b592c590909e18849e" + integrity sha512-e3sW4oEH0qS1QxSfX7PT6xIi5qk/YSMsrB9Lq8EtkhQBZB+bKyfkP+jpLJRySanvBhAQPSv2PEBe81M8Iy/7yg== + "@types/node@*": version "14.0.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3" @@ -4086,13 +4091,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-env@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" - integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== - dependencies: - cross-spawn "^7.0.1" - cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4112,7 +4110,7 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -8181,6 +8179,11 @@ mock-fs@^4.12.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.12.0.tgz#a5d50b12d2d75e5bec9dac3b67ffe3c41d31ade4" integrity sha512-/P/HtrlvBxY4o/PzXY9cCNBrdylDNxg7gnrv2sMNxj+UJ2m8jSpl0/A6fuJeNAWr99ZvGWH8XCbE0vmnM5KupQ== +module-alias@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" + integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== + moment@^2.10.2, moment@^2.26.0: version "2.26.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"