diff --git a/docs/extensions/get-started/your-first-extension.md b/docs/extensions/get-started/your-first-extension.md index 4e43246a41..218d563831 100644 --- a/docs/extensions/get-started/your-first-extension.md +++ b/docs/extensions/get-started/your-first-extension.md @@ -1,5 +1,11 @@ # 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. ## Install the Extension diff --git a/docs/extensions/guides/generator.md b/docs/extensions/guides/generator.md new file mode 100644 index 0000000000..68f0ad2e7c --- /dev/null +++ b/docs/extensions/guides/generator.md @@ -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\\.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) diff --git a/docs/extensions/guides/images/hello-lens.png b/docs/extensions/guides/images/hello-lens.png new file mode 100644 index 0000000000..5e2c0ac0a5 Binary files /dev/null and b/docs/extensions/guides/images/hello-lens.png differ diff --git a/docs/extensions/guides/images/hello-world.png b/docs/extensions/guides/images/hello-world.png new file mode 100644 index 0000000000..1a4a9c73a9 Binary files /dev/null and b/docs/extensions/guides/images/hello-world.png differ diff --git a/docs/support/README.md b/docs/support/README.md index ad10085875..8af7d38e8c 100644 --- a/docs/support/README.md +++ b/docs/support/README.md @@ -1,13 +1,16 @@ -# Welcome to Lens support -Here you will find different ways of getting support for Lens. +# Support -## Community Slack Channel -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 here +Here you will find different ways of getting support for Lens IDE. -## Open Source Github Repository -Search feature requests, submit an idea, review existing issues, or open a new one at our Github repository here +## Community Support -## Enterprise Support -If you are interested in paid support options designed for enterprises to cover Lens usage at scale please see the following links: - -- Mirantis \ No newline at end of file +* [Community Slack](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) - Request for support and help from the Lens community via Slack. +* [Github Issues](https://github.com/lensapp/lens/issues) - Submit your issues and feature requests to Lens IDE via Github. + +## 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) diff --git a/extensions/metrics-cluster-feature/renderer.tsx b/extensions/metrics-cluster-feature/renderer.tsx index 9192b10364..b1afd1e570 100644 --- a/extensions/metrics-cluster-feature/renderer.tsx +++ b/extensions/metrics-cluster-feature/renderer.tsx @@ -10,9 +10,9 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio Description: () => { return ( - Enable timeseries data visualization (Prometheus stack) for your cluster. - Install this only if you don't have existing Prometheus stack installed. - You can see preview of manifests here. + Enable timeseries data visualization (Prometheus stack) for your cluster. + Install this only if you don't have existing Prometheus stack installed. + You can see preview of manifests here. ); } diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 35891ee5fb..c8c7af7c8b 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -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) describe("Lens integration tests", () => { const TEST_NAMESPACE = "integration-tests"; - const BACKSPACE = "\uE003"; + let app: Application; const appStart = async () => { @@ -37,23 +37,33 @@ describe("Lens integration tests", () => { const minikubeReady = (): boolean => { // determine if minikube is running - let status = spawnSync("minikube status", { shell: true }); - if (status.status !== 0) { - console.warn("minikube not running"); - return false; + { + const { status } = spawnSync("minikube status", { shell: true }); + if (status !== 0) { + console.warn("minikube not running"); + return false; + } } // Remove TEST_NAMESPACE if it already exists - status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); - if (status.status === 0) { - console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); - status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }); - if (status.status !== 0) { - console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`); - return false; + { + const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); + if (status === 0) { + console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); + + const { status, stdout, stderr } = spawnSync( + `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; }; const ready = minikubeReady(); @@ -62,8 +72,8 @@ describe("Lens integration tests", () => { beforeAll(appStart, 20000); afterAll(async () => { - if (app && app.isRunning()) { - return util.tearDown(app); + if (app?.isRunning()) { + await util.tearDown(app); } }); diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 32b7bece35..9df2d9ed66 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -8,9 +8,8 @@ const AppPaths: Partial> = { export function setup(): Application { return new Application({ - // path to electron app + path: AppPaths[process.platform], // path to electron app args: [], - path: AppPaths[process.platform], startTimeout: 30000, waitTimeout: 60000, env: { @@ -19,9 +18,10 @@ export function setup(): Application { }); } +type AsyncPidGetter = () => Promise; + export async function tearDown(app: Application) { - const mpid: any = app.mainProcess.pid; - const pid = await mpid(); + const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); await app.stop(); try { process.kill(pid, "SIGKILL"); diff --git a/mkdocs.yml b/mkdocs.yml index 80a75c6dc0..f225d2250c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,7 @@ nav: - Overview: extensions/guides/README.md - Main Extension: extensions/guides/main-extension.md - Renderer Extension: extensions/guides/renderer-extension.md + - Generator: extensions/guides/generator.md - Testing and Publishing: - Testing Extensions: extensions/testing-and-publishing/testing.md - Publishing Extensions: extensions/testing-and-publishing/publishing.md diff --git a/src/common/vars.ts b/src/common/vars.ts index b37f3c2135..ac9f1336ee 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -6,8 +6,8 @@ import { defineGlobal } from "./utils/defineGlobal"; export const isMac = process.platform === "darwin"; export const isWindows = process.platform === "win32"; export const isLinux = process.platform === "linux"; -export const isDebugging = process.env.DEBUG === "true"; -export const isSnap = !!process.env["SNAP"]; +export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase()); +export const isSnap = !!process.env.SNAP; export const isProduction = process.env.NODE_ENV === "production"; export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isDevelopment = !isTestEnv && !isProduction; diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 33d8f2833d..895fb272d7 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -6,6 +6,7 @@ import path from "path"; import { getBundledExtensions } from "../common/utils/app-version"; import logger from "../main/logger"; import { extensionInstaller, PackageJson } from "./extension-installer"; +import { extensionsStore } from "./extensions-store"; import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"; export interface InstalledExtension { @@ -91,7 +92,7 @@ export class ExtensionDiscovery { init() { this.watchExtensions(); } - + /** * Watches for added/removed local extensions. * Dependencies are installed automatically after an extension folder is copied. @@ -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; - isEnabled?: boolean; } = {}): Promise { let manifestJson: LensExtensionManifest; + let isEnabled: boolean; try { // check manifest file for existence fs.accessSync(manifestPath, fs.constants.F_OK); manifestJson = __non_webpack_require__(manifestPath); + const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json"); this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); + const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); return { - manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), + manifestPath: installedManifestPath, manifest: manifestJson, isBundled, - isEnabled, + isEnabled }; } catch (error) { 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. */ - async loadExtensionFromPath(absPath: string, { isBundled = false, isEnabled = isBundled }: { + async loadExtensionFromPath(absPath: string, { isBundled = false }: { isBundled?: boolean; - isEnabled?: boolean; } = {}): Promise { const manifestPath = path.resolve(absPath, manifestFilename); - return this.getByManifest(manifestPath, { isBundled, isEnabled }); + return this.getByManifest(manifestPath, { isBundled }); } } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index e51caa298b..af0e9d6f86 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -1,6 +1,7 @@ import { app, ipcRenderer, remote } from "electron"; import { action, computed, observable, reaction, toJS, when } from "mobx"; import path from "path"; +import { getHostedCluster } from "../common/cluster-store"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; import logger from "../main/logger"; import type { InstalledExtension } from "./extension-discovery"; @@ -94,14 +95,14 @@ export class ExtensionLoader { loadOnMain() { logger.info(`${logModule}: load on main`); - this.autoInitExtensions((ext: LensMainExtension) => [ + this.autoInitExtensions(async (ext: LensMainExtension) => [ registries.menuRegistry.add(ext.appMenus) ]); } loadOnClusterManagerRenderer() { 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.globalPageMenuRegistry.add(ext.globalPageMenus, ext), registries.appPreferenceRegistry.add(ext.appPreferences), @@ -112,33 +113,43 @@ export class ExtensionLoader { loadOnClusterRenderer() { logger.info(`${logModule}: load on cluster renderer (dashboard)`); - this.autoInitExtensions((ext: LensRendererExtension) => [ - 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) - ]); + const cluster = getHostedCluster(); + this.autoInitExtensions(async (ext: LensRendererExtension) => { + if (await ext.isEnabledForCluster(cluster) === false) { + return []; + } + 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) { return reaction(() => this.toJSON(), installedExtensions => { for (const [extId, ext] of installedExtensions) { - let instance = this.instances.get(extId); - if (ext.isEnabled && !instance) { + const alreadyInit = this.instances.has(extId); + + if (ext.isEnabled && !alreadyInit) { try { - const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext); - if (!LensExtensionClass) continue; - instance = new LensExtensionClass(ext); + const LensExtensionClass = this.requireExtension(ext); + if (!LensExtensionClass) { + continue; + } + + const instance = new LensExtensionClass(ext); instance.whenEnabled(() => register(instance)); instance.enable(); this.instances.set(extId, instance); } catch (err) { logger.error(`${logModule}: activation extension error`, { ext, err }); } - } else if (!ext.isEnabled && instance) { - logger.info(`${logModule} deleting extension ${extId}`); + } else if (!ext.isEnabled && alreadyInit) { try { + const instance = this.instances.get(extId); instance.disable(); this.instances.delete(extId); } catch (err) { @@ -151,7 +162,7 @@ export class ExtensionLoader { }); } - protected requireExtension(extension: InstalledExtension) { + protected requireExtension(extension: InstalledExtension): LensExtensionConstructor { let extEntrypoint = ""; try { if (ipcRenderer && extension.manifest.renderer) { diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 731a599eef..b81c415f85 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -47,11 +47,6 @@ export class ExtensionsStore extends BaseStore { await extensionLoader.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 reaction(() => this.state.toJS(), extensionsState => { extensionsState.forEach((state, extId) => { @@ -70,7 +65,7 @@ export class ExtensionsStore extends BaseStore { isEnabled(extId: LensExtensionId) { const state = this.state.get(extId); - return !state /* enabled by default */ || state.enabled; + return state && state.enabled; // by default false } @action diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 65956ff9ee..3c9f70eb49 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,5 +1,6 @@ import type { InstalledExtension } from "./extension-discovery"; import { action, observable, reaction } from "mobx"; +import { filesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; export type LensExtensionId = string; // path to manifest (package.json) @@ -40,6 +41,17 @@ export class LensExtension { 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 { + return filesystemProvisionerStore.requestDirectory(this.id); + } + get description() { return this.manifest.description; } @@ -68,15 +80,16 @@ export class LensExtension { } } - async whenEnabled(handlers: () => Function[]) { + async whenEnabled(handlers: () => Promise) { const disposers: Function[] = []; const unregisterHandlers = () => { disposers.forEach(unregister => unregister()); disposers.length = 0; }; - const cancelReaction = reaction(() => this.isEnabled, isEnabled => { + const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => { if (isEnabled) { - disposers.push(...handlers()); + const handlerDisposers = await handlers(); + disposers.push(...handlerDisposers); } else { unregisterHandlers(); } diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index bdea1506e8..4947d76108 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -5,7 +5,7 @@ import { WindowManager } from "../main/window-manager"; import { getExtensionPageUrl } from "./registries/page-registry"; export class LensMainExtension extends LensExtension { - @observable.shallow appMenus: MenuRegistration[] = []; + appMenus: MenuRegistration[] = []; async navigate

(pageId?: string, params?: P, frameId?: number) { const windowManager = WindowManager.getInstance(); diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index c1769f0953..0ed286e94a 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,19 +1,21 @@ import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"; +import type { Cluster } from "../main/cluster"; import { observable } from "mobx"; import { LensExtension } from "./lens-extension"; import { getExtensionPageUrl } from "./registries/page-registry"; + export class LensRendererExtension extends LensExtension { - @observable.shallow globalPages: PageRegistration[] = []; - @observable.shallow clusterPages: PageRegistration[] = []; - @observable.shallow globalPageMenus: PageMenuRegistration[] = []; - @observable.shallow clusterPageMenus: PageMenuRegistration[] = []; - @observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; - @observable.shallow appPreferences: AppPreferenceRegistration[] = []; - @observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []; - @observable.shallow statusBarItems: StatusBarRegistration[] = []; - @observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = []; - @observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []; + globalPages: PageRegistration[] = []; + clusterPages: PageRegistration[] = []; + globalPageMenus: PageMenuRegistration[] = []; + clusterPageMenus: PageMenuRegistration[] = []; + kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; + appPreferences: AppPreferenceRegistration[] = []; + clusterFeatures: ClusterFeatureRegistration[] = []; + statusBarItems: StatusBarRegistration[] = []; + kubeObjectDetailItems: KubeObjectDetailRegistration[] = []; + kubeObjectMenuItems: KubeObjectMenuRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); @@ -24,4 +26,11 @@ export class LensRendererExtension extends LensExtension { }); navigate(pageUrl); } + + /** + * Defines if extension is enabled for a given cluster. Defaults to `true`. + */ + async isEnabledForCluster(cluster: Cluster): Promise { + return true; + } } diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts new file mode 100644 index 0000000000..fb3a4060be --- /dev/null +++ b/src/main/extension-filesystem.ts @@ -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; // extension names to paths +} + +export class FilesystemProvisionerStore extends BaseStore { + @observable registeredExtensions = observable.map(); + + 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 { + 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(); diff --git a/src/main/index.ts b/src/main/index.ts index d0e3740058..1d6aadfd43 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -25,6 +25,7 @@ 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"; +import { filesystemProvisionerStore } from "./extension-filesystem"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -59,6 +60,7 @@ app.on("ready", async () => { clusterStore.load(), workspaceStore.load(), extensionsStore.load(), + filesystemProvisionerStore.load(), ]); // find free port diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 7aa78f1682..ef22e72736 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -15,6 +15,7 @@ import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; import { extensionsStore } from "../extensions/extensions-store"; import { extensionLoader } from "../extensions/extension-loader"; +import { filesystemProvisionerStore } from "../main/extension-filesystem"; type AppComponent = React.ComponentType & { init?(): Promise; @@ -39,6 +40,7 @@ export async function bootstrap(App: AppComponent) { workspaceStore.load(), clusterStore.load(), extensionsStore.load(), + filesystemProvisionerStore.load(), i18nStore.init(), themeStore.init(), ]); diff --git a/src/renderer/components/+apps-releases/release-details.scss b/src/renderer/components/+apps-releases/release-details.scss index a6fb0e1cf6..672d739de1 100644 --- a/src/renderer/components/+apps-releases/release-details.scss +++ b/src/renderer/components/+apps-releases/release-details.scss @@ -32,7 +32,6 @@ border: 1px solid var(--drawerSubtitleBackground); border-radius: $radius; overflow: auto; - @include custom-scrollbar(); .TableHead { border-bottom: none; diff --git a/src/renderer/components/+preferences/preferences.scss b/src/renderer/components/+preferences/preferences.scss index 1fd6469494..cb0d866e15 100644 --- a/src/renderer/components/+preferences/preferences.scss +++ b/src/renderer/components/+preferences/preferences.scss @@ -6,7 +6,6 @@ .Badge { display: flex; - margin: 0; margin-bottom: 1px; padding: $padding $spacing; } diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index 78f2896a73..ba869cec52 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -137,7 +137,7 @@ export class Preferences extends React.Component { formatOptionLabel={this.formatHelmOptionLabel} controlShouldRenderValue={false} /> -

+
{Array.from(this.helmAddedRepos).map(([name, repo]) => { const tooltipId = `message-${name}`; return ( diff --git a/src/renderer/components/+whats-new/whats-new.scss b/src/renderer/components/+whats-new/whats-new.scss index 8aa37fdf66..d7b13ac3e7 100644 --- a/src/renderer/components/+whats-new/whats-new.scss +++ b/src/renderer/components/+whats-new/whats-new.scss @@ -26,7 +26,7 @@ } > .content { - @include custom-scrollbar; + overflow: auto; margin-top: $spacing; padding: $spacing * 2; diff --git a/src/renderer/components/ace-editor/ace-editor.scss b/src/renderer/components/ace-editor/ace-editor.scss index 7aabbf01d5..ef04a4aa6f 100644 --- a/src/renderer/components/ace-editor/ace-editor.scss +++ b/src/renderer/components/ace-editor/ace-editor.scss @@ -7,10 +7,6 @@ .theme-light & { border: 1px solid gainsboro; - - .ace_scrollbar { - @include custom-scrollbar(dark); - } } > .editor { @@ -51,8 +47,4 @@ .ace_comment { color: #808080; } - - .ace_scrollbar { - @include custom-scrollbar; - } } \ No newline at end of file diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index 836d3a4ab1..037b088efe 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -1,15 +1,6 @@ @import "~flex.box"; @import "fonts"; -*, *:before, *:after { - box-sizing: border-box; - padding: 0; - margin: 0; - border: 0; - outline: none; - -webkit-font-smoothing: antialiased; -} - :root { --unit: 8px; --padding: var(--unit); @@ -27,6 +18,33 @@ --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 { background: $primary; color: white; diff --git a/src/renderer/components/dialog/dialog.scss b/src/renderer/components/dialog/dialog.scss index 7b2887be18..949f801aa4 100644 --- a/src/renderer/components/dialog/dialog.scss +++ b/src/renderer/components/dialog/dialog.scss @@ -1,7 +1,5 @@ .Dialog { - @include custom-scrollbar; - position: fixed; overflow: auto; left: 0; @@ -11,6 +9,7 @@ padding: $unit * 5; z-index: $zIndex-dialog; overscroll-behavior: none; // prevent swiping with touch-pad on MacOSX + overflow: auto; &.modal { background: transparentize(#222, .5); diff --git a/src/renderer/components/dock/pod-logs.scss b/src/renderer/components/dock/pod-logs.scss index c90adf7ddf..47909b4fb9 100644 --- a/src/renderer/components/dock/pod-logs.scss +++ b/src/renderer/components/dock/pod-logs.scss @@ -3,12 +3,7 @@ --overlay-active-bg: orange; .logs { - @include custom-scrollbar; - - // fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight` - // `overflow: overlay` don't allow scroll to the last line overflow: auto; - position: relative; color: $textColorAccent; background: $logsBackground; diff --git a/src/renderer/components/dock/terminal-window.scss b/src/renderer/components/dock/terminal-window.scss index 5bc45c1e1f..f08c378ee9 100644 --- a/src/renderer/components/dock/terminal-window.scss +++ b/src/renderer/components/dock/terminal-window.scss @@ -5,17 +5,7 @@ margin-left: $padding * 2; margin-top: $padding * 2; - .theme-light & { - .xterm-viewport { - @include custom-scrollbar(dark); - } - } - > .xterm { overflow: hidden; } - - .xterm-viewport { - @include custom-scrollbar; - } } \ No newline at end of file diff --git a/src/renderer/components/drawer/drawer.scss b/src/renderer/components/drawer/drawer.scss index 488a890f2c..b34b3b5965 100644 --- a/src/renderer/components/drawer/drawer.scss +++ b/src/renderer/components/drawer/drawer.scss @@ -10,12 +10,6 @@ box-shadow: 0 0 $unit * 2 $boxShadow; z-index: $zIndex-drawer; - .theme-light & { - .drawer-content { - @include custom-scrollbar(dark); - } - } - &.left { left: 0; } @@ -71,11 +65,8 @@ } .drawer-content { - @include custom-scrollbar; - - > *:not(.Spinner) { - padding: var(--spacing); - } + overflow: auto; + padding: var(--spacing); .Table .TableHead { background-color: $contentColor; diff --git a/src/renderer/components/layout/page-layout.scss b/src/renderer/components/layout/page-layout.scss index b0540b9a15..c975ea3305 100644 --- a/src/renderer/components/layout/page-layout.scss +++ b/src/renderer/components/layout/page-layout.scss @@ -33,7 +33,7 @@ } > .content-wrapper { - @include custom-scrollbar-themed; + overflow: auto; padding: $spacing * 2; display: flex; flex-direction: column; diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index 7b1406fb7b..1c5932b8d9 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -19,11 +19,7 @@ &.pinned { .sidebar-nav { - @include custom-scrollbar; - - .theme-light & { - @include custom-scrollbar(dark); - } + overflow: auto; } } @@ -63,8 +59,6 @@ } .sidebar-nav { - @include hidden-scrollbar; - padding: $padding / 1.5 0; .Icon { diff --git a/src/renderer/components/layout/tab-layout.scss b/src/renderer/components/layout/tab-layout.scss index e8b62558d7..639a089527 100755 --- a/src/renderer/components/layout/tab-layout.scss +++ b/src/renderer/components/layout/tab-layout.scss @@ -9,13 +9,8 @@ main { - @include custom-scrollbar; $spacing: $margin * 2; - .theme-light & { - @include custom-scrollbar(dark); - } - grid-area: main; overflow-y: scroll; // always reserve space for scrollbar (17px) overflow-x: auto; diff --git a/src/renderer/components/layout/wizard-layout.scss b/src/renderer/components/layout/wizard-layout.scss index 73c5afc3fe..155bf0ae23 100644 --- a/src/renderer/components/layout/wizard-layout.scss +++ b/src/renderer/components/layout/wizard-layout.scss @@ -9,8 +9,8 @@ grid-template-columns: 1fr 40%; > * { - @include custom-scrollbar-themed; --flex-gap: #{$spacing}; + overflow: auto; padding: $spacing; } diff --git a/src/renderer/components/markdown-viewer/markdown-viewer.scss b/src/renderer/components/markdown-viewer/markdown-viewer.scss index 35b842d4f6..58f808b99b 100644 --- a/src/renderer/components/markdown-viewer/markdown-viewer.scss +++ b/src/renderer/components/markdown-viewer/markdown-viewer.scss @@ -4,10 +4,8 @@ line-height: 1.5; word-wrap: break-word; - &.light { - pre, table { - @include custom-scrollbar(dark); - } + pre, table { + overflow: auto; } .pl-c { @@ -513,7 +511,6 @@ } table { - @include custom-scrollbar; border-collapse: collapse; display: table; width: 100%; @@ -581,13 +578,12 @@ .highlight pre, pre { - @include custom-scrollbar; padding: 16px; font-size: 85%; line-height: 1.45; background-color: $helmDescriptionPreBackground; border-radius: 3px; - overflow: auto !important; + overflow: auto; } pre code { diff --git a/src/renderer/components/markdown-viewer/markdown-viewer.tsx b/src/renderer/components/markdown-viewer/markdown-viewer.tsx index e1870e0d30..b50e4e2c16 100644 --- a/src/renderer/components/markdown-viewer/markdown-viewer.tsx +++ b/src/renderer/components/markdown-viewer/markdown-viewer.tsx @@ -6,7 +6,6 @@ import React, { Component } from "react"; import marked from "marked"; import DOMPurify from "dompurify"; import { cssNames } from "../../utils"; -import { themeStore } from "../../theme.store"; DOMPurify.addHook('afterSanitizeAttributes', function (node) { // Set all elements owning target to target=_blank @@ -29,7 +28,7 @@ export class MarkdownViewer extends Component { const html = DOMPurify.sanitize(marked(markdown)); return (
); diff --git a/src/renderer/components/mixins.scss b/src/renderer/components/mixins.scss index 2509c1e951..6573a7487a 100755 --- a/src/renderer/components/mixins.scss +++ b/src/renderer/components/mixins.scss @@ -6,52 +6,6 @@ @import "table/table.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 @mixin hidden-scrollbar { overflow: auto; diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index a9468a0c8a..f3fd6e47b0 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -75,7 +75,6 @@ html { min-width: 100%; &-list { - @include custom-scrollbar; padding-right: 1px; padding-left: 1px; width: max-content; @@ -152,10 +151,6 @@ html { --select-option-selected-bgc: $textColorSecondary; .Select { - &__menu-list { - @include custom-scrollbar($theme: dark); - } - &__multi-value { background: none; box-shadow: 0 0 0 1px $textColorSecondary; diff --git a/src/renderer/components/table/table.scss b/src/renderer/components/table/table.scss index 1ff6343598..76b02ac62f 100644 --- a/src/renderer/components/table/table.scss +++ b/src/renderer/components/table/table.scss @@ -1,19 +1,14 @@ .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 { .TableCell { flex: 1 0; } } + &.scrollable { + overflow: auto; + } + &.selectable { .TableHead, .TableRow { padding: 0 $padding; diff --git a/src/renderer/components/virtual-list/virtual-list.scss b/src/renderer/components/virtual-list/virtual-list.scss index 4357321c7a..a7e1deda36 100644 --- a/src/renderer/components/virtual-list/virtual-list.scss +++ b/src/renderer/components/virtual-list/virtual-list.scss @@ -2,12 +2,6 @@ overflow: hidden; > .list { - @include custom-scrollbar; - - .theme-light & { - @include custom-scrollbar(dark); - } - - overflow-y: overlay !important; + overflow-y: overlay!important; } } \ No newline at end of file diff --git a/src/renderer/components/wizard/wizard.scss b/src/renderer/components/wizard/wizard.scss index 9895b1bec1..487796f822 100755 --- a/src/renderer/components/wizard/wizard.scss +++ b/src/renderer/components/wizard/wizard.scss @@ -15,7 +15,7 @@ } @mixin scrollableContent() { - @include custom-scrollbar($theme: dark); + overflow: auto; padding: var(--wizard-spacing); height: var(--wizard-content-height); max-height: var(--wizard-content-max-height); diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.json index f1afcb0d24..6606af6d15 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -107,6 +107,7 @@ "selectOptionHoveredColor": "#87909c", "lineProgressBackground": "#414448", "radioActiveBackground": "#36393e", - "menuActiveBackground": "#36393e" + "menuActiveBackground": "#36393e", + "scrollBarColor": "#5f6064" } } diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index 302a52a699..ebd8441a9b 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -39,13 +39,13 @@ "helmImgBackground": "#e8e8e8", "helmStableRepo": "#3d90ce", "helmIncubatorRepo": "#ff7043", - "helmDescriptionHr": "#41474a", + "helmDescriptionHr": "#dddddd", "helmDescriptionBlockqouteColor": "#555555", "helmDescriptionBlockqouteBorder": "#8a8f93", "helmDescriptionBlockquoteBackground": "#eeeeee", "helmDescriptionHeaders": "#3e4147", "helmDescriptionH6": "#6a737d", - "helmDescriptionTdBorder": "#47494a", + "helmDescriptionTdBorder": "#c6c6c6", "helmDescriptionTrBackground": "#1c2125", "helmDescriptionCodeBackground": "#ffffff1a", "helmDescriptionPreBackground": "#eeeeee", @@ -108,6 +108,7 @@ "selectOptionHoveredColor": "#ffffff", "lineProgressBackground": "#e8e8e8", "radioActiveBackground": "#f1f1f1", - "menuActiveBackground": "#e8e8e8" + "menuActiveBackground": "#e8e8e8", + "scrollBarColor": "#bbbbbb" } }