mirror of
https://github.com/lensapp/lens.git
synced 2025-01-07 17:10:34 +03:00
Extensions loading (#795)
Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
parent
4250523fe4
commit
f1b03990ea
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
|
||||
|
23
package.json
23
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)$": "<rootDir>/__mocks__/styleMock.ts"
|
||||
}
|
||||
},
|
||||
"modulePathIgnorePatterns": ["<rootDir>/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",
|
||||
|
@ -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<ClusterStoreModel> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
3
src/extensions/example-extension/README.md
Normal file
3
src/extensions/example-extension/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Lens Example Extension
|
||||
|
||||
*TODO*: add more info
|
17
src/extensions/example-extension/example-extension.ts
Normal file
17
src/extensions/example-extension/example-extension.ts
Normal file
@ -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<any> {
|
||||
try {
|
||||
super.enable(runtime);
|
||||
runtime.logger.info('EXAMPLE EXTENSION: ENABLE() override');
|
||||
} catch (err){
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <Icon material="camera" onClick={() => console.log("done")}/>
|
11
src/extensions/example-extension/package.json
Normal file
11
src/extensions/example-extension/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "extension-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example extension",
|
||||
"main": "example-extension.ts",
|
||||
"lens": {
|
||||
"metadata": {}
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
13
src/extensions/example-extension/tsconfig.json
Normal file
13
src/extensions/example-extension/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": ".",
|
||||
"module": "CommonJS",
|
||||
"sourceMap": false,
|
||||
"declaration": false
|
||||
},
|
||||
"include": [
|
||||
"../../../types",
|
||||
"./example-extension.ts"
|
||||
]
|
||||
}
|
22
src/extensions/extension-api.ts
Normal file
22
src/extensions/extension-api.ts
Normal file
@ -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";
|
174
src/extensions/extension-store.ts
Normal file
174
src/extensions/extension-store.ts
Normal file
@ -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<ExtensionId, ExtensionModel>
|
||||
}
|
||||
|
||||
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<T extends ExtensionModel = any> {
|
||||
manifestPath: string;
|
||||
manifest: ExtensionManifest;
|
||||
extensionModule: {
|
||||
[name: string]: any;
|
||||
default: new (model: ExtensionModel, manifest?: ExtensionManifest) => LensExtension
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "lens-extension-store",
|
||||
syncEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
@observable version: ExtensionVersion = "0.0.0";
|
||||
@observable extensions = observable.map<ExtensionId, LensExtension>();
|
||||
@observable removed = observable.map<ExtensionId, LensExtension>();
|
||||
@observable installed = observable.map<ExtensionId, InstalledExtension>([], { 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<InstalledExtension[]> {
|
||||
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<ExtensionStore>()
|
89
src/extensions/extension.ts
Normal file
89
src/extensions/extension.ts
Normal file
@ -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<LensRuntimeRendererEnv> = {};
|
||||
|
||||
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<Partial<ExtensionModel>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
19
src/extensions/lens-runtime.ts
Normal file
19
src/extensions/lens-runtime.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
13
src/extensions/tsconfig.json
Normal file
13
src/extensions/tsconfig.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
@ -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' },
|
||||
|
@ -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(),
|
||||
]);
|
||||
|
11
src/renderer/components/+extensions/extensions.route.ts
Normal file
11
src/renderer/components/+extensions/extensions.route.ts
Normal file
@ -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<IExtensionsRouteParams>(extensionsRoute.path);
|
4
src/renderer/components/+extensions/extensions.scss
Normal file
4
src/renderer/components/+extensions/extensions.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.Extensions {
|
||||
$spacing: $padding * 2;
|
||||
padding: $spacing;
|
||||
}
|
31
src/renderer/components/+extensions/extensions.tsx
Normal file
31
src/renderer/components/+extensions/extensions.tsx
Normal file
@ -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 (
|
||||
<div className="info-panel flex gaps align-center">
|
||||
<Icon material="info"/>
|
||||
<p>Extensions available to install</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { installed: installedExtensions } = extensionStore;
|
||||
return (
|
||||
<WizardLayout className="Extensions" infoPanel={this.renderInfoPanel()}>
|
||||
<h2>Extensions</h2>
|
||||
<pre>
|
||||
{JSON.stringify(installedExtensions.toJSON(), null, 2)}
|
||||
</pre>
|
||||
</WizardLayout>
|
||||
);
|
||||
}
|
||||
}
|
2
src/renderer/components/+extensions/index.ts
Normal file
2
src/renderer/components/+extensions/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./extensions.route"
|
||||
export * from "./extensions"
|
@ -4,7 +4,6 @@
|
||||
position: relative;
|
||||
border-radius: $radius;
|
||||
padding: $radius;
|
||||
margin-bottom: $padding * 2;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
|
@ -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 {
|
||||
<Route component={AddCluster} {...addClusterRoute}/>
|
||||
<Route component={ClusterView} {...clusterViewRoute}/>
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
||||
<Route component={Extensions} {...extensionsRoute}/>
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
</main>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Props> {
|
||||
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
|
||||
)}
|
||||
</div>
|
||||
<div className="dynamic-pages">
|
||||
{Array.from(dynamicPages.all).map(([path, { MenuIcon }]) => {
|
||||
if (!MenuIcon) return;
|
||||
return <MenuIcon onClick={() => navigate(path)}/>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
28
src/renderer/components/cluster-manager/register-page.ts
Normal file
28
src/renderer/components/cluster-manager/register-page.ts
Normal file
@ -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<any>;
|
||||
MenuIcon: React.ComponentType<IconProps>;
|
||||
}
|
||||
|
||||
export class PagesStore {
|
||||
all = observable.map<string, PageComponents>();
|
||||
|
||||
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();
|
@ -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 (
|
||||
<I18nProvider i18n={_i18n}>
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./out",
|
||||
"jsx": "react",
|
||||
"target": "ES2017",
|
||||
"module": "ESNext",
|
||||
|
1
types/mocks.d.ts
vendored
1
types/mocks.d.ts
vendored
@ -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;
|
||||
|
@ -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,
|
||||
})
|
||||
],
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
19
yarn.lock
19
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"
|
||||
|
Loading…
Reference in New Issue
Block a user