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

Merge branch 'master' into tweak_extension_install_1277

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

View File

@ -1,5 +1,11 @@
# Your First Extension
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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,13 +1,16 @@
# Welcome to Lens support
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 <a href="https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" target="_blank">here</a>
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 <a href="https://github.com/lensapp/lens/issues" target="_blank">here</a>
## 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:
- <a href="https://www.mirantis.com/support/enterprise-support-services" target="_blank">Mirantis</a>
* [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)

View File

@ -10,9 +10,9 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
Description: () => {
return (
<span>
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 <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
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 <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span>
);
}

View File

@ -16,8 +16,8 @@ jest.setTimeout(60000);
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
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);
}
});

View File

@ -8,9 +8,8 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
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<number>;
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");

View File

@ -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

View File

@ -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;

View File

@ -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<InstalledExtension | null> {
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<InstalledExtension | null> {
const manifestPath = path.resolve(absPath, manifestFilename);
return this.getByManifest(manifestPath, { isBundled, isEnabled });
return this.getByManifest(manifestPath, { isBundled });
}
}

View File

@ -1,6 +1,7 @@
import { app, ipcRenderer, remote } from "electron";
import { 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<Function[]>) {
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) {

View File

@ -47,11 +47,6 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
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<LensExtensionsStoreModel> {
isEnabled(extId: LensExtensionId) {
const state = this.state.get(extId);
return !state /* enabled by default */ || state.enabled;
return state && state.enabled; // by default false
}
@action

View File

@ -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<string> {
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<Function[]>) {
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();
}

View File

@ -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<P extends object>(pageId?: string, params?: P, frameId?: number) {
const windowManager = WindowManager.getInstance<WindowManager>();

View File

@ -1,19 +1,21 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
import type { 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<P extends object>(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<Boolean> {
return true;
}
}

View File

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

View File

@ -25,6 +25,7 @@ import { extensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
import 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

View File

@ -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<void>;
@ -39,6 +40,7 @@ export async function bootstrap(App: AppComponent) {
workspaceStore.load(),
clusterStore.load(),
extensionsStore.load(),
filesystemProvisionerStore.load(),
i18nStore.init(),
themeStore.init(),
]);

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

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

View File

@ -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 {

View File

@ -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;

View File

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

View File

@ -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 {

View File

@ -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<Props> {
const html = DOMPurify.sanitize(marked(markdown));
return (
<div
className={cssNames("MarkDownViewer", className, themeStore.activeTheme.type)}
className={cssNames("MarkDownViewer", className)}
dangerouslySetInnerHTML={{ __html: html }}
/>
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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);

View File

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

View File

@ -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"
}
}