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

Merge branch 'master' into tweak_extension_install_1277

This commit is contained in:
Jari Kolehmainen 2020-11-25 10:19:21 +02:00 committed by GitHub
commit f4d72e65dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 307 additions and 224 deletions

View File

@ -1,5 +1,11 @@
# Your First Extension # Your First Extension
We recommend to always use [Yeoman generator for Lens Extension](https://github.com/lensapp/generator-lens-ext) to start new extension project. [Read the generator guide here](../guides/generator.md).
If you want to setup the project manually, please continue reading.
## First Extension
In this topic, you'll learn the basics of building extensions by creating an extension that adds a "Hello World" page to a cluster menu. In this topic, you'll learn the basics of building extensions by creating an extension that adds a "Hello World" page to a cluster menu.
## Install the Extension ## Install the Extension

View File

@ -0,0 +1,65 @@
# New Extension Project with Generator
The [Lens Extension Generator](https://github.com/lensapp/generator-lens-ext) scaffolds a project ready for development. Install Yeoman and Lens Extension Generator with:
```bash
npm install -g yo generator-lens-ext
```
Run the generator and fill out a few fields for a TypeScript project:
```bash
yo lens-ext
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? my-first-lens-ext
# ? What's the description of your extension? My hello world extension
# ? What's your extension's publisher name? @my-org/my-first-lens-ext
# ? Initialize a git repository? Yes
# ? Install dependencies after initialization? Yes
# ? Which package manager to use? yarn
# ? symlink created extension folder to ~/.k8slens/extensions (mac/linux) or :User
s\<user>\.k8slens\extensions (windows)? Yes
```
Start webpack, which watches the `my-first-lens-ext` folder.
```bash
cd my-first-lens-ext
npm start # start the webpack server in watch mode
```
Then, open Lens, you should see a Hello World item in the menu:
![Hello World](images/hello-world.png)
## Developing the Extension
Try to change `my-first-lens-ext/renderer.tsx` to "Hello Lens!":
```tsx
clusterPageMenus = [
{
target: { pageId: "hello" },
title: "Hello Lens",
components: {
Icon: ExampleIcon,
}
}
]
```
Then, Reload Lens by CMD+R (Mac) / Ctrl+R (Linux/Windows), you should see the menu item text changes:
![Hello World](images/hello-lens.png)
## Debugging the Extension
[Testing](../testing-and-publishing/testing.md)
## Next steps
You can take a closer look at [Common Capabilities](../capabilities/common-capabilities.md) of extension, how to [style](../capabilities/styling.md) the extension. Or the [Extension Anatomy](anatomy.md).
You are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues) for Lens Extension Generator, if you find problems, or have feature requests.
The source code of the generator is hosted at [Github](https://github.com/lensapp/generator-lens-ext)

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,13 +1,16 @@
# Welcome to Lens support # Support
Here you will find different ways of getting support for Lens.
## Community Slack Channel Here you will find different ways of getting support for Lens IDE.
We have an active and growing community! Ask a question, see what's being discussed, get insights to up and coming features, help others, join the conversation on our community slack <a href="https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" target="_blank">here</a>
## Open Source Github Repository ## Community Support
Search feature requests, submit an idea, review existing issues, or open a new one at our Github repository <a href="https://github.com/lensapp/lens/issues" target="_blank">here</a>
## Enterprise Support * [Community Slack](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) - Request for support and help from the Lens community via Slack.
If you are interested in paid support options designed for enterprises to cover Lens usage at scale please see the following links: * [Github Issues](https://github.com/lensapp/lens/issues) - Submit your issues and feature requests to Lens IDE via Github.
- <a href="https://www.mirantis.com/support/enterprise-support-services" target="_blank">Mirantis</a> ## Commercial Support & Services
If you are interested in paid support options, professional services or training, please see the offerings from the following vendors:
* [Mirantis](https://www.mirantis.com/software/lens/) offers commercial support for officially released versions of Lens IDE on MacOS, Windows and Linux operating systems. In addition, Mirantis offers professional services to create proprietary / custom Lens IDE extensions and custom `msi` packaging to meet enterprise IT policies for software distribution and configuration. Training is also available.
If you'd like to get your business listed in here, please contact us via email [info@k8slens.dev](mailto:info@k8slens.dev)

View File

@ -10,9 +10,9 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
Description: () => { Description: () => {
return ( return (
<span> <span>
Enable timeseries data visualization (Prometheus stack) for your cluster. Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed. Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>. You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span> </span>
); );
} }

View File

@ -16,8 +16,8 @@ jest.setTimeout(60000);
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
describe("Lens integration tests", () => { describe("Lens integration tests", () => {
const TEST_NAMESPACE = "integration-tests"; const TEST_NAMESPACE = "integration-tests";
const BACKSPACE = "\uE003"; const BACKSPACE = "\uE003";
let app: Application; let app: Application;
const appStart = async () => { const appStart = async () => {
@ -37,23 +37,33 @@ describe("Lens integration tests", () => {
const minikubeReady = (): boolean => { const minikubeReady = (): boolean => {
// determine if minikube is running // determine if minikube is running
let status = spawnSync("minikube status", { shell: true }); {
if (status.status !== 0) { const { status } = spawnSync("minikube status", { shell: true });
console.warn("minikube not running"); if (status !== 0) {
return false; console.warn("minikube not running");
return false;
}
} }
// Remove TEST_NAMESPACE if it already exists // Remove TEST_NAMESPACE if it already exists
status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); {
if (status.status === 0) { const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); if (status === 0) {
status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }); console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
if (status.status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`); const { status, stdout, stderr } = spawnSync(
return false; `minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
{ shell: true },
);
if (status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
return false;
}
console.log(stdout.toString());
} }
console.log(status.stdout.toString());
} }
return true; return true;
}; };
const ready = minikubeReady(); const ready = minikubeReady();
@ -62,8 +72,8 @@ describe("Lens integration tests", () => {
beforeAll(appStart, 20000); beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app?.isRunning()) {
return util.tearDown(app); await util.tearDown(app);
} }
}); });

View File

@ -8,9 +8,8 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
export function setup(): Application { export function setup(): Application {
return new Application({ return new Application({
// path to electron app path: AppPaths[process.platform], // path to electron app
args: [], args: [],
path: AppPaths[process.platform],
startTimeout: 30000, startTimeout: 30000,
waitTimeout: 60000, waitTimeout: 60000,
env: { env: {
@ -19,9 +18,10 @@ export function setup(): Application {
}); });
} }
type AsyncPidGetter = () => Promise<number>;
export async function tearDown(app: Application) { export async function tearDown(app: Application) {
const mpid: any = app.mainProcess.pid; const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
const pid = await mpid();
await app.stop(); await app.stop();
try { try {
process.kill(pid, "SIGKILL"); process.kill(pid, "SIGKILL");

View File

@ -32,6 +32,7 @@ nav:
- Overview: extensions/guides/README.md - Overview: extensions/guides/README.md
- Main Extension: extensions/guides/main-extension.md - Main Extension: extensions/guides/main-extension.md
- Renderer Extension: extensions/guides/renderer-extension.md - Renderer Extension: extensions/guides/renderer-extension.md
- Generator: extensions/guides/generator.md
- Testing and Publishing: - Testing and Publishing:
- Testing Extensions: extensions/testing-and-publishing/testing.md - Testing Extensions: extensions/testing-and-publishing/testing.md
- Publishing Extensions: extensions/testing-and-publishing/publishing.md - Publishing Extensions: extensions/testing-and-publishing/publishing.md

View File

@ -6,8 +6,8 @@ import { defineGlobal } from "./utils/defineGlobal";
export const isMac = process.platform === "darwin"; export const isMac = process.platform === "darwin";
export const isWindows = process.platform === "win32"; export const isWindows = process.platform === "win32";
export const isLinux = process.platform === "linux"; export const isLinux = process.platform === "linux";
export const isDebugging = process.env.DEBUG === "true"; export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
export const isSnap = !!process.env["SNAP"]; export const isSnap = !!process.env.SNAP;
export const isProduction = process.env.NODE_ENV === "production"; export const isProduction = process.env.NODE_ENV === "production";
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const isDevelopment = !isTestEnv && !isProduction; export const isDevelopment = !isTestEnv && !isProduction;

View File

@ -6,6 +6,7 @@ import path from "path";
import { getBundledExtensions } from "../common/utils/app-version"; import { getBundledExtensions } from "../common/utils/app-version";
import logger from "../main/logger"; import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer"; import { extensionInstaller, PackageJson } from "./extension-installer";
import { extensionsStore } from "./extensions-store";
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"; import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
export interface InstalledExtension { export interface InstalledExtension {
@ -213,24 +214,26 @@ export class ExtensionDiscovery {
} }
} }
protected async getByManifest(manifestPath: string, { isBundled = false, isEnabled = isBundled }: { protected async getByManifest(manifestPath: string, { isBundled = false }: {
isBundled?: boolean; isBundled?: boolean;
isEnabled?: boolean;
} = {}): Promise<InstalledExtension | null> { } = {}): Promise<InstalledExtension | null> {
let manifestJson: LensExtensionManifest; let manifestJson: LensExtensionManifest;
let isEnabled: boolean;
try { try {
// check manifest file for existence // check manifest file for existence
fs.accessSync(manifestPath, fs.constants.F_OK); fs.accessSync(manifestPath, fs.constants.F_OK);
manifestJson = __non_webpack_require__(manifestPath); manifestJson = __non_webpack_require__(manifestPath);
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
return { return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), manifestPath: installedManifestPath,
manifest: manifestJson, manifest: manifestJson,
isBundled, isBundled,
isEnabled, isEnabled
}; };
} catch (error) { } catch (error) {
logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson }); logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson });
@ -316,13 +319,12 @@ export class ExtensionDiscovery {
/** /**
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension. * Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
*/ */
async loadExtensionFromPath(absPath: string, { isBundled = false, isEnabled = isBundled }: { async loadExtensionFromPath(absPath: string, { isBundled = false }: {
isBundled?: boolean; isBundled?: boolean;
isEnabled?: boolean;
} = {}): Promise<InstalledExtension | null> { } = {}): Promise<InstalledExtension | null> {
const manifestPath = path.resolve(absPath, manifestFilename); const manifestPath = path.resolve(absPath, manifestFilename);
return this.getByManifest(manifestPath, { isBundled, isEnabled }); return this.getByManifest(manifestPath, { isBundled });
} }
} }

View File

@ -1,6 +1,7 @@
import { app, ipcRenderer, remote } from "electron"; import { app, ipcRenderer, remote } from "electron";
import { action, computed, observable, reaction, toJS, when } from "mobx"; import { action, computed, observable, reaction, toJS, when } from "mobx";
import path from "path"; import path from "path";
import { getHostedCluster } from "../common/cluster-store";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import logger from "../main/logger"; import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery"; import type { InstalledExtension } from "./extension-discovery";
@ -94,14 +95,14 @@ export class ExtensionLoader {
loadOnMain() { loadOnMain() {
logger.info(`${logModule}: load on main`); logger.info(`${logModule}: load on main`);
this.autoInitExtensions((ext: LensMainExtension) => [ this.autoInitExtensions(async (ext: LensMainExtension) => [
registries.menuRegistry.add(ext.appMenus) registries.menuRegistry.add(ext.appMenus)
]); ]);
} }
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.info(`${logModule}: load on main renderer (cluster manager)`); logger.info(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions((ext: LensRendererExtension) => [ this.autoInitExtensions(async (ext: LensRendererExtension) => [
registries.globalPageRegistry.add(ext.globalPages, ext), registries.globalPageRegistry.add(ext.globalPages, ext),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext), registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
registries.appPreferenceRegistry.add(ext.appPreferences), registries.appPreferenceRegistry.add(ext.appPreferences),
@ -112,33 +113,43 @@ export class ExtensionLoader {
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info(`${logModule}: load on cluster renderer (dashboard)`); logger.info(`${logModule}: load on cluster renderer (dashboard)`);
this.autoInitExtensions((ext: LensRendererExtension) => [ const cluster = getHostedCluster();
registries.clusterPageRegistry.add(ext.clusterPages, ext), this.autoInitExtensions(async (ext: LensRendererExtension) => {
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext), if (await ext.isEnabledForCluster(cluster) === false) {
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems), return [];
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems), }
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts) return [
]); registries.clusterPageRegistry.add(ext.clusterPages, ext),
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
];
});
} }
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) { protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
return reaction(() => this.toJSON(), installedExtensions => { return reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, ext] of installedExtensions) { for (const [extId, ext] of installedExtensions) {
let instance = this.instances.get(extId); const alreadyInit = this.instances.has(extId);
if (ext.isEnabled && !instance) {
if (ext.isEnabled && !alreadyInit) {
try { try {
const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext); const LensExtensionClass = this.requireExtension(ext);
if (!LensExtensionClass) continue; if (!LensExtensionClass) {
instance = new LensExtensionClass(ext); continue;
}
const instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance)); instance.whenEnabled(() => register(instance));
instance.enable(); instance.enable();
this.instances.set(extId, instance); this.instances.set(extId, instance);
} catch (err) { } catch (err) {
logger.error(`${logModule}: activation extension error`, { ext, err }); logger.error(`${logModule}: activation extension error`, { ext, err });
} }
} else if (!ext.isEnabled && instance) { } else if (!ext.isEnabled && alreadyInit) {
logger.info(`${logModule} deleting extension ${extId}`);
try { try {
const instance = this.instances.get(extId);
instance.disable(); instance.disable();
this.instances.delete(extId); this.instances.delete(extId);
} catch (err) { } catch (err) {
@ -151,7 +162,7 @@ export class ExtensionLoader {
}); });
} }
protected requireExtension(extension: InstalledExtension) { protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
let extEntrypoint = ""; let extEntrypoint = "";
try { try {
if (ipcRenderer && extension.manifest.renderer) { if (ipcRenderer && extension.manifest.renderer) {

View File

@ -47,11 +47,6 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
await extensionLoader.whenLoaded; await extensionLoader.whenLoaded;
await this.whenLoaded; await this.whenLoaded;
// activate user-extensions when state is ready
extensionLoader.userExtensions.forEach((ext, extId) => {
ext.isEnabled = this.isEnabled(extId);
});
// apply state on changes from store // apply state on changes from store
reaction(() => this.state.toJS(), extensionsState => { reaction(() => this.state.toJS(), extensionsState => {
extensionsState.forEach((state, extId) => { extensionsState.forEach((state, extId) => {
@ -70,7 +65,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
isEnabled(extId: LensExtensionId) { isEnabled(extId: LensExtensionId) {
const state = this.state.get(extId); const state = this.state.get(extId);
return !state /* enabled by default */ || state.enabled; return state && state.enabled; // by default false
} }
@action @action

View File

@ -1,5 +1,6 @@
import type { InstalledExtension } from "./extension-discovery"; import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx"; import { action, observable, reaction } from "mobx";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionId = string; // path to manifest (package.json)
@ -40,6 +41,17 @@ export class LensExtension {
return this.manifest.version; return this.manifest.version;
} }
/**
* getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension.
*
* Note: there is no security done on this folder, only obfiscation of the
* folder name.
*/
async getExtensionFileFolder(): Promise<string> {
return filesystemProvisionerStore.requestDirectory(this.id);
}
get description() { get description() {
return this.manifest.description; return this.manifest.description;
} }
@ -68,15 +80,16 @@ export class LensExtension {
} }
} }
async whenEnabled(handlers: () => Function[]) { async whenEnabled(handlers: () => Promise<Function[]>) {
const disposers: Function[] = []; const disposers: Function[] = [];
const unregisterHandlers = () => { const unregisterHandlers = () => {
disposers.forEach(unregister => unregister()); disposers.forEach(unregister => unregister());
disposers.length = 0; disposers.length = 0;
}; };
const cancelReaction = reaction(() => this.isEnabled, isEnabled => { const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) { if (isEnabled) {
disposers.push(...handlers()); const handlerDisposers = await handlers();
disposers.push(...handlerDisposers);
} else { } else {
unregisterHandlers(); unregisterHandlers();
} }

View File

@ -5,7 +5,7 @@ import { WindowManager } from "../main/window-manager";
import { getExtensionPageUrl } from "./registries/page-registry"; import { getExtensionPageUrl } from "./registries/page-registry";
export class LensMainExtension extends LensExtension { export class LensMainExtension extends LensExtension {
@observable.shallow appMenus: MenuRegistration[] = []; appMenus: MenuRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) { async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
const windowManager = WindowManager.getInstance<WindowManager>(); const windowManager = WindowManager.getInstance<WindowManager>();

View File

@ -1,19 +1,21 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"; import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
import type { Cluster } from "../main/cluster";
import { observable } from "mobx"; import { observable } from "mobx";
import { LensExtension } from "./lens-extension"; import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry"; import { getExtensionPageUrl } from "./registries/page-registry";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
@observable.shallow globalPages: PageRegistration[] = []; globalPages: PageRegistration[] = [];
@observable.shallow clusterPages: PageRegistration[] = []; clusterPages: PageRegistration[] = [];
@observable.shallow globalPageMenus: PageMenuRegistration[] = []; globalPageMenus: PageMenuRegistration[] = [];
@observable.shallow clusterPageMenus: PageMenuRegistration[] = []; clusterPageMenus: PageMenuRegistration[] = [];
@observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
@observable.shallow appPreferences: AppPreferenceRegistration[] = []; appPreferences: AppPreferenceRegistration[] = [];
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []; clusterFeatures: ClusterFeatureRegistration[] = [];
@observable.shallow statusBarItems: StatusBarRegistration[] = []; statusBarItems: StatusBarRegistration[] = [];
@observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = []; kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []; kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P) { async navigate<P extends object>(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation"); const { navigate } = await import("../renderer/navigation");
@ -24,4 +26,11 @@ export class LensRendererExtension extends LensExtension {
}); });
navigate(pageUrl); navigate(pageUrl);
} }
/**
* Defines if extension is enabled for a given cluster. Defaults to `true`.
*/
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
return true;
}
} }

View File

@ -0,0 +1,57 @@
import { randomBytes } from "crypto";
import { SHA256 } from "crypto-js";
import { app } from "electron";
import fse from "fs-extra";
import { action, observable, toJS } from "mobx";
import path from "path";
import { BaseStore } from "../common/base-store";
import { LensExtensionId } from "../extensions/lens-extension";
interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths
}
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
@observable registeredExtensions = observable.map<LensExtensionId, string>();
private constructor() {
super({
configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
});
}
/**
* This function retrieves the saved path to the folder which the extension
* can saves files to. If the folder is not present then it is created.
* @param extensionName the name of the extension requesting the path
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
if (!this.registeredExtensions.has(extensionName)) {
const salt = randomBytes(32).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
this.registeredExtensions.set(extensionName, dirPath);
}
const dirPath = this.registeredExtensions.get(extensionName);
await fse.ensureDir(dirPath);
return dirPath;
}
@action
protected fromStore({ extensions }: FSProvisionModel = { extensions: {} }): void {
this.registeredExtensions.merge(extensions);
}
toJSON(): FSProvisionModel {
return toJS({
extensions: this.registeredExtensions.toJSON(),
}, {
recurseEverything: true
});
}
}
export const filesystemProvisionerStore = FilesystemProvisionerStore.getInstance<FilesystemProvisionerStore>();

View File

@ -25,6 +25,7 @@ import { extensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery"; import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number; let proxyPort: number;
@ -59,6 +60,7 @@ app.on("ready", async () => {
clusterStore.load(), clusterStore.load(),
workspaceStore.load(), workspaceStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(),
]); ]);
// find free port // find free port

View File

@ -15,6 +15,7 @@ import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import { extensionsStore } from "../extensions/extensions-store"; import { extensionsStore } from "../extensions/extensions-store";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
@ -39,6 +40,7 @@ export async function bootstrap(App: AppComponent) {
workspaceStore.load(), workspaceStore.load(),
clusterStore.load(), clusterStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(),
i18nStore.init(), i18nStore.init(),
themeStore.init(), themeStore.init(),
]); ]);

View File

@ -32,7 +32,6 @@
border: 1px solid var(--drawerSubtitleBackground); border: 1px solid var(--drawerSubtitleBackground);
border-radius: $radius; border-radius: $radius;
overflow: auto; overflow: auto;
@include custom-scrollbar();
.TableHead { .TableHead {
border-bottom: none; border-bottom: none;

View File

@ -6,7 +6,6 @@
.Badge { .Badge {
display: flex; display: flex;
margin: 0;
margin-bottom: 1px; margin-bottom: 1px;
padding: $padding $spacing; padding: $padding $spacing;
} }

View File

@ -137,7 +137,7 @@ export class Preferences extends React.Component {
formatOptionLabel={this.formatHelmOptionLabel} formatOptionLabel={this.formatHelmOptionLabel}
controlShouldRenderValue={false} controlShouldRenderValue={false}
/> />
<div className="repos flex gaps column"> <div className="repos flex column">
{Array.from(this.helmAddedRepos).map(([name, repo]) => { {Array.from(this.helmAddedRepos).map(([name, repo]) => {
const tooltipId = `message-${name}`; const tooltipId = `message-${name}`;
return ( return (

View File

@ -26,7 +26,7 @@
} }
> .content { > .content {
@include custom-scrollbar; overflow: auto;
margin-top: $spacing; margin-top: $spacing;
padding: $spacing * 2; padding: $spacing * 2;

View File

@ -7,10 +7,6 @@
.theme-light & { .theme-light & {
border: 1px solid gainsboro; border: 1px solid gainsboro;
.ace_scrollbar {
@include custom-scrollbar(dark);
}
} }
> .editor { > .editor {
@ -51,8 +47,4 @@
.ace_comment { .ace_comment {
color: #808080; color: #808080;
} }
.ace_scrollbar {
@include custom-scrollbar;
}
} }

View File

@ -1,15 +1,6 @@
@import "~flex.box"; @import "~flex.box";
@import "fonts"; @import "fonts";
*, *:before, *:after {
box-sizing: border-box;
padding: 0;
margin: 0;
border: 0;
outline: none;
-webkit-font-smoothing: antialiased;
}
:root { :root {
--unit: 8px; --unit: 8px;
--padding: var(--unit); --padding: var(--unit);
@ -27,6 +18,33 @@
--drag-region-height: 22px --drag-region-height: 22px
} }
*, *:before, *:after {
box-sizing: border-box;
padding: 0;
margin: 0;
border: 0;
outline: none;
-webkit-font-smoothing: antialiased;
}
::-webkit-scrollbar {
width: 16px;
height: 15px; // Align sizes visually
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--scrollBarColor);
background-clip: padding-box;
border: 4px solid transparent;
border-right-width: 5px;
border-radius: 16px;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
::selection { ::selection {
background: $primary; background: $primary;
color: white; color: white;

View File

@ -1,7 +1,5 @@
.Dialog { .Dialog {
@include custom-scrollbar;
position: fixed; position: fixed;
overflow: auto; overflow: auto;
left: 0; left: 0;
@ -11,6 +9,7 @@
padding: $unit * 5; padding: $unit * 5;
z-index: $zIndex-dialog; z-index: $zIndex-dialog;
overscroll-behavior: none; // prevent swiping with touch-pad on MacOSX overscroll-behavior: none; // prevent swiping with touch-pad on MacOSX
overflow: auto;
&.modal { &.modal {
background: transparentize(#222, .5); background: transparentize(#222, .5);

View File

@ -3,12 +3,7 @@
--overlay-active-bg: orange; --overlay-active-bg: orange;
.logs { .logs {
@include custom-scrollbar;
// fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight`
// `overflow: overlay` don't allow scroll to the last line
overflow: auto; overflow: auto;
position: relative; position: relative;
color: $textColorAccent; color: $textColorAccent;
background: $logsBackground; background: $logsBackground;

View File

@ -5,17 +5,7 @@
margin-left: $padding * 2; margin-left: $padding * 2;
margin-top: $padding * 2; margin-top: $padding * 2;
.theme-light & {
.xterm-viewport {
@include custom-scrollbar(dark);
}
}
> .xterm { > .xterm {
overflow: hidden; overflow: hidden;
} }
.xterm-viewport {
@include custom-scrollbar;
}
} }

View File

@ -10,12 +10,6 @@
box-shadow: 0 0 $unit * 2 $boxShadow; box-shadow: 0 0 $unit * 2 $boxShadow;
z-index: $zIndex-drawer; z-index: $zIndex-drawer;
.theme-light & {
.drawer-content {
@include custom-scrollbar(dark);
}
}
&.left { &.left {
left: 0; left: 0;
} }
@ -71,11 +65,8 @@
} }
.drawer-content { .drawer-content {
@include custom-scrollbar; overflow: auto;
padding: var(--spacing);
> *:not(.Spinner) {
padding: var(--spacing);
}
.Table .TableHead { .Table .TableHead {
background-color: $contentColor; background-color: $contentColor;

View File

@ -33,7 +33,7 @@
} }
> .content-wrapper { > .content-wrapper {
@include custom-scrollbar-themed; overflow: auto;
padding: $spacing * 2; padding: $spacing * 2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -19,11 +19,7 @@
&.pinned { &.pinned {
.sidebar-nav { .sidebar-nav {
@include custom-scrollbar; overflow: auto;
.theme-light & {
@include custom-scrollbar(dark);
}
} }
} }
@ -63,8 +59,6 @@
} }
.sidebar-nav { .sidebar-nav {
@include hidden-scrollbar;
padding: $padding / 1.5 0; padding: $padding / 1.5 0;
.Icon { .Icon {

View File

@ -9,13 +9,8 @@
main { main {
@include custom-scrollbar;
$spacing: $margin * 2; $spacing: $margin * 2;
.theme-light & {
@include custom-scrollbar(dark);
}
grid-area: main; grid-area: main;
overflow-y: scroll; // always reserve space for scrollbar (17px) overflow-y: scroll; // always reserve space for scrollbar (17px)
overflow-x: auto; overflow-x: auto;

View File

@ -9,8 +9,8 @@
grid-template-columns: 1fr 40%; grid-template-columns: 1fr 40%;
> * { > * {
@include custom-scrollbar-themed;
--flex-gap: #{$spacing}; --flex-gap: #{$spacing};
overflow: auto;
padding: $spacing; padding: $spacing;
} }

View File

@ -4,10 +4,8 @@
line-height: 1.5; line-height: 1.5;
word-wrap: break-word; word-wrap: break-word;
&.light { pre, table {
pre, table { overflow: auto;
@include custom-scrollbar(dark);
}
} }
.pl-c { .pl-c {
@ -513,7 +511,6 @@
} }
table { table {
@include custom-scrollbar;
border-collapse: collapse; border-collapse: collapse;
display: table; display: table;
width: 100%; width: 100%;
@ -581,13 +578,12 @@
.highlight pre, .highlight pre,
pre { pre {
@include custom-scrollbar;
padding: 16px; padding: 16px;
font-size: 85%; font-size: 85%;
line-height: 1.45; line-height: 1.45;
background-color: $helmDescriptionPreBackground; background-color: $helmDescriptionPreBackground;
border-radius: 3px; border-radius: 3px;
overflow: auto !important; overflow: auto;
} }
pre code { pre code {

View File

@ -6,7 +6,6 @@ import React, { Component } from "react";
import marked from "marked"; import marked from "marked";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { themeStore } from "../../theme.store";
DOMPurify.addHook('afterSanitizeAttributes', function (node) { DOMPurify.addHook('afterSanitizeAttributes', function (node) {
// Set all elements owning target to target=_blank // Set all elements owning target to target=_blank
@ -29,7 +28,7 @@ export class MarkdownViewer extends Component<Props> {
const html = DOMPurify.sanitize(marked(markdown)); const html = DOMPurify.sanitize(marked(markdown));
return ( return (
<div <div
className={cssNames("MarkDownViewer", className, themeStore.activeTheme.type)} className={cssNames("MarkDownViewer", className)}
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
); );

View File

@ -6,52 +6,6 @@
@import "table/table.mixins"; @import "table/table.mixins";
@import "+network/network-mixins"; @import "+network/network-mixins";
// todo: re-use in other places with theming
@mixin custom-scrollbar-themed($invert: false) {
@if ($invert) {
@include custom-scrollbar(dark);
.theme-light & {
@include custom-scrollbar(light);
}
} @else {
// fits better with dark background
@include custom-scrollbar(light);
.theme-light & {
@include custom-scrollbar(dark);
}
}
}
@mixin custom-scrollbar($theme: light, $size: 7px, $borderSpacing: 5px) {
$themes: (
light: #5f6064,
dark: #bbb,
);
$scrollBarColor: if(map_has_key($themes, $theme), map_get($themes, $theme), none);
$scrollBarSize: calc(#{$size} + #{$borderSpacing} * 2);
overflow: auto; // allow scrolling for container
// Webkit
&::-webkit-scrollbar {
width: $scrollBarSize;
height: $scrollBarSize;
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: $scrollBarColor;
background-clip: padding-box;
border: $borderSpacing solid transparent;
border-radius: $scrollBarSize;
}
&::-webkit-scrollbar-corner {
background-color: transparent;
}
}
// Hide scrollbar but keep the element scrollable // Hide scrollbar but keep the element scrollable
@mixin hidden-scrollbar { @mixin hidden-scrollbar {
overflow: auto; overflow: auto;

View File

@ -75,7 +75,6 @@ html {
min-width: 100%; min-width: 100%;
&-list { &-list {
@include custom-scrollbar;
padding-right: 1px; padding-right: 1px;
padding-left: 1px; padding-left: 1px;
width: max-content; width: max-content;
@ -152,10 +151,6 @@ html {
--select-option-selected-bgc: $textColorSecondary; --select-option-selected-bgc: $textColorSecondary;
.Select { .Select {
&__menu-list {
@include custom-scrollbar($theme: dark);
}
&__multi-value { &__multi-value {
background: none; background: none;
box-shadow: 0 0 0 1px $textColorSecondary; box-shadow: 0 0 0 1px $textColorSecondary;

View File

@ -1,19 +1,14 @@
.Table { .Table {
&.scrollable {
.theme-light & {
@include custom-scrollbar(dark);
}
@include custom-scrollbar();
flex: 1 0 0; // hackfix: flex-basis must be "0" for proper work in firefox
}
&.autoSize { &.autoSize {
.TableCell { .TableCell {
flex: 1 0; flex: 1 0;
} }
} }
&.scrollable {
overflow: auto;
}
&.selectable { &.selectable {
.TableHead, .TableRow { .TableHead, .TableRow {
padding: 0 $padding; padding: 0 $padding;

View File

@ -2,12 +2,6 @@
overflow: hidden; overflow: hidden;
> .list { > .list {
@include custom-scrollbar; overflow-y: overlay!important;
.theme-light & {
@include custom-scrollbar(dark);
}
overflow-y: overlay !important;
} }
} }

View File

@ -15,7 +15,7 @@
} }
@mixin scrollableContent() { @mixin scrollableContent() {
@include custom-scrollbar($theme: dark); overflow: auto;
padding: var(--wizard-spacing); padding: var(--wizard-spacing);
height: var(--wizard-content-height); height: var(--wizard-content-height);
max-height: var(--wizard-content-max-height); max-height: var(--wizard-content-max-height);

View File

@ -107,6 +107,7 @@
"selectOptionHoveredColor": "#87909c", "selectOptionHoveredColor": "#87909c",
"lineProgressBackground": "#414448", "lineProgressBackground": "#414448",
"radioActiveBackground": "#36393e", "radioActiveBackground": "#36393e",
"menuActiveBackground": "#36393e" "menuActiveBackground": "#36393e",
"scrollBarColor": "#5f6064"
} }
} }

View File

@ -39,13 +39,13 @@
"helmImgBackground": "#e8e8e8", "helmImgBackground": "#e8e8e8",
"helmStableRepo": "#3d90ce", "helmStableRepo": "#3d90ce",
"helmIncubatorRepo": "#ff7043", "helmIncubatorRepo": "#ff7043",
"helmDescriptionHr": "#41474a", "helmDescriptionHr": "#dddddd",
"helmDescriptionBlockqouteColor": "#555555", "helmDescriptionBlockqouteColor": "#555555",
"helmDescriptionBlockqouteBorder": "#8a8f93", "helmDescriptionBlockqouteBorder": "#8a8f93",
"helmDescriptionBlockquoteBackground": "#eeeeee", "helmDescriptionBlockquoteBackground": "#eeeeee",
"helmDescriptionHeaders": "#3e4147", "helmDescriptionHeaders": "#3e4147",
"helmDescriptionH6": "#6a737d", "helmDescriptionH6": "#6a737d",
"helmDescriptionTdBorder": "#47494a", "helmDescriptionTdBorder": "#c6c6c6",
"helmDescriptionTrBackground": "#1c2125", "helmDescriptionTrBackground": "#1c2125",
"helmDescriptionCodeBackground": "#ffffff1a", "helmDescriptionCodeBackground": "#ffffff1a",
"helmDescriptionPreBackground": "#eeeeee", "helmDescriptionPreBackground": "#eeeeee",
@ -108,6 +108,7 @@
"selectOptionHoveredColor": "#ffffff", "selectOptionHoveredColor": "#ffffff",
"lineProgressBackground": "#e8e8e8", "lineProgressBackground": "#e8e8e8",
"radioActiveBackground": "#f1f1f1", "radioActiveBackground": "#f1f1f1",
"menuActiveBackground": "#e8e8e8" "menuActiveBackground": "#e8e8e8",
"scrollBarColor": "#bbbbbb"
} }
} }