mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into extension_install_1277
# Conflicts: # src/renderer/components/+extensions/extensions.tsx # src/renderer/utils/downloadFile.ts
This commit is contained in:
commit
e460b3f532
55 files changed
+1029
-266
No files matched your search
@@ -84,7 +84,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
super({
|
||||
configName: "lens-cluster-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
migrations: migrations,
|
||||
migrations,
|
||||
});
|
||||
|
||||
this.pushStateToViewsAutomatically();
|
||||
|
||||
@@ -36,7 +36,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
private constructor() {
|
||||
super({
|
||||
// configName: "lens-user-store", // todo: migrate from default "config.json"
|
||||
migrations: migrations,
|
||||
migrations,
|
||||
});
|
||||
|
||||
this.handleOnLoad();
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
import chokidar from "chokidar";
|
||||
import { EventEmitter } from "events";
|
||||
import fs from "fs-extra";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { getBundledExtensions } from "../common/utils/app-version";
|
||||
import logger from "../main/logger";
|
||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||
|
||||
export interface InstalledExtension {
|
||||
readonly manifest: LensExtensionManifest;
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean; // defined in project root's package.json
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
const logModule = "[EXTENSION-DISCOVERY]";
|
||||
const manifestFilename = "package.json";
|
||||
|
||||
/**
|
||||
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
|
||||
* @param lstat the stats to compare
|
||||
*/
|
||||
const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||
|
||||
/**
|
||||
* Discovers installed bundled and local extensions from the filesystem.
|
||||
* Also watches for added and removed local extensions by watching the directory.
|
||||
* Uses ExtensionInstaller to install dependencies for all of the extensions.
|
||||
* This is also done when a new extension is copied to the local extensions directory.
|
||||
* .init() must be called to start the directory watching.
|
||||
* The class emits events for added and removed extensions:
|
||||
* - "add": When extension is added. The event is of type InstalledExtension
|
||||
* - "remove": When extension is removed. The event is of type LensExtensionId
|
||||
*/
|
||||
export class ExtensionDiscovery {
|
||||
protected bundledFolderPath: string;
|
||||
|
||||
private loadStarted = false;
|
||||
|
||||
// This promise is resolved when .load() is finished.
|
||||
// This allows operations to be added after .load() success.
|
||||
private loaded: Promise<void>;
|
||||
|
||||
// These are called to either resolve or reject this.loaded promise
|
||||
private resolveLoaded: () => void;
|
||||
private rejectLoaded: (error: any) => void;
|
||||
|
||||
public events: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
this.loaded = new Promise((resolve, reject) => {
|
||||
this.resolveLoaded = resolve;
|
||||
this.rejectLoaded = reject;
|
||||
});
|
||||
|
||||
this.events = new EventEmitter();
|
||||
}
|
||||
|
||||
// Each extension is added as a single dependency to this object, which is written as package.json.
|
||||
// Each dependency key is the name of the dependency, and
|
||||
// each dependency value is the non-symlinked path to the dependency (folder).
|
||||
protected packagesJson: PackageJson = {
|
||||
dependencies: {}
|
||||
};
|
||||
|
||||
get localFolderPath(): string {
|
||||
return path.join(os.homedir(), ".k8slens", "extensions");
|
||||
}
|
||||
|
||||
get packageJsonPath() {
|
||||
return path.join(extensionInstaller.extensionPackagesRoot, manifestFilename);
|
||||
}
|
||||
|
||||
get inTreeTargetPath() {
|
||||
return path.join(extensionInstaller.extensionPackagesRoot, "extensions");
|
||||
}
|
||||
|
||||
get inTreeFolderPath(): string {
|
||||
return path.resolve(__static, "../extensions");
|
||||
}
|
||||
|
||||
get nodeModulesPath(): string {
|
||||
return path.join(extensionInstaller.extensionPackagesRoot, "node_modules");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the class and setups the file watcher for added/removed local extensions.
|
||||
*/
|
||||
init() {
|
||||
this.watchExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches for added/removed local extensions.
|
||||
* Dependencies are installed automatically after an extension folder is copied.
|
||||
*/
|
||||
async watchExtensions() {
|
||||
logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`);
|
||||
|
||||
// Wait until .load() has been called and has been resolved
|
||||
await this.loaded;
|
||||
|
||||
// chokidar works better than fs.watch
|
||||
chokidar.watch(this.localFolderPath, {
|
||||
// Dont watch recursively into subdirectories
|
||||
depth: 0,
|
||||
// Try to wait until the file has been completely copied.
|
||||
// The OS might emit an event for added file even it's not completely written to the filesysten.
|
||||
awaitWriteFinish: {
|
||||
// Wait 300ms until the file size doesn't change to consider the file written.
|
||||
// For a small file like package.json this should be plenty of time.
|
||||
stabilityThreshold: 300
|
||||
}
|
||||
})
|
||||
// Extension add is detected by watching "<extensionDir>package.json" add
|
||||
.on("add", this.handleWatchFileAdd)
|
||||
// Extension remove is detected by watching <extensionDir>" unlink
|
||||
.on("unlinkDir", this.handleWatchUnlinkDir);
|
||||
}
|
||||
|
||||
handleWatchFileAdd = async (filePath: string) => {
|
||||
if (path.basename(filePath) === manifestFilename) {
|
||||
try {
|
||||
const absPath = path.dirname(filePath);
|
||||
|
||||
// this.loadExtensionFromPath updates this.packagesJson
|
||||
const extension = await this.loadExtensionFromPath(absPath);
|
||||
|
||||
if (extension) {
|
||||
// Install dependencies for the new extension
|
||||
await this.installPackages();
|
||||
|
||||
logger.info(`${logModule} Added extension ${extension.manifest.name}`);
|
||||
this.events.emit("add", extension);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleWatchUnlinkDir = async (filePath: string) => {
|
||||
// filePath is the non-symlinked path to the extension folder
|
||||
// this.packagesJson.dependencies value is the non-symlinked path to the extension folder
|
||||
// LensExtensionId in extension-loader is the symlinked path to the extension folder manifest file
|
||||
|
||||
// Check that the removed path is directly under this.localFolderPath
|
||||
// Note that the watcher can create unlink events for subdirectories of the extension
|
||||
const extensionFolderName = path.basename(filePath);
|
||||
|
||||
if (path.relative(this.localFolderPath, filePath) === extensionFolderName) {
|
||||
const extensionName: string | undefined = Object
|
||||
.entries(this.packagesJson.dependencies)
|
||||
.find(([_name, extensionFolder]) => filePath === extensionFolder)?.[0];
|
||||
|
||||
if (extensionName !== undefined) {
|
||||
delete this.packagesJson.dependencies[extensionName];
|
||||
|
||||
// Reinstall dependencies to remove the extension from package.json
|
||||
await this.installPackages();
|
||||
|
||||
// The path to the manifest file is the lens extension id
|
||||
// Note that we need to use the symlinked path
|
||||
const lensExtensionId = path.join(this.nodeModulesPath, extensionName, "package.json");
|
||||
|
||||
logger.info(`${logModule} removed extension ${extensionName}`);
|
||||
this.events.emit("remove", lensExtensionId as LensExtensionId);
|
||||
} else {
|
||||
logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||
if (this.loadStarted) {
|
||||
// The class is simplified by only supporting .load() to be called once
|
||||
throw new Error("ExtensionDiscovery.load() can be only be called once");
|
||||
}
|
||||
|
||||
this.loadStarted = true;
|
||||
|
||||
try {
|
||||
logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`);
|
||||
|
||||
if (fs.existsSync(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"))) {
|
||||
await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
|
||||
this.bundledFolderPath = this.inTreeFolderPath;
|
||||
} catch {
|
||||
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
|
||||
await fs.remove(this.inTreeTargetPath);
|
||||
await fs.ensureDir(this.inTreeTargetPath);
|
||||
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath);
|
||||
this.bundledFolderPath = this.inTreeTargetPath;
|
||||
}
|
||||
|
||||
await fs.ensureDir(this.nodeModulesPath);
|
||||
await fs.ensureDir(this.localFolderPath);
|
||||
|
||||
const extensions = await this.loadExtensions();
|
||||
|
||||
// resolve the loaded promise
|
||||
this.resolveLoaded();
|
||||
|
||||
return extensions;
|
||||
} catch (error) {
|
||||
this.rejectLoaded(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false, isEnabled = isBundled }: {
|
||||
isBundled?: boolean;
|
||||
isEnabled?: boolean;
|
||||
} = {}): Promise<InstalledExtension | null> {
|
||||
let manifestJson: LensExtensionManifest;
|
||||
|
||||
try {
|
||||
// check manifest file for existence
|
||||
fs.accessSync(manifestPath, fs.constants.F_OK);
|
||||
|
||||
manifestJson = __non_webpack_require__(manifestPath);
|
||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
||||
|
||||
return {
|
||||
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
|
||||
manifest: manifestJson,
|
||||
isBundled,
|
||||
isEnabled,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson });
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||
const bundledExtensions = await this.loadBundledExtensions();
|
||||
const localExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||
await this.installPackages();
|
||||
const extensions = bundledExtensions.concat(localExtensions);
|
||||
|
||||
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write package.json to file system and install dependencies.
|
||||
*/
|
||||
installPackages() {
|
||||
return extensionInstaller.installPackages(this.packageJsonPath, this.packagesJson);
|
||||
}
|
||||
|
||||
async loadBundledExtensions() {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const folderPath = this.bundledFolderPath;
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
const paths = await fs.readdir(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
if (!bundledExtensions.includes(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const extension = await this.loadExtensionFromPath(absPath, { isBundled: true });
|
||||
|
||||
if (extension) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const paths = await fs.readdir(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
// do not allow to override bundled extensions
|
||||
if (bundledExtensions.includes(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
|
||||
if (!fs.existsSync(absPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lstat = await fs.lstat(absPath);
|
||||
|
||||
// skip non-directories
|
||||
if (!isDirectoryLike(lstat)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extension = await this.loadExtensionFromPath(absPath);
|
||||
|
||||
if (extension) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
||||
*/
|
||||
async loadExtensionFromPath(absPath: string, { isBundled = false, isEnabled = isBundled }: {
|
||||
isBundled?: boolean;
|
||||
isEnabled?: boolean;
|
||||
} = {}): Promise<InstalledExtension | null> {
|
||||
const manifestPath = path.resolve(absPath, manifestFilename);
|
||||
|
||||
return this.getByManifest(manifestPath, { isBundled, isEnabled });
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionDiscovery = new ExtensionDiscovery();
|
||||
@@ -0,0 +1,69 @@
|
||||
import AwaitLock from 'await-lock';
|
||||
import child_process from "child_process";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import logger from "../main/logger";
|
||||
import { extensionPackagesRoot } from "./extension-loader";
|
||||
|
||||
const logModule = "[EXTENSION-INSTALLER]";
|
||||
|
||||
type Dependencies = {
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
// Type for the package.json file that is written by ExtensionInstaller
|
||||
export type PackageJson = {
|
||||
dependencies: Dependencies;
|
||||
};
|
||||
|
||||
/**
|
||||
* Installs dependencies for extensions
|
||||
*/
|
||||
export class ExtensionInstaller {
|
||||
private installLock = new AwaitLock();
|
||||
|
||||
get extensionPackagesRoot() {
|
||||
return extensionPackagesRoot();
|
||||
}
|
||||
|
||||
get npmPath() {
|
||||
return __non_webpack_require__.resolve('npm/bin/npm-cli');
|
||||
}
|
||||
|
||||
installDependencies(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`);
|
||||
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
||||
cwd: extensionPackagesRoot(),
|
||||
silent: true
|
||||
});
|
||||
child.on("close", () => {
|
||||
resolve();
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write package.json to the file system and execute npm install for it.
|
||||
*/
|
||||
async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise<void> {
|
||||
// Mutual exclusion to install packages in sequence
|
||||
await this.installLock.acquireAsync();
|
||||
|
||||
try {
|
||||
// Write the package.json which will be installed in .installDependencies()
|
||||
await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), {
|
||||
mode: 0o600
|
||||
});
|
||||
|
||||
await this.installDependencies();
|
||||
} finally {
|
||||
this.installLock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionInstaller = new ExtensionInstaller();
|
||||
@@ -1,20 +1,25 @@
|
||||
import { app, ipcRenderer, remote } from "electron";
|
||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import path from "path";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import logger from "../main/logger";
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
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 { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import logger from "../main/logger";
|
||||
import { app, ipcRenderer, remote } from "electron";
|
||||
import * as registries from "./registries";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
|
||||
// lazy load so that we get correct userData
|
||||
export function extensionPackagesRoot() {
|
||||
return path.join((app || remote.app).getPath("userData"));
|
||||
}
|
||||
|
||||
const logModule = "[EXTENSIONS-LOADER]";
|
||||
|
||||
/**
|
||||
* Loads installed extensions to the Lens application
|
||||
*/
|
||||
export class ExtensionLoader {
|
||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||
@@ -47,6 +52,17 @@ export class ExtensionLoader {
|
||||
this.extensions.replace(extensions);
|
||||
}
|
||||
|
||||
addExtension(extension: InstalledExtension) {
|
||||
this.extensions.set(extension.manifestPath as LensExtensionId, extension);
|
||||
}
|
||||
|
||||
removeExtension(lensExtensionId: LensExtensionId) {
|
||||
// TODO: Remove the extension properly (from menus etc.)
|
||||
if (!this.extensions.delete(lensExtensionId)) {
|
||||
throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async initMain() {
|
||||
this.isLoaded = true;
|
||||
this.loadOnMain();
|
||||
@@ -77,14 +93,14 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
loadOnMain() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on main');
|
||||
logger.info(`${logModule}: load on main`);
|
||||
this.autoInitExtensions((ext: LensMainExtension) => [
|
||||
registries.menuRegistry.add(ext.appMenus)
|
||||
]);
|
||||
}
|
||||
|
||||
loadOnClusterManagerRenderer() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)');
|
||||
logger.info(`${logModule}: load on main renderer (cluster manager)`);
|
||||
this.autoInitExtensions((ext: LensRendererExtension) => [
|
||||
registries.globalPageRegistry.add(ext.globalPages, ext),
|
||||
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
|
||||
@@ -95,7 +111,7 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
loadOnClusterRenderer() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)');
|
||||
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
||||
this.autoInitExtensions((ext: LensRendererExtension) => [
|
||||
registries.clusterPageRegistry.add(ext.clusterPages, ext),
|
||||
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
|
||||
@@ -118,14 +134,15 @@ export class ExtensionLoader {
|
||||
instance.enable();
|
||||
this.instances.set(extId, instance);
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION-LOADER]: activation extension error`, { ext, err });
|
||||
logger.error(`${logModule}: activation extension error`, { ext, err });
|
||||
}
|
||||
} else if (!ext.isEnabled && instance) {
|
||||
logger.info(`${logModule} deleting extension ${extId}`);
|
||||
try {
|
||||
instance.disable();
|
||||
this.instances.delete(extId);
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION-LOADER]: deactivation extension error`, { ext, err });
|
||||
logger.error(`${logModule}: deactivation extension error`, { ext, err });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +163,7 @@ export class ExtensionLoader {
|
||||
return __non_webpack_require__(extEntrypoint).default;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
|
||||
console.error(`${logModule}: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
|
||||
console.trace(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
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 } from "./extension-loader";
|
||||
import { getBundledExtensions } from "../common/utils/app-version";
|
||||
|
||||
export interface InstalledExtension {
|
||||
readonly manifest: LensExtensionManifest;
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean; // defined in project root's package.json
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
type Dependencies = {
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
type PackageJson = {
|
||||
dependencies: Dependencies;
|
||||
};
|
||||
|
||||
export class ExtensionManager {
|
||||
|
||||
protected bundledFolderPath: string;
|
||||
|
||||
protected packagesJson: PackageJson = {
|
||||
dependencies: {}
|
||||
};
|
||||
|
||||
get extensionPackagesRoot() {
|
||||
return extensionPackagesRoot();
|
||||
}
|
||||
|
||||
get inTreeTargetPath() {
|
||||
return path.join(this.extensionPackagesRoot, "extensions");
|
||||
}
|
||||
|
||||
get inTreeFolderPath(): string {
|
||||
return path.resolve(__static, "../extensions");
|
||||
}
|
||||
|
||||
get nodeModulesPath(): string {
|
||||
return path.join(this.extensionPackagesRoot, "node_modules");
|
||||
}
|
||||
|
||||
get localFolderPath(): string {
|
||||
return path.join(os.homedir(), ".k8slens", "extensions");
|
||||
}
|
||||
|
||||
get npmPath() {
|
||||
return __non_webpack_require__.resolve('npm/bin/npm-cli');
|
||||
}
|
||||
|
||||
get packageJsonPath() {
|
||||
return path.join(this.extensionPackagesRoot, "package.json");
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
try {
|
||||
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
|
||||
this.bundledFolderPath = this.inTreeFolderPath;
|
||||
} catch {
|
||||
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
|
||||
await fs.remove(this.inTreeTargetPath);
|
||||
await fs.ensureDir(this.inTreeTargetPath);
|
||||
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath);
|
||||
this.bundledFolderPath = this.inTreeTargetPath;
|
||||
}
|
||||
await fs.ensureDir(this.nodeModulesPath);
|
||||
await fs.ensureDir(this.localFolderPath);
|
||||
return await this.loadExtensions();
|
||||
}
|
||||
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension> {
|
||||
let manifestJson: LensExtensionManifest;
|
||||
try {
|
||||
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
|
||||
manifestJson = __non_webpack_require__(manifestPath);
|
||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
||||
|
||||
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name);
|
||||
return {
|
||||
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
|
||||
manifest: manifestJson,
|
||||
isBundled: isBundled,
|
||||
isEnabled: isBundled,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
|
||||
}
|
||||
}
|
||||
|
||||
protected installPackages(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
||||
cwd: extensionPackagesRoot(),
|
||||
silent: true
|
||||
});
|
||||
child.on("close", () => {
|
||||
resolve();
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 this.installPackages();
|
||||
const extensions = bundledExtensions.concat(localExtensions);
|
||||
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
|
||||
}
|
||||
|
||||
async loadBundledExtensions() {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const folderPath = this.bundledFolderPath;
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
const paths = await fs.readdir(folderPath);
|
||||
for (const fileName of paths) {
|
||||
if (!bundledExtensions.includes(fileName)) {
|
||||
continue;
|
||||
}
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const manifestPath = path.resolve(absPath, "package.json");
|
||||
const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null);
|
||||
if (ext) {
|
||||
extensions.push(ext);
|
||||
}
|
||||
}
|
||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const paths = await fs.readdir(folderPath);
|
||||
for (const fileName of paths) {
|
||||
if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
|
||||
continue;
|
||||
}
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
continue;
|
||||
}
|
||||
const lstat = await fs.lstat(absPath);
|
||||
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories
|
||||
continue;
|
||||
}
|
||||
const manifestPath = path.resolve(absPath, "package.json");
|
||||
const ext = await this.getByManifest(manifestPath).catch(() => null);
|
||||
if (ext) {
|
||||
extensions.push(ext);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions;
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionManager = new ExtensionManager();
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InstalledExtension } from "./extension-manager";
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { action, observable, reaction } from "mobx";
|
||||
import logger from "../main/logger";
|
||||
|
||||
@@ -28,6 +28,7 @@ export class LensExtension {
|
||||
}
|
||||
|
||||
get id(): LensExtensionId {
|
||||
// This is the symlinked path under node_modules
|
||||
return this.manifestPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export class LensMainExtension extends LensExtension {
|
||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
pageId: pageId,
|
||||
pageId,
|
||||
params: params ?? {}, // compile to url with params
|
||||
});
|
||||
await windowManager.navigate(pageUrl, frameId);
|
||||
|
||||
@@ -19,7 +19,7 @@ export class LensRendererExtension extends LensExtension {
|
||||
const { navigate } = await import("../renderer/navigation");
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
pageId: pageId,
|
||||
pageId,
|
||||
params: params ?? {}, // compile to url with params
|
||||
});
|
||||
navigate(pageUrl);
|
||||
|
||||
@@ -13,7 +13,7 @@ export class ClusterIdDetector extends BaseClusterDetector {
|
||||
id = this.cluster.apiUrl;
|
||||
}
|
||||
const value = createHash("sha256").update(id).digest("hex");
|
||||
return { value: value, accuracy: 100 };
|
||||
return { value, accuracy: 100 };
|
||||
}
|
||||
|
||||
protected async getDefaultNamespaceId() {
|
||||
|
||||
@@ -91,7 +91,7 @@ export class ContextHandler {
|
||||
return {
|
||||
target: proxyUrl,
|
||||
changeOrigin: true,
|
||||
timeout: timeout,
|
||||
timeout,
|
||||
headers: {
|
||||
"Host": this.clusterUrl.hostname,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Installs Electron developer tools in the development build.
|
||||
* The dependency is not bundled to the production build.
|
||||
*/
|
||||
export const installDeveloperTools = async () => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import('electron-devtools-installer');
|
||||
|
||||
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export class HelmCli extends LensBinary {
|
||||
public constructor(baseDir: string, version: string) {
|
||||
const opts: LensBinaryOpts = {
|
||||
version,
|
||||
baseDir: baseDir,
|
||||
baseDir,
|
||||
originalBinaryName: "helm",
|
||||
newBinaryName: "helm3"
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ export class HelmReleaseManager {
|
||||
log: stdout,
|
||||
release: {
|
||||
name: releaseName,
|
||||
namespace: namespace
|
||||
namespace
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
|
||||
+20
-2
@@ -21,8 +21,10 @@ import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { extensionManager } from "../extensions/extension-manager";
|
||||
import { extensionsStore } from "../extensions/extensions-store";
|
||||
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
|
||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
import { installDeveloperTools } from "./developer-tools";
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
let proxyPort: number;
|
||||
@@ -49,6 +51,8 @@ app.on("ready", async () => {
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
await installDeveloperTools();
|
||||
|
||||
// preload
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
@@ -79,8 +83,22 @@ app.on("ready", async () => {
|
||||
}
|
||||
|
||||
extensionLoader.init();
|
||||
|
||||
extensionDiscovery.init();
|
||||
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
||||
extensionLoader.initExtensions(await extensionManager.load()); // call after windowManager to see splash earlier
|
||||
|
||||
// call after windowManager to see splash earlier
|
||||
const extensions = await extensionDiscovery.load();
|
||||
|
||||
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
||||
extensionDiscovery.events.on("add", (extension: InstalledExtension) => {
|
||||
extensionLoader.addExtension(extension);
|
||||
});
|
||||
extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||
extensionLoader.removeExtension(lensExtensionId);
|
||||
});
|
||||
|
||||
extensionLoader.initExtensions(extensions);
|
||||
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({ name: "service", action: "start" });
|
||||
|
||||
+4
-4
@@ -67,15 +67,15 @@ export class Router {
|
||||
output: "data",
|
||||
});
|
||||
return {
|
||||
cluster: cluster,
|
||||
cluster,
|
||||
path: url.pathname,
|
||||
raw: {
|
||||
req: req,
|
||||
req,
|
||||
},
|
||||
response: res,
|
||||
query: url.searchParams,
|
||||
payload: payload,
|
||||
params: params
|
||||
payload,
|
||||
params
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -78,16 +78,16 @@ class PortForwardRoute extends LensApi {
|
||||
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||
namespace: namespace, port: port
|
||||
namespace, port
|
||||
});
|
||||
if (!portForward) {
|
||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
||||
portForward = new PortForward({
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
namespace: namespace,
|
||||
namespace,
|
||||
name: resourceName,
|
||||
port: port,
|
||||
port,
|
||||
kubeConfig: cluster.getProxyKubeconfigPath()
|
||||
});
|
||||
const started = await portForward.start();
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ShellSession extends EventEmitter {
|
||||
this.shellProcess = pty.spawn(shell, args, {
|
||||
cols: 80,
|
||||
cwd: this.cwd() || env.HOME,
|
||||
env: env,
|
||||
env,
|
||||
name: "xterm-256color",
|
||||
rows: 30,
|
||||
});
|
||||
|
||||
@@ -124,7 +124,7 @@ export class WindowManager extends Singleton {
|
||||
await this.ensureMainWindow();
|
||||
this.sendToView({
|
||||
channel: "renderer:navigate",
|
||||
frameId: frameId,
|
||||
frameId,
|
||||
data: [url],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default migration({
|
||||
user["auth-provider"].config = authConfig;
|
||||
kubeConfig.users = [{
|
||||
name: userObj.name,
|
||||
user: user
|
||||
user
|
||||
}];
|
||||
cluster.kubeConfig = yaml.safeDump(kubeConfig);
|
||||
store.set(clusterKey, cluster);
|
||||
|
||||
@@ -8,7 +8,7 @@ export class ClusterApi extends KubeApi<Cluster> {
|
||||
|
||||
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
||||
const nodes = nodeNames.join("|");
|
||||
const opts = { category: "cluster", nodes: nodes };
|
||||
const opts = { category: "cluster", nodes };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
memoryUsage: opts,
|
||||
|
||||
@@ -20,7 +20,7 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
||||
data: {
|
||||
metadata: params,
|
||||
spec: {
|
||||
replicas: replicas
|
||||
replicas
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ export const helmReleasesApi = {
|
||||
const path = endpoint({ name, namespace }) + "/rollback";
|
||||
return apiBase.put(path, {
|
||||
data: {
|
||||
revision: revision
|
||||
revision
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
}
|
||||
const infoLog: JsonApiLog = {
|
||||
method: reqInit.method.toUpperCase(),
|
||||
reqUrl: reqUrl,
|
||||
reqInit: reqInit,
|
||||
reqUrl,
|
||||
reqInit,
|
||||
};
|
||||
return cancelableFetch(reqUrl, reqInit).then(res => {
|
||||
return this.parseResponse<D>(res, infoLog);
|
||||
|
||||
@@ -73,7 +73,7 @@ export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeC
|
||||
});
|
||||
return new KubeApi({
|
||||
objectConstructor: kubeClass,
|
||||
request: request
|
||||
request
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
resource: this.apiResource,
|
||||
namespace: this.isNamespaced ? namespace : undefined,
|
||||
name: name,
|
||||
name,
|
||||
});
|
||||
return resourcePath + (query ? `?` + stringify(this.normalizeQuery(query)) : "");
|
||||
}
|
||||
@@ -256,7 +256,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
this.setResourceVersion("", metadata.resourceVersion);
|
||||
return items.map(item => new KubeObjectConstructor({
|
||||
kind: this.kind,
|
||||
apiVersion: apiVersion,
|
||||
apiVersion,
|
||||
...item,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { cssNames } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { ExecValidationNotFoundError } from "../../../common/custom-errors";
|
||||
import { appEventBus } from "../../../common/event-bus";
|
||||
|
||||
enum KubeConfigSourceTab {
|
||||
FILE = "file",
|
||||
@@ -47,6 +48,7 @@ export class AddCluster extends React.Component {
|
||||
componentDidMount() {
|
||||
clusterStore.setActive(null);
|
||||
this.setKubeConfig(userStore.kubeConfigPath);
|
||||
appEventBus.emit({ name: "cluster-add", action: "start" });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -133,7 +135,7 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
this.error = "";
|
||||
this.isWaiting = true;
|
||||
|
||||
appEventBus.emit({ name: "cluster-add", action: "click" });
|
||||
newClusters = this.selectedContexts.filter(context => {
|
||||
try {
|
||||
const kubeConfig = this.kubeContexts.get(context);
|
||||
@@ -156,7 +158,7 @@ export class AddCluster extends React.Component {
|
||||
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
|
||||
return {
|
||||
id: clusterId,
|
||||
kubeConfigPath: kubeConfigPath,
|
||||
kubeConfigPath,
|
||||
workspace: workspaceStore.currentWorkspaceId,
|
||||
contextName: kubeConfig.currentContext,
|
||||
preferences: {
|
||||
|
||||
@@ -36,7 +36,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
const loadVersions = (repo: string) => {
|
||||
return helmChartsApi.get(repo, chartName).then(({ versions }) => {
|
||||
return versions.map(chart => ({
|
||||
repo: repo,
|
||||
repo,
|
||||
version: chart.getVersion()
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ClusterMetrics = observer(() => {
|
||||
id: metricType + metricNodeRole,
|
||||
label: metricType.toUpperCase() + " usage",
|
||||
borderColor: colors[metricType],
|
||||
data: data
|
||||
data
|
||||
}];
|
||||
const cpuOptions: ChartOptions = {
|
||||
scales: {
|
||||
|
||||
@@ -92,11 +92,11 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
const { name, namespace, type } = this;
|
||||
const { data = [], labels = [], annotations = [] } = this.secret[type];
|
||||
const secret: Partial<Secret> = {
|
||||
type: type,
|
||||
type,
|
||||
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
|
||||
metadata: {
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
name,
|
||||
namespace,
|
||||
annotations: this.getDataFromFields(annotations),
|
||||
labels: this.getDataFromFields(labels),
|
||||
} as IKubeObjectMetadata
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PageLayout } from "../layout/page-layout";
|
||||
import { Clipboard } from "../clipboard";
|
||||
import logger from "../../../main/logger";
|
||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||
import { extensionManager } from "../../../extensions/extension-manager";
|
||||
import { extensionDiscovery } from "../../../extensions/extension-discovery";
|
||||
import { LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
|
||||
import { Notifications } from "../notifications";
|
||||
import { downloadFile } from "../../../common/utils";
|
||||
@@ -57,7 +57,7 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
|
||||
get extensionsPath() {
|
||||
return extensionManager.localFolderPath;
|
||||
return extensionDiscovery.localFolderPath;
|
||||
}
|
||||
|
||||
getExtensionPackageTemp(fileName = "") {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
kind: "Namespace",
|
||||
apiVersion: "v1",
|
||||
metadata: {
|
||||
name: name,
|
||||
name,
|
||||
uid: "",
|
||||
resourceVersion: "",
|
||||
selfLink: `/api/v1/namespaces/${name}`
|
||||
|
||||
@@ -137,7 +137,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
else {
|
||||
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
|
||||
roleBinding = await roleBindingsStore.create({ name, namespace }, {
|
||||
subjects: subjects,
|
||||
subjects,
|
||||
roleRef: {
|
||||
name: selectedRole.getName(),
|
||||
kind: selectedRole.kind,
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {
|
||||
name: name,
|
||||
name,
|
||||
uid: null,
|
||||
selfLink: null,
|
||||
resourceVersion: null
|
||||
|
||||
@@ -59,7 +59,7 @@ export class App extends React.Component {
|
||||
name: "cluster",
|
||||
action: "open",
|
||||
params: {
|
||||
clusterId: clusterId
|
||||
clusterId
|
||||
}
|
||||
});
|
||||
window.addEventListener("online", () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class Checkbox extends React.PureComponent<CheckboxProps> {
|
||||
render() {
|
||||
const { label, inline, className, value, theme, children, ...inputProps } = this.props;
|
||||
const componentClass = cssNames('Checkbox flex', className, {
|
||||
inline: inline,
|
||||
inline,
|
||||
checked: value,
|
||||
disabled: this.props.disabled,
|
||||
["theme-" + theme]: theme,
|
||||
|
||||
@@ -55,7 +55,7 @@ export class EditResource extends React.Component<Props> {
|
||||
}
|
||||
editResourceStore.setData(this.tabId, {
|
||||
...this.tabData,
|
||||
draft: draft,
|
||||
draft,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export class FileInput extends React.Component<Props> {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
resolve({
|
||||
file: file,
|
||||
file,
|
||||
data: reader.result,
|
||||
error: reader.error ? String(reader.error) : null,
|
||||
});
|
||||
|
||||
@@ -151,7 +151,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
this.setState({
|
||||
validating: false,
|
||||
valid: !errors.length,
|
||||
errors: errors,
|
||||
errors,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -275,11 +275,11 @@ export class Input extends React.Component<InputProps, State> {
|
||||
|
||||
const className = cssNames("Input", this.props.className, {
|
||||
[`theme ${theme}`]: theme,
|
||||
focused: focused,
|
||||
disabled: disabled,
|
||||
focused,
|
||||
disabled,
|
||||
invalid: !valid,
|
||||
dirty: dirty,
|
||||
validating: validating,
|
||||
dirty,
|
||||
validating,
|
||||
validatingLine: validating && showValidationLine,
|
||||
});
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export class MenuActions extends React.Component<MenuActionsProps> {
|
||||
...menuProps
|
||||
} = this.props;
|
||||
const menuClassName = cssNames("MenuActions flex", className, {
|
||||
toolbar: toolbar,
|
||||
toolbar,
|
||||
gaps: toolbar, // add spacing for .flex
|
||||
});
|
||||
const autoClose = !toolbar;
|
||||
|
||||
@@ -15,7 +15,7 @@ export class Notifications extends React.Component {
|
||||
|
||||
static ok(message: NotificationMessage) {
|
||||
notificationsStore.add({
|
||||
message: message,
|
||||
message,
|
||||
timeout: 2500,
|
||||
status: NotificationStatus.OK
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export class Notifications extends React.Component {
|
||||
|
||||
static error(message: NotificationMessage) {
|
||||
notificationsStore.add({
|
||||
message: message,
|
||||
message,
|
||||
timeout: 10000,
|
||||
status: NotificationStatus.ERROR
|
||||
});
|
||||
@@ -33,7 +33,7 @@ export class Notifications extends React.Component {
|
||||
return notificationsStore.add({
|
||||
status: NotificationStatus.INFO,
|
||||
timeout: 0,
|
||||
message: message,
|
||||
message,
|
||||
...customOpts,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ export class RadioGroup extends React.Component<RadioGroupProps, {}> {
|
||||
<div className={className}>
|
||||
{radios.map(radio => {
|
||||
return React.cloneElement(radio, {
|
||||
name: name,
|
||||
name,
|
||||
disabled: disabled !== undefined ? disabled : radio.props.disabled,
|
||||
checked: radio.props.value === value,
|
||||
onChange: onChange
|
||||
onChange
|
||||
} as any);
|
||||
})}
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@ export class Radio extends React.Component<RadioProps> {
|
||||
render() {
|
||||
const { className, label, checked, children, ...inputProps } = this.props;
|
||||
const componentClass = cssNames('Radio flex align-center', className, {
|
||||
checked: checked,
|
||||
checked,
|
||||
disabled: this.props.disabled,
|
||||
});
|
||||
return (
|
||||
|
||||
@@ -60,7 +60,7 @@ export class TableCell extends React.Component<TableCellProps> {
|
||||
render() {
|
||||
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, ...cellProps } = this.props;
|
||||
const classNames = cssNames("TableCell", className, {
|
||||
checkbox: checkbox,
|
||||
checkbox,
|
||||
nowrap: _nowrap,
|
||||
sorting: this.isSortable,
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export class Table extends React.Component<TableProps> {
|
||||
typeof elem.props.children === "string" ? elem.props.children : undefined
|
||||
);
|
||||
return React.cloneElement(elem, {
|
||||
title: title,
|
||||
title,
|
||||
_sort: this.sort,
|
||||
_sorting: this.sortParams,
|
||||
_nowrap: headElem.props.nowrap,
|
||||
|
||||
@@ -32,10 +32,10 @@ export class Tabs extends React.PureComponent<TabsProps> {
|
||||
render() {
|
||||
const { center, wrap, onChange, value, autoFocus, scrollable = true, withBorder, ...elemProps } = this.props;
|
||||
const className = cssNames("Tabs", this.props.className, {
|
||||
center: center,
|
||||
wrap: wrap,
|
||||
scrollable: scrollable,
|
||||
withBorder: withBorder,
|
||||
center,
|
||||
wrap,
|
||||
scrollable,
|
||||
withBorder,
|
||||
});
|
||||
return (
|
||||
<TabsContext.Provider value={{ autoFocus, value, onChange }}>
|
||||
@@ -120,7 +120,7 @@ export class Tab extends React.PureComponent<TabProps> {
|
||||
let { className } = this.props;
|
||||
className = cssNames("Tab flex gaps align-center", className, {
|
||||
"active": this.isActive,
|
||||
"disabled": disabled,
|
||||
disabled,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -180,8 +180,8 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
break;
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
top: top,
|
||||
left,
|
||||
top,
|
||||
right: left + tooltipBounds.width,
|
||||
bottom: top + tooltipBounds.height,
|
||||
};
|
||||
|
||||
@@ -146,7 +146,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
|
||||
protected bindWatchEventsUpdater(delay = 1000) {
|
||||
return reaction(() => this.eventsBuffer.toJS()[0], this.updateFromEventsBuffer, {
|
||||
delay: delay
|
||||
delay
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export function getSelectedDetails() {
|
||||
export function getDetailsUrl(details: string) {
|
||||
if (!details) return "";
|
||||
return getQueryString({
|
||||
details: details,
|
||||
details,
|
||||
selected: getSelectedDetails(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ export function interval(timeSec = 1, callback: IntervalCallback, autoRun = fals
|
||||
let timer = -1;
|
||||
let isRunning = false;
|
||||
const intervalManager = {
|
||||
start: function (runImmediately = false) {
|
||||
start (runImmediately = false) {
|
||||
if (isRunning) return;
|
||||
const tick = () => callback(++count);
|
||||
isRunning = true;
|
||||
timer = window.setInterval(tick, 1000 * timeSec);
|
||||
if (runImmediately) tick();
|
||||
},
|
||||
stop: function () {
|
||||
stop () {
|
||||
count = 0;
|
||||
isRunning = false;
|
||||
clearInterval(timer);
|
||||
},
|
||||
restart: function (runImmediately = false) {
|
||||
restart (runImmediately = false) {
|
||||
this.stop();
|
||||
this.start(runImmediately);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user