1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into logs-search

This commit is contained in:
Alex Andreev 2020-11-05 11:14:07 +03:00
commit 4caddf5046
47 changed files with 537 additions and 241 deletions

View File

@ -43,6 +43,8 @@ jobs:
displayName: Build bundled extensions
- script: make integration-win
displayName: Run integration tests
- script: make test-extensions
displayName: Run In-tree Extension tests
- script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
displayName: Build
@ -86,6 +88,8 @@ jobs:
displayName: Run tests
- script: make integration-mac
displayName: Run integration tests
- script: make test-extensions
displayName: Run In-tree Extension tests
- script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
displayName: Build
@ -127,6 +131,8 @@ jobs:
condition: eq(variables.CACHE_RESTORED, 'true')
- script: make install-deps
displayName: Install dependencies
- script: make test-extensions
displayName: Run In-tree Extension tests
- script: make lint
displayName: Lint
- script: make build-npm

View File

@ -1,7 +1,7 @@
---
name: "Pull Request Labeler"
'on':
on:
- pull_request
jobs:
@ -9,6 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2
if: github.repository == 'lensapp/lens'
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler-config.yml

View File

@ -33,15 +33,15 @@ lint:
test: download-bins
yarn test
integration-linux:
integration-linux: build-extension-types build-extensions
yarn build:linux
yarn integration
integration-mac:
integration-mac: build-extension-types build-extensions
yarn build:mac
yarn integration
integration-win:
integration-win: build-extension-types build-extensions
yarn build:win
yarn integration
@ -58,10 +58,15 @@ endif
build-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
build-npm:
yarn compile:extension-types
test-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) test;)
build-npm: build-extension-types
yarn npm:fix-package-version
build-extension-types:
yarn compile:extension-types
publish-npm: build-npm
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
cd src/extensions/npm/extensions && npm publish --access=public

View File

@ -6,4 +6,4 @@ import appInfo from "../package.json"
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json")
packageInfo.version = appInfo.version
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2))
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n")

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -10,7 +10,8 @@
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
"dev": "npm run build --watch",
"test": "echo NO TESTS"
},
"dependencies": {
"react-open-doodles": "^1.0.5"

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -5,7 +5,8 @@
"main": "dist/main.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
"dev": "webpack --watch",
"test": "echo NO TESTS"
},
"dependencies": {},
"devDependencies": {

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -9,7 +9,8 @@
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
"dev": "npm run build --watch",
"test": "echo NO TESTS"
},
"dependencies": {
"semver": "^7.3.2"

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -9,7 +9,8 @@
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
"dev": "npm run build --watch",
"test": "echo NO TESTS"
},
"dependencies": {},
"devDependencies": {

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -9,7 +9,8 @@
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
"dev": "npm run build --watch",
"test": "echo NO TESTS"
},
"dependencies": {},
"devDependencies": {

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -26,12 +26,6 @@
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
"dev": true
},
"@types/node": {
"version": "14.11.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz",
"integrity": "sha512-UcaAZrL8uO5GNS+NLxkYg1RiOMgdLxCXGqs+TTupltXN8rTvUEKTOpqCV3tlcAIZJXzcBQajzmjdrvuPvnuMUw==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",

View File

@ -6,12 +6,12 @@
"renderer": "dist/renderer.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
"dev": "webpack --watch",
"test": "echo NO TESTS"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"@types/node": "^14.11.11",
"@types/react": "^16.9.53",
"@types/react-router": "^5.1.8",
"@types/webpack": "^4.41.17",

View File

@ -1,5 +1,8 @@
install-deps:
npm install
yarn install
build: install-deps
npm run build
yarn run build
test:
yarn run test

View File

@ -8,12 +8,6 @@
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@types/analytics-node": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/analytics-node/-/analytics-node-3.1.3.tgz",
"integrity": "sha512-Yk299LUqnyJ6fNYQkLFd0yTfUwIvgfxH3f5WEX3ib0PC5T+mZgqcOPMDhNZ4AOD/A9tXKJQeBIb6KvgzuXflaQ==",
"dev": true
},
"@segment/loosely-validate-event": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz",
@ -24,6 +18,12 @@
"join-component": "^1.1.0"
}
},
"@types/analytics-node": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/analytics-node/-/analytics-node-3.1.3.tgz",
"integrity": "sha512-Yk299LUqnyJ6fNYQkLFd0yTfUwIvgfxH3f5WEX3ib0PC5T+mZgqcOPMDhNZ4AOD/A9tXKJQeBIb6KvgzuXflaQ==",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

@ -10,7 +10,8 @@
},
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
"dev": "webpack --watch",
"test": "echo NO TESTS"
},
"dependencies": {},
"devDependencies": {

View File

@ -549,6 +549,14 @@ msgstr "Condition"
msgid "Conditions"
msgstr "Conditions"
#: src/renderer/components/+workloads-deployments/deployments.tsx: 118
msgid "Restart"
msgstr "Restart"
#: src/renderer/components/+workloads-deployments/deployments.tsx: 121
msgid "Are you sure you want to restart deployment <0>{0}</0>?"
msgstr "Are you sure you want to restart deployment <0>{0}</0>?"
#: src/renderer/components/+config-maps/config-maps.tsx:33
msgid "Config Maps"
msgstr "Config Maps"

View File

@ -545,6 +545,14 @@ msgstr ""
msgid "Conditions"
msgstr ""
#: src/renderer/components/+workloads-deployments/deployments.tsx: 118
msgid "Restart"
msgstr ""
#: src/renderer/components/+workloads-deployments/deployments.tsx: 121
msgid "Are you sure you want to restart deployment <0>{0}</0>?"
msgstr ""
#: src/renderer/components/+config-maps/config-maps.tsx:33
msgid "Config Maps"
msgstr ""

View File

@ -550,6 +550,14 @@ msgstr "Состояние"
msgid "Conditions"
msgstr "Состояния"
#: src/renderer/components/+workloads-deployments/deployments.tsx: 118
msgid "Restart"
msgstr "Перезагрузка"
#: src/renderer/components/+workloads-deployments/deployments.tsx: 121
msgid "Are you sure you want to restart deployment <0>{0}</0>?"
msgstr "Выполнить перезагрузку деплоймента <0>{0}</0>?"
#: src/renderer/components/+config-maps/config-maps.tsx:33
msgid "Config Maps"
msgstr ""

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",

View File

@ -2,7 +2,7 @@ import path from "path"
import Config from "conf"
import { Options as ConfOptions } from "conf/dist/source/types"
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron"
import { action, observable, reaction, runInAction, toJS, when } from "mobx";
import { action, IReactionOptions, observable, reaction, runInAction, toJS, when } from "mobx";
import Singleton from "./utils/singleton";
import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger";
@ -12,6 +12,7 @@ import isEqual from "lodash/isEqual";
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
autoLoad?: boolean;
syncEnabled?: boolean;
syncOptions?: IReactionOptions;
}
export class BaseStore<T = any> extends Singleton {
@ -20,7 +21,7 @@ export class BaseStore<T = any> extends Singleton {
whenLoaded = when(() => this.isLoaded);
@observable isLoaded = false;
@observable protected data: T;
@observable data = {} as T;
protected constructor(protected params: BaseStoreParams) {
super();
@ -36,8 +37,12 @@ export class BaseStore<T = any> extends Singleton {
return path.basename(this.storeConfig.path);
}
get path() {
return this.storeConfig.path;
}
get syncChannel() {
return `store-sync:${this.name}`
return `STORE-SYNC:${this.path}`
}
protected async init() {
@ -56,19 +61,19 @@ export class BaseStore<T = any> extends Singleton {
...confOptions,
projectName: "lens",
projectVersion: getAppVersion(),
cwd: this.storePath(),
cwd: this.cwd(),
});
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`);
logger.info(`[STORE]: LOADED from ${this.path}`);
this.fromStore(this.storeConfig.store);
this.isLoaded = true;
}
protected storePath() {
protected cwd() {
return (app || remote.app).getPath("userData")
}
protected async saveToFile(model: T) {
logger.info(`[STORE]: SAVING ${this.name}`);
logger.info(`[STORE]: SAVING ${this.path}`);
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
Object.entries(model).forEach(([key, value]) => {
this.storeConfig.set(key, value);
@ -77,7 +82,7 @@ export class BaseStore<T = any> extends Singleton {
enableSync() {
this.syncDisposers.push(
reaction(() => this.toJSON(), model => this.onModelChange(model)),
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
);
if (ipcMain) {
const callback = (event: IpcMainEvent, model: T) => {
@ -169,6 +174,7 @@ export class BaseStore<T = any> extends Singleton {
@action
protected fromStore(data: T) {
if (!data) return;
this.data = data;
}

View File

@ -1 +1,2 @@
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
export { ClusterFeature as Feature } from "../cluster-feature"
export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"

View File

@ -1,4 +1,6 @@
export { ExtensionStore } from "../extension-store"
export { clusterStore, ClusterModel } from "../../common/cluster-store"
export { Cluster } from "../../main/cluster"
export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store"
export { clusterStore } from "../../common/cluster-store"
export type { ClusterModel } from "../../common/cluster-store"
export { Cluster } from "../../main/cluster"
export { workspaceStore, Workspace } from "../../common/workspace-store"
export type { WorkspaceModel } from "../../common/workspace-store"

View File

@ -1,20 +1,13 @@
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"
import type { LensMainExtension } from "./lens-main-extension"
import type { LensRendererExtension } from "./lens-renderer-extension"
import type { InstalledExtension } from "./extension-manager";
import path from "path"
import { broadcastIpc } from "../common/ipc"
import { observable, reaction, toJS, } from "mobx"
import { computed, observable, reaction, when } from "mobx"
import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron"
import {
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
} from "./registries";
export interface InstalledExtension extends ExtensionModel {
manifestPath: string;
manifest: ExtensionManifest;
}
import * as registries from "./registries";
// lazy load so that we get correct userData
export function extensionPackagesRoot() {
@ -22,69 +15,82 @@ export function extensionPackagesRoot() {
}
export class ExtensionLoader {
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
@observable isLoaded = false;
protected extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false });
protected instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
constructor() {
if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
this.isLoaded = true;
extensions.forEach((ext) => {
if (!this.getById(ext.manifestPath)) {
if (!this.extensions.has(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext)
}
})
})
});
}
}
@computed get userExtensions(): LensExtension[] {
return [...this.instances.values()].filter(ext => !ext.isBundled)
}
async init() {
const { extensionManager } = await import("./extension-manager");
const installedExtensions = await extensionManager.load();
this.extensions.replace(installedExtensions);
this.isLoaded = true;
this.loadOnMain();
}
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions((extension: LensMainExtension) => {
extension.registerTo(menuRegistry, extension.appMenus)
})
this.autoInitExtensions((extension: LensMainExtension) => [
registries.menuRegistry.add(...extension.appMenus)
]);
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(globalPageRegistry, extension.globalPages)
extension.registerTo(appPreferenceRegistry, extension.appPreferences)
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures)
extension.registerTo(statusBarRegistry, extension.statusBarItems)
})
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.globalPageRegistry.add(...extension.globalPages),
registries.appPreferenceRegistry.add(...extension.appPreferences),
registries.clusterFeatureRegistry.add(...extension.clusterFeatures),
registries.statusBarRegistry.add(...extension.statusBarItems),
]);
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
extension.registerTo(kubeObjectDetailRegistry, extension.kubeObjectDetailItems)
})
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.clusterPageRegistry.add(...extension.clusterPages),
registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems),
]);
}
protected autoloadExtensions(callback: (instance: LensExtension) => void) {
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for(const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.id)
for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath)
if (!instance) {
const extensionModule = this.requireExtension(ext)
if (!extensionModule) {
continue
}
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
try {
instance.enable()
callback(instance)
} finally {
this.instances.set(ext.id, instance)
const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance));
this.instances.set(ext.manifestPath, instance);
} catch (err) {
logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err })
}
}
}
}, {
fireImmediately: true,
delay: 0,
})
}
@ -105,37 +111,17 @@ export class ExtensionLoader {
}
}
getById(id: ExtensionId): InstalledExtension {
return this.extensions.get(id);
}
async removeById(id: ExtensionId) {
const extension = this.getById(id);
if (extension) {
const instance = this.instances.get(extension.id)
if (instance) {
await instance.disable()
}
this.extensions.delete(id);
}
}
broadcastExtensions(frameId?: number) {
async broadcastExtensions(frameId?: number) {
await when(() => this.isLoaded);
broadcastIpc({
channel: "extensions:loaded",
frameId: frameId,
frameOnly: !!frameId,
args: [this.toJSON().extensions],
})
}
toJSON() {
return toJS({
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
}, {
recurseEverything: true,
args: [
Array.from(this.extensions.toJS().values())
],
})
}
}
export const extensionLoader = new ExtensionLoader()
export const extensionLoader = new ExtensionLoader();

View File

@ -1,12 +1,18 @@
import type { ExtensionManifest } from "./lens-extension"
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"
import path from "path"
import os from "os"
import fs from "fs-extra"
import child_process from "child_process";
import logger from "../main/logger"
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
import * as child_process from 'child_process';
import { extensionPackagesRoot } from "./extension-loader"
import { getBundledExtensions } from "../common/utils/app-version"
export interface InstalledExtension {
manifest: LensExtensionManifest;
manifestPath: string;
isBundled?: boolean; // defined in package.json
}
type Dependencies = {
[name: string]: string;
}
@ -51,7 +57,7 @@ export class ExtensionManager {
return path.join(this.extensionPackagesRoot, "package.json")
}
async load() {
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
@ -71,8 +77,8 @@ export class ExtensionManager {
return await this.loadExtensions();
}
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: ExtensionManifest;
protected async getByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: LensExtensionManifest;
try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
manifestJson = __non_webpack_require__(manifestPath)
@ -80,11 +86,8 @@ export class ExtensionManager {
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return {
id: manifestJson.name,
version: manifestJson.version,
name: manifestJson.name,
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson
manifest: manifestJson,
}
} catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
@ -109,10 +112,10 @@ export class ExtensionManager {
async loadExtensions() {
const bundledExtensions = await this.loadBundledExtensions()
const localExtensions = await this.loadFromFolder(this.localFolderPath)
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), {mode: 0o600})
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 })
await this.installPackages()
const extensions = bundledExtensions.concat(localExtensions)
return new Map(extensions.map(ext => [ext.id, ext]));
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
}
async loadBundledExtensions() {
@ -126,8 +129,9 @@ export class ExtensionManager {
}
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) {
ext.isBundled = true;
extensions.push(ext)
}
}
@ -152,7 +156,7 @@ export class ExtensionManager {
continue
}
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) {
extensions.push(ext)
}

View File

@ -15,7 +15,7 @@ export class ExtensionStore<T = any> extends BaseStore<T> {
await super.load()
}
protected storePath() {
return path.join(super.storePath(), "extension-store", this.extension.name)
protected cwd() {
return path.join(super.cwd(), "extension-store", this.extension.name)
}
}

View File

@ -1,75 +1,111 @@
import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx";
import type { InstalledExtension } from "./extension-manager";
import { action, reaction } from "mobx";
import logger from "../main/logger";
import { BaseRegistry } from "./registries/base-registry";
import { ExtensionStore } from "./extension-store";
export type ExtensionId = string | ExtensionPackageJsonPath;
export type ExtensionPackageJsonPath = string;
export type ExtensionVersion = string | number;
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
export interface ExtensionModel {
id: ExtensionId;
version: ExtensionVersion;
export interface LensExtensionManifest {
name: string;
manifestPath: string;
version: string;
description?: string;
enabled?: boolean;
updateUrl?: string;
main?: string; // path to %ext/dist/main.js
renderer?: string; // path to %ext/dist/renderer.js
}
export interface ExtensionManifest extends ExtensionModel {
main?: string;
renderer?: string;
description?: string; // todo: add more fields similar to package.json + some extra
export interface LensExtensionStoreModel {
isEnabled: boolean;
}
export class LensExtension implements ExtensionModel {
public id: ExtensionId;
public updateUrl: string;
protected disposers: (() => void)[] = [];
export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = any> {
protected store: S;
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
@observable name = "";
@observable description = "";
@observable version: ExtensionVersion = "0.0.0";
@observable manifest: ExtensionManifest;
@observable manifestPath: string;
@observable isEnabled = false;
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
this.manifest = manifest
this.manifestPath = manifestPath
this.isBundled = !!isBundled
this.init();
}
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
this.importModel(model, manifest);
protected async init(store: S = createBaseStore().getInstance()) {
this.store = store;
await this.store.loadExtension(this);
reaction(() => this.store.data.isEnabled, (isEnabled = true) => {
this.toggle(isEnabled); // handle activation & deactivation
}, {
fireImmediately: true
});
}
get isEnabled() {
return !!this.store.data.isEnabled;
}
get id(): LensExtensionId {
return this.manifestPath;
}
get name() {
return this.manifest.name
}
get version() {
return this.manifest.version
}
get description() {
return this.manifest.description
}
@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 migrate(appVersion: string) {
// mock
}
async enable() {
this.isEnabled = true;
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
if (this.isEnabled) return;
this.store.data.isEnabled = true;
this.onActivate();
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@action
async disable() {
if (!this.isEnabled) return;
this.store.data.isEnabled = false;
this.onDeactivate();
this.isEnabled = false;
this.disposers.forEach(cleanUp => cleanUp());
this.disposers.length = 0;
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
}
// todo: add more hooks
toggle(enable?: boolean) {
if (typeof enable === "boolean") {
enable ? this.enable() : this.disable()
} else {
this.isEnabled ? this.disable() : this.enable()
}
}
async whenEnabled(handlers: () => Function[]) {
const disposers: Function[] = [];
const unregisterHandlers = () => {
disposers.forEach(unregister => unregister())
disposers.length = 0;
}
const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
if (isEnabled) {
disposers.push(...handlers());
} else {
unregisterHandlers();
}
}, {
fireImmediately: true
})
return () => {
unregisterHandlers();
cancelReaction();
}
}
protected onActivate() {
// mock
}
@ -77,37 +113,14 @@ export class LensExtension implements ExtensionModel {
protected onDeactivate() {
// mock
}
}
registerTo<T = any>(registry: BaseRegistry<T>, items: T[] = []) {
const disposers = items.map(item => registry.add(item));
this.disposers.push(...disposers);
return () => {
this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer))
};
}
getMeta() {
return toJS({
id: this.id,
manifest: this.manifest,
manifestPath: this.manifestPath,
enabled: this.isEnabled
}, {
recurseEverything: true
})
}
toJSON(): ExtensionModel {
return toJS({
id: this.id,
name: this.name,
version: this.version,
description: this.description,
manifestPath: this.manifestPath,
enabled: this.isEnabled,
updateUrl: this.updateUrl,
}, {
recurseEverything: true,
})
function createBaseStore() {
return class extends ExtensionStore<LensExtensionStoreModel> {
constructor() {
super({
configName: "state"
});
}
}
}

View File

@ -1 +1,2 @@
api.d.ts
yarn.lock

View File

@ -12,5 +12,8 @@
"author": {
"name": "Mirantis, Inc.",
"email": "info@k8slens.dev"
},
"devDependencies": {
"@types/node": "^14.14.6"
}
}

View File

@ -1,5 +1,5 @@
// Base class for extensions-api registries
import { observable } from "mobx";
import { action, observable } from "mobx";
export class BaseRegistry<T = any> {
protected items = observable<T>([], { deep: false });
@ -8,10 +8,16 @@ export class BaseRegistry<T = any> {
return this.items.toJS();
}
add(item: T) {
this.items.push(item);
return () => {
@action
add(...items: T[]) {
this.items.push(...items);
return () => this.remove(...items);
}
@action
remove(...items: T[]) {
items.forEach(item => {
this.items.remove(item); // works because of {deep: false};
}
})
}
}

View File

@ -15,13 +15,12 @@ import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger"
import { clusterStore } from "../common/cluster-store"
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { appEventBus } from "../common/event-bus"
import { extensionManager } from "../extensions/extension-manager";
import { extensionLoader } from "../extensions/extension-loader";
import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
@ -48,7 +47,7 @@ app.on("ready", async () => {
registerFileProtocol("static", __static);
// preload isomorphic stores
// preload
await Promise.all([
userStore.load(),
clusterStore.load(),
@ -76,12 +75,8 @@ app.on("ready", async () => {
app.exit();
}
windowManager = new WindowManager(proxyPort);
LensExtensionsApi.windowManager = windowManager; // expose to extensions
extensionLoader.loadOnMain()
extensionLoader.extensions.replace(await extensionManager.load())
extensionLoader.broadcastExtensions()
LensExtensionsApi.windowManager = windowManager = new WindowManager(proxyPort);
extensionLoader.init(); // call after windowManager to see splash earlier
setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" })

View File

@ -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 { menuRegistry } from "../extensions/registries/menu-registry";
import logger from "./logger";
@ -70,6 +71,13 @@ export function buildMenu(windowManager: WindowManager) {
navigate(preferencesURL())
}
},
{
label: 'Extensions',
accelerator: 'CmdOrCtrl+Shift+E',
click() {
navigate(extensionsURL())
}
},
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },

View File

@ -1,3 +1,5 @@
import moment from "moment";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
@ -23,6 +25,25 @@ export class DeploymentApi extends KubeApi<Deployment> {
}
})
}
restart(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
template: {
metadata: {
annotations: {"kubectl.kubernetes.io/restartedAt" : moment.utc().format()}
}
}
}
}
},
{
headers: {
'content-type': 'application/strategic-merge-patch+json'
}
})
}
}
@autobind()
@ -38,6 +59,7 @@ export class Deployment extends WorkloadKubeObject {
metadata: {
creationTimestamp?: string;
labels: { [app: string]: string };
annotations?: { [app: string]: string };
};
spec: {
containers: {

View File

@ -64,7 +64,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
}
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
return this.request<T>(path, params, { ...reqInit, method: "patch" });
return this.request<T>(path, params, { ...reqInit, method: "PATCH" });
}
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {

View File

@ -0,0 +1,8 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const extensionsRoute: RouteProps = {
path: "/extensions"
}
export const extensionsURL = buildURL(extensionsRoute.path)

View File

@ -0,0 +1,35 @@
.Extensions {
--width: 100%;
--max-width: auto;
.extension {
--flex-gap: $padding / 3;
padding: $padding $padding * 2;
background: $colorVague;
border-radius: $radius;
}
.extensions-path {
word-break: break-all;
}
.WizardLayout {
padding: 0;
.info-col {
flex: 0.6;
align-self: flex-start;
}
}
.SearchInput {
margin-top: $margin / 2;
margin-bottom: $margin * 2;
max-width: none;
> label {
padding: $padding $padding * 2;
border-radius: $radius;
}
}
}

View File

@ -0,0 +1,112 @@
import "./extensions.scss";
import { shell } from "electron";
import React from "react";
import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Button } from "../button";
import { WizardLayout } from "../layout/wizard-layout";
import { Input } from "../input";
import { Icon } from "../icon";
import { PageLayout } from "../layout/page-layout";
import { extensionLoader } from "../../../extensions/extension-loader";
import { extensionManager } from "../../../extensions/extension-manager";
@observer
export class Extensions extends React.Component {
@observable search = ""
@computed get extensions() {
const searchText = this.search.toLowerCase();
return extensionLoader.userExtensions.filter(({ name, description }) => {
return [
name.toLowerCase().includes(searchText),
description.toLowerCase().includes(searchText),
].some(v => v)
})
}
get extensionsPath() {
return extensionManager.localFolderPath;
}
renderInfo() {
return (
<div className="flex column gaps">
<h2>Lens Extension API</h2>
<div>
The Extensions API in Lens allows users to customize and enhance the Lens experience by creating their own menus or page content that is extended from the existing pages. Many of the core
features of Lens are built as extensions and use the same Extension API.
</div>
<div>
Extensions loaded from:
<div className="extensions-path flex inline">
<code>{this.extensionsPath}</code>
<Icon
material="folder"
tooltip="Open folder"
onClick={() => shell.openPath(this.extensionsPath)}
/>
</div>
</div>
<div>
Check out documentation to <a href="https://docs.k8slens.dev/" target="_blank">learn more</a>
</div>
</div>
)
}
renderExtensions() {
const { extensions, extensionsPath, search } = this;
if (!extensions.length) {
return (
<div className="flex align-center box grow justify-center gaps">
{search && <Trans>No search results found</Trans>}
{!search && <p><Trans>There are no extensions in</Trans> <code>{extensionsPath}</code></p>}
</div>
)
}
return extensions.map(ext => {
const { id, name, description, isEnabled } = ext;
return (
<div key={id} className="extension flex gaps align-center">
<div className="box grow flex column gaps">
<div className="package">
Name: <code className="name">{name}</code>
</div>
<div>
Description: <span className="text-secondary">{description}</span>
</div>
</div>
{!isEnabled && (
<Button plain active onClick={() => ext.enable()}>Enable</Button>
)}
{isEnabled && (
<Button accent onClick={() => ext.disable()}>Disable</Button>
)}
</div>
)
})
}
render() {
return (
<PageLayout showOnTop className="Extensions" header={<h2>Extensions</h2>}>
<WizardLayout infoPanel={this.renderInfo()}>
<Input
autoFocus
theme="round-black"
className="SearchInput"
placeholder={_i18n._(t`Search extensions`)}
value={this.search}
onChange={(value) => this.search = value}
/>
<div className="extension-list flex column gaps">
{this.renderExtensions()}
</div>
</WizardLayout>
</PageLayout>
);
}
}

View File

@ -0,0 +1,2 @@
export * from "./extensions.route"
export * from "./extensions"

View File

@ -4,11 +4,12 @@ import React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { Deployment } from "../../api/endpoints";
import { Deployment, deploymentApi } from "../../api/endpoints";
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
import { ConfirmDialog } from "../confirm-dialog";
import { deploymentStore } from "./deployments.store";
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
import { podsStore } from "../+workloads-pods/pods.store";
@ -22,6 +23,8 @@ import kebabCase from "lodash/kebabCase";
import orderBy from "lodash/orderBy";
import { KubeEventIcon } from "../+events/kube-event-icon";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
import { apiManager } from "../../api/api-manager";
import { Notifications } from "../notifications";
enum sortBy {
name = "name",
@ -96,10 +99,34 @@ export class Deployments extends React.Component<Props> {
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
const { object, toolbar } = props;
return (
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
<span className="title"><Trans>Scale</Trans></span>
</MenuItem>
<>
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
<span className="title"><Trans>Scale</Trans></span>
</MenuItem>
<MenuItem onClick={() => ConfirmDialog.open({
ok: async () =>
{
try {
await deploymentApi.restart({
namespace: object.getNs(),
name: object.getName(),
})
} catch (err) {
Notifications.error(err);
}
},
labelOk: _i18n._(t`Restart`),
message: (
<p>
<Trans>Are you sure you want to restart deployment <b>{object.getName()}</b>?</Trans>
</p>
),
})}>
<Icon material="autorenew" title={_i18n._(t`Restart`)} interactive={toolbar}/>
<span className="title"><Trans>Restart</Trans></span>
</MenuItem>
</>
)
}
@ -110,4 +137,3 @@ kubeObjectMenuRegistry.add({
MenuItem: DeploymentMenu
}
})

View File

@ -16,6 +16,7 @@ import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
import { Extensions, extensionsRoute } from "../+extensions";
import { getMatchedClusterId } from "../../navigation";
@observer
@ -63,6 +64,7 @@ export class ClusterManager extends React.Component {
<Switch>
<Route component={LandingPage} {...landingRoute} />
<Route component={Preferences} {...preferencesRoute} />
<Route component={Extensions} {...extensionsRoute} />
<Route component={Workspaces} {...workspacesRoute} />
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />

View File

@ -1,5 +1,8 @@
.PageLayout {
$spacing: $padding * 2;
--width: 60%;
--max-width: 1000px;
--min-width: 570px;
position: relative;
height: 100%;
@ -26,12 +29,15 @@
> .content-wrapper {
@include custom-scrollbar-themed;
padding: $spacing * 2;
display: flex;
flex-direction: column;
> .content {
flex: 1;
margin: 0 auto;
width: 60%;
min-width: 570px;
max-width: 1000px;
width: var(--width);
min-width: var(--min-width);
max-width: var(--max-width);
}
}

View File

@ -167,7 +167,6 @@ export class Tooltip extends React.Component<TooltipProps> {
top = topCenter;
break;
case "top_right":
default:
left = targetBounds.right - tooltipBounds.width;
top = topCenter;
break;

View File

@ -2,7 +2,7 @@
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!
## 4.0.0-alpha.3 (current version)
## 4.0.0-alpha.4 (current version)
- Extension API
- Improved pod logs
@ -14,6 +14,8 @@ Here you can find description of changes we've built into each release. While we
- Status bar visual fixes
- Fix proxy upgrade socket timeouts
- Fix UI staleness after network issues
- Add +/- buttons in scale deployment popup screen
- Update chart details when selecting another chart
## 3.6.6
- Fix labels' word boundary to cover only drawer badges