mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00: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:
31 files changed
+564
-66
No files matched your search
+4
-2
@@ -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
|
||||
+16
-7
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Lens Example Extension
|
||||
|
||||
*TODO*: add more info
|
||||
@@ -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")}/>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "extension-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example extension",
|
||||
"main": "example-extension.ts",
|
||||
"lens": {
|
||||
"metadata": {}
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": ".",
|
||||
"module": "CommonJS",
|
||||
"sourceMap": false,
|
||||
"declaration": false
|
||||
},
|
||||
"include": [
|
||||
"../../../types",
|
||||
"./example-extension.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";
|
||||
@@ -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>()
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+9
-1
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
@@ -0,0 +1,4 @@
|
||||
.Extensions {
|
||||
$spacing: $padding * 2;
|
||||
padding: $spacing;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
+18
-1
@@ -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",
|
||||
|
||||
Vendored
+1
@@ -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,
|
||||
})
|
||||
],
|
||||
}
|
||||
}
|
||||
+28
-3
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user