From 1be86b34f44677a84b13d9c1cb1d349b4c3ed93b Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 24 Nov 2020 16:15:14 +0200 Subject: [PATCH 1/8] Add LensRendererExtension.isEnabledForCluster (#1502) Signed-off-by: Jari Kolehmainen --- src/extensions/extension-loader.ts | 27 ++++++++++++++--------- src/extensions/lens-extension.ts | 7 +++--- src/extensions/lens-renderer-extension.ts | 9 ++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index e51caa298b..18840da5fc 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,16 +113,22 @@ 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); diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index fc7f3ff0df..0dd6980102 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -67,15 +67,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-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index c1769f0953..0043f2673f 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,8 +1,10 @@ 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[] = []; @@ -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; + } } From 877c827e3512ddf2206780669b654d09e027c21b Mon Sep 17 00:00:00 2001 From: Miska Kaipiainen Date: Tue, 24 Nov 2020 16:17:13 +0200 Subject: [PATCH 2/8] Fix Documentation: Support (#1501) Signed-off-by: Miska Kaipiainen --- docs/support/README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) 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) From b94e523ad5a191d6541c4f2c3f6c8277e7479b2d Mon Sep 17 00:00:00 2001 From: chh <1474479+chenhunghan@users.noreply.github.com> Date: Tue, 24 Nov 2020 22:38:43 +0800 Subject: [PATCH 3/8] Add documentation on how to use Lens Extension Generator (#1411) * Add generator docs Signed-off-by: Hung-Han (Henry) Chen <1474479+chenhunghan@users.noreply.github.com> * 'Welcome' > 'You are welcome to ...' Signed-off-by: Hung-Han (Henry) Chen <1474479+chenhunghan@users.noreply.github.com> * Add missing backslash Signed-off-by: Hung-Han (Henry) Chen <1474479+chenhunghan@users.noreply.github.com> * Move Generator section to Extension Guides Signed-off-by: Hung-Han (Henry) Chen <1474479+chenhunghan@users.noreply.github.com> --- .../get-started/your-first-extension.md | 6 ++ docs/extensions/guides/generator.md | 65 ++++++++++++++++++ docs/extensions/guides/images/hello-lens.png | Bin 0 -> 61522 bytes docs/extensions/guides/images/hello-world.png | Bin 0 -> 63048 bytes mkdocs.yml | 1 + 5 files changed, 72 insertions(+) create mode 100644 docs/extensions/guides/generator.md create mode 100644 docs/extensions/guides/images/hello-lens.png create mode 100644 docs/extensions/guides/images/hello-world.png 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 0000000000000000000000000000000000000000..5e2c0ac0a53febbc6e1a3bb52f24ccf218ee06a0 GIT binary patch literal 61522 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

(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 0043f2673f..0ed286e94a 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -6,16 +6,16 @@ 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"); From 29ea0c84049fca2e857ed2836e5821a4582f8cf0 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 25 Nov 2020 09:09:26 +0200 Subject: [PATCH 6/8] Resolve extension enabled status when loading it (#1485) * Resolve extension enabled status when loading it Signed-off-by: Lauri Nevala * Check if extension is enabled in store unless it is bundled Signed-off-by: Lauri Nevala * Return false by default for isEnabled Signed-off-by: Lauri Nevala * Refactor isEnabled assignment Signed-off-by: Lauri Nevala --- src/extensions/extension-discovery.ts | 18 ++++++++++-------- src/extensions/extensions-store.ts | 7 +------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index bb1d1db420..991725ee67 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/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 From 2da598b66eb0972395f3b3164f80a4af4e4f0279 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 25 Nov 2020 10:25:37 +0300 Subject: [PATCH 7/8] Set scrollbar colors as global styles (#1484) * Setting global webkit-scrollbar styles Signed-off-by: Alex Andreev * Removing usage of custom-scrollbar mixin Signed-off-by: Alex Andreev * Set overflow:auto on .Table Signed-off-by: Alex Andreev * Fixing .drawer-content paddings Signed-off-by: Alex Andreev * Cleaning up Signed-off-by: Alex Andreev --- .../+apps-releases/release-details.scss | 1 - .../components/+preferences/preferences.scss | 1 - .../components/+preferences/preferences.tsx | 2 +- .../components/+whats-new/whats-new.scss | 2 +- .../components/ace-editor/ace-editor.scss | 8 ---- src/renderer/components/app.scss | 36 +++++++++++---- src/renderer/components/dialog/dialog.scss | 3 +- src/renderer/components/dock/pod-logs.scss | 5 -- .../components/dock/terminal-window.scss | 10 ---- src/renderer/components/drawer/drawer.scss | 13 +----- .../components/layout/page-layout.scss | 2 +- src/renderer/components/layout/sidebar.scss | 8 +--- .../components/layout/tab-layout.scss | 5 -- .../components/layout/wizard-layout.scss | 2 +- .../markdown-viewer/markdown-viewer.scss | 10 ++-- .../markdown-viewer/markdown-viewer.tsx | 3 +- src/renderer/components/mixins.scss | 46 ------------------- src/renderer/components/select/select.scss | 5 -- src/renderer/components/table/table.scss | 13 ++---- .../components/virtual-list/virtual-list.scss | 8 +--- src/renderer/components/wizard/wizard.scss | 2 +- src/renderer/themes/lens-dark.json | 3 +- src/renderer/themes/lens-light.json | 7 +-- 23 files changed, 51 insertions(+), 144 deletions(-) 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" } } From 77ae31550af91d256a47ef1f17fd7a79bc0a9450 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 25 Nov 2020 09:55:28 +0200 Subject: [PATCH 8/8] Allow to install packed extensions from URL or local file (#1456) * Option to install an extension from filesystem/url #1227 -- part 1 (UI) Signed-off-by: Roman * DropFileInput: common component to handle droped files (replaced also in add-cluster-page) Signed-off-by: Roman * fix: install via url-string on input.submit Signed-off-by: Roman * ui tweaks & minor fixes Signed-off-by: Roman * more ui/ux tweaks & fixes Signed-off-by: Roman * layout fixes Signed-off-by: Roman * component renaming: `copy-to-click` => `copy-to-clipboard` => `clipboard` Signed-off-by: Roman * reworks -- part 1 Signed-off-by: Roman * fix downloading file, added common/utils/downloadFile Signed-off-by: Roman * confirm before install, unpack tar first steps Signed-off-by: Roman * installation flow, extracting .tgz Signed-off-by: Roman * clean up, fix lint issues Signed-off-by: Roman * update .azure-pipelines.yml Signed-off-by: Roman * fixes & refactoring Signed-off-by: Roman * fix lint harder :/ Signed-off-by: Roman * fix validation Signed-off-by: Roman * fix validation harder Signed-off-by: Roman * responding to comments, fixed package validation Signed-off-by: Roman * common/utils/tar.ts: reject with Error-type Signed-off-by: Roman * fix: unit-tests Signed-off-by: Roman --- package.json | 7 +- src/common/utils/downloadFile.ts | 35 ++ src/common/utils/escapeRegExp.ts | 5 + src/common/utils/index.ts | 3 + src/common/utils/tar.ts | 55 +++ src/common/vars.ts | 2 + src/extensions/extension-discovery.ts | 2 +- src/extensions/lens-extension.ts | 5 + .../__tests__/page-registry.test.ts | 4 +- src/extensions/registries/page-registry.ts | 6 +- src/main/menu.ts | 7 +- .../components/+add-cluster/add-cluster.scss | 8 - .../components/+add-cluster/add-cluster.tsx | 110 +++--- .../components/+extensions/extensions.scss | 65 +++- .../components/+extensions/extensions.tsx | 316 ++++++++++++++++-- .../components/clipboard/clipboard.scss | 3 + .../components/clipboard/clipboard.tsx | 62 ++++ src/renderer/components/clipboard/index.ts | 1 + .../components/dock/pod-log-controls.tsx | 4 +- src/renderer/components/icon/icon.tsx | 2 +- .../components/input/drop-file-input.scss | 9 + .../components/input/drop-file-input.tsx | 76 +++++ src/renderer/components/input/index.ts | 1 + src/renderer/components/input/input.scss | 10 + src/renderer/components/input/input.tsx | 34 +- .../components/input/search-input.scss | 3 +- .../kubeconfig-dialog/kubeconfig-dialog.tsx | 4 +- .../components/layout/wizard-layout.tsx | 5 +- .../notifications/notifications.store.ts | 47 +-- .../notifications/notifications.tsx | 10 +- src/renderer/components/tooltip/tooltip.scss | 13 +- src/renderer/utils/copyToClipboard.ts | 18 +- src/renderer/utils/downloadFile.ts | 12 - src/renderer/utils/index.ts | 2 +- src/renderer/utils/saveFile.ts | 18 + yarn.lock | 28 +- 36 files changed, 799 insertions(+), 193 deletions(-) create mode 100644 src/common/utils/downloadFile.ts create mode 100644 src/common/utils/escapeRegExp.ts create mode 100644 src/common/utils/tar.ts create mode 100644 src/renderer/components/clipboard/clipboard.scss create mode 100644 src/renderer/components/clipboard/clipboard.tsx create mode 100644 src/renderer/components/clipboard/index.ts create mode 100644 src/renderer/components/input/drop-file-input.scss create mode 100644 src/renderer/components/input/drop-file-input.tsx delete mode 100644 src/renderer/utils/downloadFile.ts create mode 100644 src/renderer/utils/saveFile.ts diff --git a/package.json b/package.json index 26c08a2a85..53d67fb862 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "bundledHelmVersion": "3.3.4" }, "engines": { - "node": ">=12.0 <13.0" + "node": ">=12 <13" }, "lingui": { "locales": [ @@ -214,7 +214,7 @@ "@types/node": "^12.12.45", "@types/proper-lockfile": "^4.1.1", "@types/react-beautiful-dnd": "^13.0.0", - "@types/tar": "^4.0.3", + "@types/tar": "^4.0.4", "array-move": "^3.0.0", "await-lock": "^2.1.0", "chalk": "^4.1.0", @@ -251,7 +251,7 @@ "serializr": "^2.0.3", "shell-env": "^3.0.0", "spdy": "^4.0.2", - "tar": "^6.0.2", + "tar": "^6.0.5", "tcp-port-used": "^1.0.1", "tempy": "^0.5.0", "uuid": "^8.1.0", @@ -314,7 +314,6 @@ "@types/sharp": "^0.26.0", "@types/shelljs": "^0.8.8", "@types/spdy": "^3.4.4", - "@types/tar": "^4.0.3", "@types/tcp-port-used": "^1.0.0", "@types/tempy": "^0.3.0", "@types/terser-webpack-plugin": "^3.0.0", diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts new file mode 100644 index 0000000000..a58e9242b4 --- /dev/null +++ b/src/common/utils/downloadFile.ts @@ -0,0 +1,35 @@ +import request from "request"; + +export interface DownloadFileOptions { + url: string; + gzip?: boolean; +} + +export interface DownloadFileTicket { + url: string; + promise: Promise; + cancel(): void; +} + +export function downloadFile({ url, gzip = true }: DownloadFileOptions): DownloadFileTicket { + const fileChunks: Buffer[] = []; + const req = request(url, { gzip }); + const promise: Promise = new Promise((resolve, reject) => { + req.on("data", (chunk: Buffer) => { + fileChunks.push(chunk); + }); + req.once("error", err => { + reject({ url, err }); + }); + req.once("complete", () => { + resolve(Buffer.concat(fileChunks)); + }); + }); + return { + url, + promise, + cancel() { + req.abort(); + } + }; +} diff --git a/src/common/utils/escapeRegExp.ts b/src/common/utils/escapeRegExp.ts new file mode 100644 index 0000000000..dbf10e4bfb --- /dev/null +++ b/src/common/utils/escapeRegExp.ts @@ -0,0 +1,5 @@ +// Helper to sanitize / escape special chars for passing to RegExp-constructor + +export function escapeRegExp(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 330b98fcf7..b1006b5f58 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -15,3 +15,6 @@ export * from "./saveToAppFiles"; export * from "./singleton"; export * from "./openExternal"; export * from "./rectify-array"; +export * from "./downloadFile"; +export * from "./escapeRegExp"; +export * from "./tar"; diff --git a/src/common/utils/tar.ts b/src/common/utils/tar.ts new file mode 100644 index 0000000000..004fa354dc --- /dev/null +++ b/src/common/utils/tar.ts @@ -0,0 +1,55 @@ +// Helper for working with tarball files (.tar, .tgz) +// Docs: https://github.com/npm/node-tar +import tar, { ExtractOptions, FileStat } from "tar"; +import path from "path"; + +export interface ReadFileFromTarOpts { + tarPath: string; + filePath: string; + parseJson?: boolean; +} + +export function readFileFromTar({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise { + return new Promise(async (resolve, reject) => { + const fileChunks: Buffer[] = []; + + await tar.list({ + file: tarPath, + filter: path => path === filePath, + onentry(entry: FileStat) { + entry.on("data", chunk => { + fileChunks.push(chunk); + }); + entry.once("error", err => { + reject(new Error(`reading file has failed ${entry.path}: ${err}`)); + }); + entry.once("end", () => { + const data = Buffer.concat(fileChunks); + const result = parseJson ? JSON.parse(data.toString("utf8")) : data; + resolve(result); + }); + }, + }); + + if (!fileChunks.length) { + reject(new Error("Not found")); + } + }); +} + +export async function listTarEntries(filePath: string): Promise { + const entries: string[] = []; + await tar.list({ + file: filePath, + onentry: (entry: FileStat) => entries.push(entry.path as any as string), + }); + return entries; +} + +export function extractTar(filePath: string, opts: ExtractOptions & { sync?: boolean } = {}) { + return tar.extract({ + file: filePath, + cwd: path.dirname(filePath), + ...opts, + }); +} diff --git a/src/common/vars.ts b/src/common/vars.ts index ab566bb675..ac9f1336ee 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -41,3 +41,5 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis // Links export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues"; export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI"; +export const docsUrl = "https://docs.k8slens.dev/"; +export const supportUrl = "https://docs.k8slens.dev/latest/support/"; diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 991725ee67..895fb272d7 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -17,7 +17,7 @@ export interface InstalledExtension { } const logModule = "[EXTENSION-DISCOVERY]"; -const manifestFilename = "package.json"; +export const manifestFilename = "package.json"; /** * Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link) diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 1af3300ff0..3c9f70eb49 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -12,6 +12,7 @@ export interface LensExtensionManifest { description?: string; main?: string; // path to %ext/dist/main.js renderer?: string; // path to %ext/dist/renderer.js + lens?: object; // fixme: add more required fields for validation } export class LensExtension { @@ -109,3 +110,7 @@ export class LensExtension { // mock } } + +export function sanitizeExtensionName(name: string) { + return name.replace("@", "").replace("/", "--"); +} diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 29e60dccc0..fafc801cc4 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -25,8 +25,8 @@ describe("getPageUrl", () => { expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test"); }); - it("removes @", () => { - expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo-bar"); + it("removes @ and replace `/` to `--`", () => { + expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar"); }); it("adds / prefix", () => { diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index 0b385c02c4..c346c6a56d 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -5,7 +5,7 @@ import path from "path"; import { action } from "mobx"; import { compile } from "path-to-regexp"; import { BaseRegistry } from "./base-registry"; -import { LensExtension } from "../lens-extension"; +import { LensExtension, sanitizeExtensionName } from "../lens-extension"; import logger from "../../main/logger"; import { recitfy } from "../../common/utils"; @@ -45,10 +45,6 @@ export interface PageComponents { Page: React.ComponentType; } -export function sanitizeExtensionName(name: string) { - return name.replace("@", "").replace("/", "-"); -} - export function getExtensionPageUrl

({ extensionId, pageId = "", params }: PageMenuTarget

): string { const extensionBaseUrl = compile(`/extension/:name`)({ name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path diff --git a/src/main/menu.ts b/src/main/menu.ts index e7add90e77..06bd9095cb 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,7 +1,7 @@ import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron"; import { autorun } from "mobx"; import { WindowManager } from "./window-manager"; -import { appName, isMac, isWindows, isTestEnv } from "../common/vars"; +import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl } from "../common/vars"; import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route"; import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; @@ -24,6 +24,7 @@ export function showAbout(browserWindow: BrowserWindow) { `${appName}: ${app.getVersion()}`, `Electron: ${process.versions.electron}`, `Chrome: ${process.versions.chrome}`, + `Node: ${process.versions.node}`, `Copyright 2020 Mirantis, Inc.`, ]; dialog.showMessageBoxSync(browserWindow, { @@ -215,13 +216,13 @@ export function buildMenu(windowManager: WindowManager) { { label: "Documentation", click: async () => { - shell.openExternal('https://docs.k8slens.dev/'); + shell.openExternal(docsUrl); }, }, { label: "Support", click: async () => { - shell.openExternal('https://docs.k8slens.dev/latest/support/'); + shell.openExternal(supportUrl); }, }, ...ignoreOnMac([ diff --git a/src/renderer/components/+add-cluster/add-cluster.scss b/src/renderer/components/+add-cluster/add-cluster.scss index 90977fa78b..d80373406d 100644 --- a/src/renderer/components/+add-cluster/add-cluster.scss +++ b/src/renderer/components/+add-cluster/add-cluster.scss @@ -1,12 +1,4 @@ .AddCluster { - .droppable { - box-shadow: 0 0 0 5px inset $primary; - - > * { - pointer-events: none; - } - } - .hint { margin-top: -$padding; color: $textColorSecondary; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 72b54aabd1..1fae256618 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -8,7 +8,7 @@ import { KubeConfig } from "@kubernetes/client-node"; import { _i18n } from "../../i18n"; import { t, Trans } from "@lingui/macro"; import { Select, SelectOption } from "../select"; -import { Input } from "../input"; +import { DropFileInput, Input } from "../input"; import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { Icon } from "../icon"; @@ -44,7 +44,6 @@ export class AddCluster extends React.Component { @observable proxyServer = ""; @observable isWaiting = false; @observable showSettings = false; - @observable dropAreaActive = false; componentDidMount() { clusterStore.setActive(null); @@ -121,6 +120,11 @@ export class AddCluster extends React.Component { } }; + onDropKubeConfig = (files: File[]) => { + this.sourceTab = KubeConfigSourceTab.FILE; + this.setKubeConfig(files[0].path); + }; + @action addClusters = () => { let newClusters: ClusterModel[] = []; @@ -139,7 +143,7 @@ export class AddCluster extends React.Component { return true; } catch (err) { this.error = String(err.message); - if (err instanceof ExecValidationNotFoundError ) { + if (err instanceof ExecValidationNotFoundError) { Notifications.error(Error while adding cluster(s): {this.error}); return false; } else { @@ -230,7 +234,7 @@ export class AddCluster extends React.Component { Select kubeconfig file} - active={this.sourceTab == KubeConfigSourceTab.FILE} /> + active={this.sourceTab == KubeConfigSourceTab.FILE}/> Paste as text} @@ -344,71 +348,55 @@ export class AddCluster extends React.Component { return (

{context} - {isNew && } - {isSelected && } + {isNew && } + {isSelected && }
); }; render() { const addDisabled = this.selectedContexts.length === 0; - return ( - this.dropAreaActive = true, - onDragLeave: event => this.dropAreaActive = false, - onDragOver: event => { - event.preventDefault(); // enable onDrop()-callback - event.dataTransfer.dropEffect = "move"; - }, - onDrop: event => { - this.sourceTab = KubeConfigSourceTab.FILE; - this.dropAreaActive = false; - this.setKubeConfig(event.dataTransfer.files[0].path); - } - }} - > -

Add Cluster

- {this.renderKubeConfigSource()} - {this.renderContextSelector()} - - {this.showSettings && ( -
-

HTTP Proxy server. Used for communicating with Kubernetes API.

- this.proxyServer = value} - theme="round-black" - /> - - {'A HTTP proxy server URL (format: http://
:).'} - + + +

Add Cluster

+ {this.renderKubeConfigSource()} + {this.renderContextSelector()} + - )} - {this.error && ( -
{this.error}
- )} -
-
-
+ {this.showSettings && ( +
+

HTTP Proxy server. Used for communicating with Kubernetes API.

+ this.proxyServer = value} + theme="round-black" + /> + + {'A HTTP proxy server URL (format: http://
:).'} + +
+ )} + {this.error && ( +
{this.error}
+ )} +
+
+ +
); } } diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index 63778d37e9..8350b62b9c 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -1,22 +1,54 @@ .Extensions { + $spacing: $padding * 2; --width: 100%; --max-width: auto; - .extension-list { + .extensions-list { .extension { --flex-gap: $padding / 3; - padding: $padding $padding * 2; + padding: $padding $spacing; background: $colorVague; border-radius: $radius; &:not(:first-of-type) { - margin-top: $padding * 2; + margin-top: $spacing; } } } + .extensions-info { + --flex-gap: #{$spacing}; + + > .flex.gaps { + --flex-gap: #{$padding}; + } + } + .extensions-path { word-break: break-all; + + &:hover code { + color: $textColorSecondary; + cursor: pointer; + } + } + + .Clipboard { + display: inline; + vertical-align: baseline; + font-size: $font-size-small; + + &:hover { + color: $textColorSecondary; + } + } + + .SearchInput { + --spacing: #{$padding}; + } + + .SubTitle { + font-style: italic; } .WizardLayout { @@ -27,15 +59,28 @@ align-self: flex-start; } } +} - .SearchInput { - margin-top: $margin / 2; - margin-bottom: $margin * 2; - max-width: none; +.InstallingExtensionNotification { + .remove-folder-warning { + font-size: $font-size-small; + font-style: italic; + opacity: .8; + cursor: pointer; - > label { - padding: $padding $padding * 2; - border-radius: $radius; + &:hover { + opacity: 1; + } + + code { + display: inline; + color: inherit; } } + + .Button { + background-color: unset; + border: 1px solid currentColor; + box-shadow: none !important; + } } \ No newline at end of file diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index a8b9c54f51..90577cd3a4 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -1,5 +1,8 @@ import "./extensions.scss"; -import { shell } from "electron"; +import { remote, shell } from "electron"; +import os from "os"; +import path from "path"; +import fse from "fs-extra"; import React from "react"; import { computed, observable } from "mobx"; import { observer } from "mobx-react"; @@ -7,15 +10,39 @@ import { t, Trans } from "@lingui/macro"; import { _i18n } from "../../i18n"; import { Button } from "../button"; import { WizardLayout } from "../layout/wizard-layout"; -import { Input } from "../input"; +import { DropFileInput, Input, InputValidators, SearchInput } from "../input"; import { Icon } from "../icon"; +import { SubTitle } from "../layout/sub-title"; import { PageLayout } from "../layout/page-layout"; +import { Clipboard } from "../clipboard"; +import logger from "../../../main/logger"; import { extensionLoader } from "../../../extensions/extension-loader"; -import { extensionDiscovery } from "../../../extensions/extension-discovery"; +import { extensionDiscovery, manifestFilename } from "../../../extensions/extension-discovery"; +import { LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension"; +import { Notifications } from "../notifications"; +import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils"; +import { docsUrl } from "../../../common/vars"; + +interface InstallRequest { + fileName: string; + filePath?: string; + data?: Buffer; +} + +interface InstallRequestPreloaded extends InstallRequest { + data: Buffer; +} + +interface InstallRequestValidated extends InstallRequestPreloaded { + manifest: LensExtensionManifest; + tempFile: string; // temp system path to packed extension for unpacking +} @observer export class Extensions extends React.Component { + private supportedFormats = [".tar", ".tgz"]; @observable search = ""; + @observable downloadUrl = ""; @computed get extensions() { const searchText = this.search.toLowerCase(); @@ -32,27 +59,265 @@ export class Extensions extends React.Component { return extensionDiscovery.localFolderPath; } + getExtensionPackageTemp(fileName = "") { + return path.join(os.tmpdir(), "lens-extensions", fileName); + } + + getExtensionDestFolder(name: string) { + return path.join(this.extensionsPath, sanitizeExtensionName(name)); + } + + installFromSelectFileDialog = async () => { + const { dialog, BrowserWindow, app } = remote; + const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { + defaultPath: app.getPath("downloads"), + properties: ["openFile", "multiSelections"], + message: _i18n._(t`Select extensions to install (formats: ${this.supportedFormats.join(", ")}), `), + buttonLabel: _i18n._(t`Use configuration`), + filters: [ + { name: "tarball", extensions: this.supportedFormats } + ] + }); + if (!canceled && filePaths.length) { + this.requestInstall( + filePaths.map(filePath => ({ + fileName: path.basename(filePath), + filePath, + })) + ); + } + }; + + addExtensions = () => { + const { downloadUrl } = this; + if (downloadUrl && InputValidators.isUrl.validate(downloadUrl)) { + this.installFromUrl(downloadUrl); + } else { + this.installFromSelectFileDialog(); + } + }; + + installFromUrl = async (url: string) => { + try { + const { promise: filePromise } = downloadFile({ url }); + this.requestInstall([{ + fileName: path.basename(url), + data: await filePromise, + }]); + } catch (err) { + Notifications.error( +

Installation via URL has failed: {String(err)}

+ ); + } + }; + + installOnDrop = (files: File[]) => { + logger.info('Install from D&D'); + return this.requestInstall( + files.map(file => ({ + fileName: path.basename(file.path), + filePath: file.path, + })) + ); + }; + + async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) { + const preloadedRequests = requests.filter(req => req.data); + await Promise.all( + requests + .filter(req => !req.data && req.filePath) + .map(req => { + return fse.readFile(req.filePath).then(data => { + req.data = data; + preloadedRequests.push(req); + }).catch(err => { + if (showError) { + Notifications.error(`Error while reading "${req.filePath}": ${String(err)}`); + } + }); + }) + ); + return preloadedRequests as InstallRequestPreloaded[]; + } + + async validatePackage(filePath: string): Promise { + const tarFiles = await listTarEntries(filePath); + + // tarball from npm contains single root folder "package/*" + const rootFolder = tarFiles[0].split("/")[0]; + const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder)); + const manifestLocation = packedInRootFolder ? path.join(rootFolder, manifestFilename) : manifestFilename; + + if (!tarFiles.includes(manifestLocation)) { + throw new Error(`invalid extension bundle, ${manifestFilename} not found`); + } + const manifest = await readFileFromTar({ + tarPath: filePath, + filePath: manifestLocation, + parseJson: true, + }); + if (!manifest.lens && !manifest.renderer) { + throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`); + } + return manifest; + } + + async createTempFilesAndValidate(requests: InstallRequestPreloaded[], { showErrors = true } = {}) { + const validatedRequests: InstallRequestValidated[] = []; + + // copy files to temp + await fse.ensureDir(this.getExtensionPackageTemp()); + requests.forEach(req => { + const tempFile = this.getExtensionPackageTemp(req.fileName); + fse.writeFileSync(tempFile, req.data); + }); + + // validate packages + await Promise.all( + requests.map(async req => { + const tempFile = this.getExtensionPackageTemp(req.fileName); + try { + const manifest = await this.validatePackage(tempFile); + validatedRequests.push({ + ...req, + manifest, + tempFile, + }); + } catch (err) { + fse.unlink(tempFile).catch(() => null); // remove invalid temp package + if (showErrors) { + Notifications.error( +
+

Installing {req.fileName} has failed, skipping.

+

Reason: {String(err)}

+
+ ); + } + } + }) + ); + return validatedRequests; + } + + async requestInstall(requests: InstallRequest[]) { + const preloadedRequests = await this.preloadExtensions(requests); + const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests); + + validatedRequests.forEach(install => { + const { name, version, description } = install.manifest; + const extensionFolder = this.getExtensionDestFolder(name); + const folderExists = fse.existsSync(extensionFolder); + if (!folderExists) { + // auto-install extension if not yet exists + this.unpackExtension(install); + } else { + // otherwise confirmation required (re-install / update) + const removeNotification = Notifications.info( +
+
+

Install extension {name}@{version}?

+

Description: {description}

+
shell.openPath(extensionFolder)}> + Warning: {extensionFolder} will be removed before installation. +
+
+
+ ); + } + }); + } + + async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) { + const extName = `${name}@${version}`; + logger.info(`Unpacking extension ${extName}`, { fileName, tempFile }); + const unpackingTempFolder = path.join(path.dirname(tempFile), path.basename(tempFile) + "-unpacked"); + const extensionFolder = this.getExtensionDestFolder(name); + try { + // extract to temp folder first + await fse.remove(unpackingTempFolder).catch(Function); + await fse.ensureDir(unpackingTempFolder); + await extractTar(tempFile, { cwd: unpackingTempFolder }); + + // move contents to extensions folder + const unpackedFiles = await fse.readdir(unpackingTempFolder); + let unpackedRootFolder = unpackingTempFolder; + if (unpackedFiles.length === 1) { + // check if %extension.tgz was packed with single top folder, + // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball + unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]); + } + await fse.ensureDir(extensionFolder); + await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); + Notifications.ok( +

Extension {extName} successfully installed!

+ ); + } catch (err) { + Notifications.error( +

Installing extension {extName} has failed: {err}

+ ); + } finally { + // clean up + fse.remove(unpackingTempFolder).catch(Function); + fse.unlink(tempFile).catch(Function); + } + } + renderInfo() { return ( -
+

Lens Extension API

The Extensions API in Lens allows users to customize and enhance the Lens experience by creating their own menus or page content that is extended from the existing pages. Many of the core features of Lens are built as extensions and use the same Extension API.
- Extensions loaded from: -
+ +
shell.openPath(this.extensionsPath)}> + {this.extensionsPath} - shell.openPath(this.extensionsPath)} - />
-
- Check out documentation to learn more +
+ + this.downloadUrl = v} + onSubmit={this.addExtensions} + /> +
+
+ +

+ Check out documentation to learn more +

); @@ -95,19 +360,18 @@ export class Extensions extends React.Component { render() { return ( Extensions}> - - this.search = value} - /> -
- {this.renderExtensions()} -
-
+ + + this.search = value} + /> +
+ {this.renderExtensions()} +
+
+
); } diff --git a/src/renderer/components/clipboard/clipboard.scss b/src/renderer/components/clipboard/clipboard.scss new file mode 100644 index 0000000000..1c8e007dba --- /dev/null +++ b/src/renderer/components/clipboard/clipboard.scss @@ -0,0 +1,3 @@ +.Clipboard { + cursor: pointer; +} \ No newline at end of file diff --git a/src/renderer/components/clipboard/clipboard.tsx b/src/renderer/components/clipboard/clipboard.tsx new file mode 100644 index 0000000000..77543ea8f4 --- /dev/null +++ b/src/renderer/components/clipboard/clipboard.tsx @@ -0,0 +1,62 @@ +import "./clipboard.scss"; +import React from "react"; +import { findDOMNode } from "react-dom"; +import { autobind } from "../../../common/utils"; +import { Notifications } from "../notifications"; +import { copyToClipboard } from "../../utils/copyToClipboard"; +import logger from "../../../main/logger"; +import { cssNames } from "../../utils"; + +export interface CopyToClipboardProps { + resetSelection?: boolean; + showNotification?: boolean; + cssSelectorLimit?: string; // allows to copy partial content with css-selector in children-element context + getNotificationMessage?(copiedText: string): React.ReactNode; +} + +export const defaultProps: Partial = { + getNotificationMessage(copiedText: string) { + return

Copied to clipboard: {copiedText}

; + } +}; + +export class Clipboard extends React.Component { + static displayName = "Clipboard"; + static defaultProps = defaultProps as object; + + get rootElem(): HTMLElement { + return findDOMNode(this) as HTMLElement; + } + + get rootReactElem(): React.ReactElement> { + return React.Children.only(this.props.children) as React.ReactElement; + } + + @autobind() + onClick(evt: React.MouseEvent) { + if (this.rootReactElem.props.onClick) { + this.rootReactElem.props.onClick(evt); // pass event to children-root-element if any + } + const { showNotification, resetSelection, getNotificationMessage, cssSelectorLimit } = this.props; + const contentElem = this.rootElem.querySelector(cssSelectorLimit) || this.rootElem; + if (contentElem) { + const { copiedText, copied } = copyToClipboard(contentElem, { resetSelection }); + if (copied && showNotification) { + Notifications.ok(getNotificationMessage(copiedText)); + } + } + } + + render() { + try { + const rootElem = this.rootReactElem; + return React.cloneElement(rootElem, { + className: cssNames(Clipboard.displayName, rootElem.props.className), + onClick: this.onClick, + }); + } catch (err) { + logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) }); + return this.rootReactElem; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/clipboard/index.ts b/src/renderer/components/clipboard/index.ts new file mode 100644 index 0000000000..b711992418 --- /dev/null +++ b/src/renderer/components/clipboard/index.ts @@ -0,0 +1 @@ +export * from "./clipboard"; diff --git a/src/renderer/components/dock/pod-log-controls.tsx b/src/renderer/components/dock/pod-log-controls.tsx index 469264210d..17ad8a2ddf 100644 --- a/src/renderer/components/dock/pod-log-controls.tsx +++ b/src/renderer/components/dock/pod-log-controls.tsx @@ -6,7 +6,7 @@ import { Select, SelectOption } from "../select"; import { Badge } from "../badge"; import { Icon } from "../icon"; import { _i18n } from "../../i18n"; -import { cssNames, downloadFile } from "../../utils"; +import { cssNames, saveFileDialog } from "../../utils"; import { Pod } from "../../api/endpoints"; import { PodLogSearch, PodLogSearchProps } from "./pod-log-search"; @@ -39,7 +39,7 @@ export const PodLogControls = observer((props: Props) => { const downloadLogs = () => { const fileName = selectedContainer ? selectedContainer.name : pod.getName(); - downloadFile(fileName + ".log", logs.join("\n"), "text/plain"); + saveFileDialog(fileName + ".log", logs.join("\n"), "text/plain"); }; const onContainerChange = (option: SelectOption) => { diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index 98ac5ff97a..bc7534d167 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -32,7 +32,7 @@ export class Icon extends React.PureComponent { get isInteractive() { const { interactive, onClick, href, link } = this.props; - return interactive || !!(onClick || href || link); + return interactive ?? !!(onClick || href || link); } @autobind() diff --git a/src/renderer/components/input/drop-file-input.scss b/src/renderer/components/input/drop-file-input.scss new file mode 100644 index 0000000000..3675d5b843 --- /dev/null +++ b/src/renderer/components/input/drop-file-input.scss @@ -0,0 +1,9 @@ +.DropFileInput { + &.droppable { + box-shadow: 0 0 0 5px $primary; // fixme: might not work sometimes + + > * { + pointer-events: none; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/input/drop-file-input.tsx b/src/renderer/components/input/drop-file-input.tsx new file mode 100644 index 0000000000..32ace899d6 --- /dev/null +++ b/src/renderer/components/input/drop-file-input.tsx @@ -0,0 +1,76 @@ +import "./drop-file-input.scss"; +import React from "react"; +import { autobind, cssNames, IClassName } from "../../utils"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import logger from "../../../main/logger"; + +export interface DropFileInputProps extends React.DOMAttributes { + className?: IClassName; + disabled?: boolean; + onDropFiles(files: File[], meta: DropFileMeta): void; +} + +export interface DropFileMeta { + evt: React.DragEvent; +} + +@observer +export class DropFileInput extends React.Component { + @observable dropAreaActive = false; + + @autobind() + onDragEnter() { + this.dropAreaActive = true; + } + + @autobind() + onDragLeave() { + this.dropAreaActive = false; + } + + @autobind() + onDragOver(evt: React.DragEvent) { + if (this.props.onDragOver) { + this.props.onDragOver(evt); + } + evt.preventDefault(); // enable onDrop()-callback + evt.dataTransfer.dropEffect = "move"; + } + + @autobind() + onDrop(evt: React.DragEvent) { + if (this.props.onDrop) { + this.props.onDrop(evt); + } + this.dropAreaActive = false; + const files = Array.from(evt.dataTransfer.files); + if (files.length > 0) { + this.props.onDropFiles(files, { evt }); + } + } + + render() { + const { disabled, className } = this.props; + const { onDragEnter, onDragLeave, onDragOver, onDrop } = this; + try { + const contentElem = React.Children.only(this.props.children) as React.ReactElement>; + const isValidContentElem = React.isValidElement(contentElem); + if (!disabled && isValidContentElem) { + const contentElemProps: React.HTMLProps = { + className: cssNames("DropFileInput", className, { + droppable: this.dropAreaActive, + }), + onDragEnter, + onDragLeave, + onDragOver, + onDrop, + }; + return React.cloneElement(contentElem, contentElemProps); + } + } catch (err) { + logger.error("Invalid root content-element for DropFileInput", { err: String(err) }); + return this.props.children; + } + } +} diff --git a/src/renderer/components/input/index.ts b/src/renderer/components/input/index.ts index fc930e45d2..2e807d0be6 100644 --- a/src/renderer/components/input/index.ts +++ b/src/renderer/components/input/index.ts @@ -2,3 +2,4 @@ export * from './input'; export * from './search-input'; export * from './search-input-url'; export * from './file-input'; +export * from './drop-file-input'; diff --git a/src/renderer/components/input/input.scss b/src/renderer/components/input/input.scss index 31f3c9c46c..b4c3e21703 100644 --- a/src/renderer/components/input/input.scss +++ b/src/renderer/components/input/input.scss @@ -89,6 +89,10 @@ &.theme { &.round-black { + &.invalid label { + border-color: $colorSoftError !important; + } + label { background: $mainBackground; border: 1px solid $borderFaintColor; @@ -107,3 +111,9 @@ } } } + +.Tooltip.InputTooltipError { + --bgc: #{$colorError}; + --border: none; + --color: white; +} diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index 1b4f575b26..cddfcb92eb 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -1,7 +1,7 @@ import "./input.scss"; import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react"; -import { autobind, cssNames, debouncePromise } from "../../utils"; +import { autobind, cssNames, debouncePromise, getRandId } from "../../utils"; import { Icon } from "../icon"; import * as Validators from "./input_validators"; import { InputValidator } from "./input_validators"; @@ -9,6 +9,7 @@ import isString from "lodash/isString"; import isFunction from "lodash/isFunction"; import isBoolean from "lodash/isBoolean"; import uniqueId from "lodash/uniqueId"; +import { Tooltip } from "../tooltip"; const { conditionalValidators, ...InputValidators } = Validators; export { InputValidators, InputValidator }; @@ -25,6 +26,7 @@ export type InputProps = Omit { render() { const { - multiLine, showValidationLine, validators, theme, maxRows, children, + multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip, maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, ...inputProps } = this.props; @@ -292,21 +294,31 @@ export class Input extends React.Component { ref: this.bindRef, spellCheck: "false", }); - + const tooltipId = showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined; + const showErrors = errors.length > 0 && !valid && dirty; + const errorsInfo = ( +
+ {errors.map((error, i) =>

{error}

)} +
+ ); return ( -
-

tbCkLJ( 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 literal 0 HcmV?d00001 diff --git a/docs/extensions/guides/images/hello-world.png b/docs/extensions/guides/images/hello-world.png new file mode 100644 index 0000000000000000000000000000000000000000..1a4a9c73a9dd6a2eb4fce761b36a5abdf7f8112d GIT binary patch literal 63048 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 by$=Ycm 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 TcEa%s;GewABk3H;7asow Date: Tue, 24 Nov 2020 10:28:08 -0500 Subject: [PATCH 4/8] Add designated folder for extensions (#1245) * add store for mapping extension names to filesystem paths - add extension mechinism for getting a folder to save files to - add test to make sure that extensions are being loaded - skip extension loading test Signed-off-by: Sebastian Malton --- .../metrics-cluster-feature/renderer.tsx | 6 +- integration/__tests__/app.tests.ts | 40 ++++++++----- integration/helpers/utils.ts | 8 +-- src/common/vars.ts | 4 +- src/extensions/extension-loader.ts | 20 ++++--- src/extensions/lens-extension.ts | 12 ++++ src/main/extension-filesystem.ts | 57 +++++++++++++++++++ src/main/index.ts | 2 + src/renderer/bootstrap.tsx | 2 + 9 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 src/main/extension-filesystem.ts 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/src/common/vars.ts b/src/common/vars.ts index 1957a6dcff..ab566bb675 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-loader.ts b/src/extensions/extension-loader.ts index 18840da5fc..af0e9d6f86 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -131,21 +131,25 @@ export class ExtensionLoader { 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) { @@ -158,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/lens-extension.ts b/src/extensions/lens-extension.ts index 0dd6980102..1af3300ff0 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) @@ -39,6 +40,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; } 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(), ]); From ca49fb98d9ec67bc35ab793865af652dd0a5dd6e Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 24 Nov 2020 12:41:57 -0500 Subject: [PATCH 5/8] remove @observable.shallow from Lens(M|R)Extension (#1504) Signed-off-by: Sebastian Malton --- src/extensions/lens-main-extension.ts | 2 +- src/extensions/lens-renderer-extension.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) 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