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

|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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)
|
||||||
BIN
docs/extensions/guides/images/hello-lens.png
Normal file
BIN
docs/extensions/guides/images/hello-lens.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/extensions/guides/images/hello-world.png
Normal file
BIN
docs/extensions/guides/images/hello-world.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@ -1,13 +1,16 @@
|
|||||||
# Welcome to Lens support
|
# Support
|
||||||
Here you will find different ways of getting support for Lens.
|
|
||||||
|
|
||||||
## Community Slack Channel
|
Here you will find different ways of getting support for Lens IDE.
|
||||||
We have an active and growing community! Ask a question, see what's being discussed, get insights to up and coming features, help others, join the conversation on our community slack <a href="https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" target="_blank">here</a>
|
|
||||||
|
|
||||||
## Open Source Github Repository
|
## Community Support
|
||||||
Search feature requests, submit an idea, review existing issues, or open a new one at our Github repository <a href="https://github.com/lensapp/lens/issues" target="_blank">here</a>
|
|
||||||
|
|
||||||
## Enterprise Support
|
* [Community Slack](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) - Request for support and help from the Lens community via Slack.
|
||||||
If you are interested in paid support options designed for enterprises to cover Lens usage at scale please see the following links:
|
* [Github Issues](https://github.com/lensapp/lens/issues) - Submit your issues and feature requests to Lens IDE via Github.
|
||||||
|
|
||||||
- <a href="https://www.mirantis.com/support/enterprise-support-services" target="_blank">Mirantis</a>
|
## Commercial Support & Services
|
||||||
|
|
||||||
|
If you are interested in paid support options, professional services or training, please see the offerings from the following vendors:
|
||||||
|
|
||||||
|
* [Mirantis](https://www.mirantis.com/software/lens/) offers commercial support for officially released versions of Lens IDE on MacOS, Windows and Linux operating systems. In addition, Mirantis offers professional services to create proprietary / custom Lens IDE extensions and custom `msi` packaging to meet enterprise IT policies for software distribution and configuration. Training is also available.
|
||||||
|
|
||||||
|
If you'd like to get your business listed in here, please contact us via email [info@k8slens.dev](mailto:info@k8slens.dev)
|
||||||
|
|||||||
@ -10,9 +10,9 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
|
|||||||
Description: () => {
|
Description: () => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
||||||
Install this only if you don't have existing Prometheus stack installed.
|
Install this only if you don't have existing Prometheus stack installed.
|
||||||
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
|
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ jest.setTimeout(60000);
|
|||||||
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
||||||
describe("Lens integration tests", () => {
|
describe("Lens integration tests", () => {
|
||||||
const TEST_NAMESPACE = "integration-tests";
|
const TEST_NAMESPACE = "integration-tests";
|
||||||
|
|
||||||
const BACKSPACE = "\uE003";
|
const BACKSPACE = "\uE003";
|
||||||
|
|
||||||
let app: Application;
|
let app: Application;
|
||||||
|
|
||||||
const appStart = async () => {
|
const appStart = async () => {
|
||||||
@ -37,23 +37,33 @@ describe("Lens integration tests", () => {
|
|||||||
|
|
||||||
const minikubeReady = (): boolean => {
|
const minikubeReady = (): boolean => {
|
||||||
// determine if minikube is running
|
// determine if minikube is running
|
||||||
let status = spawnSync("minikube status", { shell: true });
|
{
|
||||||
if (status.status !== 0) {
|
const { status } = spawnSync("minikube status", { shell: true });
|
||||||
console.warn("minikube not running");
|
if (status !== 0) {
|
||||||
return false;
|
console.warn("minikube not running");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove TEST_NAMESPACE if it already exists
|
// Remove TEST_NAMESPACE if it already exists
|
||||||
status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
{
|
||||||
if (status.status === 0) {
|
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
||||||
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
if (status === 0) {
|
||||||
status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true });
|
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
||||||
if (status.status !== 0) {
|
|
||||||
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`);
|
const { status, stdout, stderr } = spawnSync(
|
||||||
return false;
|
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
||||||
|
{ shell: true },
|
||||||
|
);
|
||||||
|
if (status !== 0) {
|
||||||
|
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(stdout.toString());
|
||||||
}
|
}
|
||||||
console.log(status.stdout.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const ready = minikubeReady();
|
const ready = minikubeReady();
|
||||||
@ -62,8 +72,8 @@ describe("Lens integration tests", () => {
|
|||||||
beforeAll(appStart, 20000);
|
beforeAll(appStart, 20000);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (app && app.isRunning()) {
|
if (app?.isRunning()) {
|
||||||
return util.tearDown(app);
|
await util.tearDown(app);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,8 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
|||||||
|
|
||||||
export function setup(): Application {
|
export function setup(): Application {
|
||||||
return new Application({
|
return new Application({
|
||||||
// path to electron app
|
path: AppPaths[process.platform], // path to electron app
|
||||||
args: [],
|
args: [],
|
||||||
path: AppPaths[process.platform],
|
|
||||||
startTimeout: 30000,
|
startTimeout: 30000,
|
||||||
waitTimeout: 60000,
|
waitTimeout: 60000,
|
||||||
env: {
|
env: {
|
||||||
@ -19,9 +18,10 @@ export function setup(): Application {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AsyncPidGetter = () => Promise<number>;
|
||||||
|
|
||||||
export async function tearDown(app: Application) {
|
export async function tearDown(app: Application) {
|
||||||
const mpid: any = app.mainProcess.pid;
|
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
||||||
const pid = await mpid();
|
|
||||||
await app.stop();
|
await app.stop();
|
||||||
try {
|
try {
|
||||||
process.kill(pid, "SIGKILL");
|
process.kill(pid, "SIGKILL");
|
||||||
|
|||||||
@ -32,6 +32,7 @@ nav:
|
|||||||
- Overview: extensions/guides/README.md
|
- Overview: extensions/guides/README.md
|
||||||
- Main Extension: extensions/guides/main-extension.md
|
- Main Extension: extensions/guides/main-extension.md
|
||||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||||
|
- Generator: extensions/guides/generator.md
|
||||||
- Testing and Publishing:
|
- Testing and Publishing:
|
||||||
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
||||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { defineGlobal } from "./utils/defineGlobal";
|
|||||||
export const isMac = process.platform === "darwin";
|
export const isMac = process.platform === "darwin";
|
||||||
export const isWindows = process.platform === "win32";
|
export const isWindows = process.platform === "win32";
|
||||||
export const isLinux = process.platform === "linux";
|
export const isLinux = process.platform === "linux";
|
||||||
export const isDebugging = process.env.DEBUG === "true";
|
export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
|
||||||
export const isSnap = !!process.env["SNAP"];
|
export const isSnap = !!process.env.SNAP;
|
||||||
export const isProduction = process.env.NODE_ENV === "production";
|
export const isProduction = process.env.NODE_ENV === "production";
|
||||||
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
||||||
export const isDevelopment = !isTestEnv && !isProduction;
|
export const isDevelopment = !isTestEnv && !isProduction;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import path from "path";
|
|||||||
import { getBundledExtensions } from "../common/utils/app-version";
|
import { getBundledExtensions } from "../common/utils/app-version";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||||
|
import { extensionsStore } from "./extensions-store";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
@ -91,7 +92,7 @@ export class ExtensionDiscovery {
|
|||||||
init() {
|
init() {
|
||||||
this.watchExtensions();
|
this.watchExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for added/removed local extensions.
|
* Watches for added/removed local extensions.
|
||||||
* Dependencies are installed automatically after an extension folder is copied.
|
* 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;
|
isBundled?: boolean;
|
||||||
isEnabled?: boolean;
|
|
||||||
} = {}): Promise<InstalledExtension | null> {
|
} = {}): Promise<InstalledExtension | null> {
|
||||||
let manifestJson: LensExtensionManifest;
|
let manifestJson: LensExtensionManifest;
|
||||||
|
let isEnabled: boolean;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// check manifest file for existence
|
// check manifest file for existence
|
||||||
fs.accessSync(manifestPath, fs.constants.F_OK);
|
fs.accessSync(manifestPath, fs.constants.F_OK);
|
||||||
|
|
||||||
manifestJson = __non_webpack_require__(manifestPath);
|
manifestJson = __non_webpack_require__(manifestPath);
|
||||||
|
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
|
||||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
||||||
|
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
|
manifestPath: installedManifestPath,
|
||||||
manifest: manifestJson,
|
manifest: manifestJson,
|
||||||
isBundled,
|
isBundled,
|
||||||
isEnabled,
|
isEnabled
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson });
|
logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson });
|
||||||
@ -316,13 +319,12 @@ export class ExtensionDiscovery {
|
|||||||
/**
|
/**
|
||||||
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
||||||
*/
|
*/
|
||||||
async loadExtensionFromPath(absPath: string, { isBundled = false, isEnabled = isBundled }: {
|
async loadExtensionFromPath(absPath: string, { isBundled = false }: {
|
||||||
isBundled?: boolean;
|
isBundled?: boolean;
|
||||||
isEnabled?: boolean;
|
|
||||||
} = {}): Promise<InstalledExtension | null> {
|
} = {}): Promise<InstalledExtension | null> {
|
||||||
const manifestPath = path.resolve(absPath, manifestFilename);
|
const manifestPath = path.resolve(absPath, manifestFilename);
|
||||||
|
|
||||||
return this.getByManifest(manifestPath, { isBundled, isEnabled });
|
return this.getByManifest(manifestPath, { isBundled });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { app, ipcRenderer, remote } from "electron";
|
import { app, ipcRenderer, remote } from "electron";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { getHostedCluster } from "../common/cluster-store";
|
||||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "./extension-discovery";
|
||||||
@ -94,14 +95,14 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
logger.info(`${logModule}: load on main`);
|
logger.info(`${logModule}: load on main`);
|
||||||
this.autoInitExtensions((ext: LensMainExtension) => [
|
this.autoInitExtensions(async (ext: LensMainExtension) => [
|
||||||
registries.menuRegistry.add(ext.appMenus)
|
registries.menuRegistry.add(ext.appMenus)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterManagerRenderer() {
|
loadOnClusterManagerRenderer() {
|
||||||
logger.info(`${logModule}: load on main renderer (cluster manager)`);
|
logger.info(`${logModule}: load on main renderer (cluster manager)`);
|
||||||
this.autoInitExtensions((ext: LensRendererExtension) => [
|
this.autoInitExtensions(async (ext: LensRendererExtension) => [
|
||||||
registries.globalPageRegistry.add(ext.globalPages, ext),
|
registries.globalPageRegistry.add(ext.globalPages, ext),
|
||||||
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
|
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
|
||||||
registries.appPreferenceRegistry.add(ext.appPreferences),
|
registries.appPreferenceRegistry.add(ext.appPreferences),
|
||||||
@ -112,33 +113,43 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
loadOnClusterRenderer() {
|
loadOnClusterRenderer() {
|
||||||
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
||||||
this.autoInitExtensions((ext: LensRendererExtension) => [
|
const cluster = getHostedCluster();
|
||||||
registries.clusterPageRegistry.add(ext.clusterPages, ext),
|
this.autoInitExtensions(async (ext: LensRendererExtension) => {
|
||||||
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
|
if (await ext.isEnabledForCluster(cluster) === false) {
|
||||||
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
|
return [];
|
||||||
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
|
}
|
||||||
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
|
return [
|
||||||
]);
|
registries.clusterPageRegistry.add(ext.clusterPages, ext),
|
||||||
|
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
|
||||||
|
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
|
||||||
|
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
|
||||||
|
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
|
||||||
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
|
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
|
||||||
return reaction(() => this.toJSON(), installedExtensions => {
|
return reaction(() => this.toJSON(), installedExtensions => {
|
||||||
for (const [extId, ext] of installedExtensions) {
|
for (const [extId, ext] of installedExtensions) {
|
||||||
let instance = this.instances.get(extId);
|
const alreadyInit = this.instances.has(extId);
|
||||||
if (ext.isEnabled && !instance) {
|
|
||||||
|
if (ext.isEnabled && !alreadyInit) {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext);
|
const LensExtensionClass = this.requireExtension(ext);
|
||||||
if (!LensExtensionClass) continue;
|
if (!LensExtensionClass) {
|
||||||
instance = new LensExtensionClass(ext);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new LensExtensionClass(ext);
|
||||||
instance.whenEnabled(() => register(instance));
|
instance.whenEnabled(() => register(instance));
|
||||||
instance.enable();
|
instance.enable();
|
||||||
this.instances.set(extId, instance);
|
this.instances.set(extId, instance);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`${logModule}: activation extension error`, { ext, err });
|
logger.error(`${logModule}: activation extension error`, { ext, err });
|
||||||
}
|
}
|
||||||
} else if (!ext.isEnabled && instance) {
|
} else if (!ext.isEnabled && alreadyInit) {
|
||||||
logger.info(`${logModule} deleting extension ${extId}`);
|
|
||||||
try {
|
try {
|
||||||
|
const instance = this.instances.get(extId);
|
||||||
instance.disable();
|
instance.disable();
|
||||||
this.instances.delete(extId);
|
this.instances.delete(extId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -151,7 +162,7 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected requireExtension(extension: InstalledExtension) {
|
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
|
||||||
let extEntrypoint = "";
|
let extEntrypoint = "";
|
||||||
try {
|
try {
|
||||||
if (ipcRenderer && extension.manifest.renderer) {
|
if (ipcRenderer && extension.manifest.renderer) {
|
||||||
|
|||||||
@ -47,11 +47,6 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
await extensionLoader.whenLoaded;
|
await extensionLoader.whenLoaded;
|
||||||
await this.whenLoaded;
|
await this.whenLoaded;
|
||||||
|
|
||||||
// activate user-extensions when state is ready
|
|
||||||
extensionLoader.userExtensions.forEach((ext, extId) => {
|
|
||||||
ext.isEnabled = this.isEnabled(extId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// apply state on changes from store
|
// apply state on changes from store
|
||||||
reaction(() => this.state.toJS(), extensionsState => {
|
reaction(() => this.state.toJS(), extensionsState => {
|
||||||
extensionsState.forEach((state, extId) => {
|
extensionsState.forEach((state, extId) => {
|
||||||
@ -70,7 +65,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
isEnabled(extId: LensExtensionId) {
|
isEnabled(extId: LensExtensionId) {
|
||||||
const state = this.state.get(extId);
|
const state = this.state.get(extId);
|
||||||
return !state /* enabled by default */ || state.enabled;
|
return state && state.enabled; // by default false
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "./extension-discovery";
|
||||||
import { action, observable, reaction } from "mobx";
|
import { action, observable, reaction } from "mobx";
|
||||||
|
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
|
||||||
export type LensExtensionId = string; // path to manifest (package.json)
|
export type LensExtensionId = string; // path to manifest (package.json)
|
||||||
@ -40,6 +41,17 @@ export class LensExtension {
|
|||||||
return this.manifest.version;
|
return this.manifest.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getExtensionFileFolder returns the path to an already created folder. This
|
||||||
|
* folder is for the sole use of this extension.
|
||||||
|
*
|
||||||
|
* Note: there is no security done on this folder, only obfiscation of the
|
||||||
|
* folder name.
|
||||||
|
*/
|
||||||
|
async getExtensionFileFolder(): Promise<string> {
|
||||||
|
return filesystemProvisionerStore.requestDirectory(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
get description() {
|
get description() {
|
||||||
return this.manifest.description;
|
return this.manifest.description;
|
||||||
}
|
}
|
||||||
@ -68,15 +80,16 @@ export class LensExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async whenEnabled(handlers: () => Function[]) {
|
async whenEnabled(handlers: () => Promise<Function[]>) {
|
||||||
const disposers: Function[] = [];
|
const disposers: Function[] = [];
|
||||||
const unregisterHandlers = () => {
|
const unregisterHandlers = () => {
|
||||||
disposers.forEach(unregister => unregister());
|
disposers.forEach(unregister => unregister());
|
||||||
disposers.length = 0;
|
disposers.length = 0;
|
||||||
};
|
};
|
||||||
const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
|
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
disposers.push(...handlers());
|
const handlerDisposers = await handlers();
|
||||||
|
disposers.push(...handlerDisposers);
|
||||||
} else {
|
} else {
|
||||||
unregisterHandlers();
|
unregisterHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { WindowManager } from "../main/window-manager";
|
|||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
@observable.shallow appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
||||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
|
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
|
||||||
|
import type { Cluster } from "../main/cluster";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
@observable.shallow globalPages: PageRegistration[] = [];
|
globalPages: PageRegistration[] = [];
|
||||||
@observable.shallow clusterPages: PageRegistration[] = [];
|
clusterPages: PageRegistration[] = [];
|
||||||
@observable.shallow globalPageMenus: PageMenuRegistration[] = [];
|
globalPageMenus: PageMenuRegistration[] = [];
|
||||||
@observable.shallow clusterPageMenus: PageMenuRegistration[] = [];
|
clusterPageMenus: PageMenuRegistration[] = [];
|
||||||
@observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||||
@observable.shallow appPreferences: AppPreferenceRegistration[] = [];
|
appPreferences: AppPreferenceRegistration[] = [];
|
||||||
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = [];
|
clusterFeatures: ClusterFeatureRegistration[] = [];
|
||||||
@observable.shallow statusBarItems: StatusBarRegistration[] = [];
|
statusBarItems: StatusBarRegistration[] = [];
|
||||||
@observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||||
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
@ -24,4 +26,11 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
});
|
});
|
||||||
navigate(pageUrl);
|
navigate(pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines if extension is enabled for a given cluster. Defaults to `true`.
|
||||||
|
*/
|
||||||
|
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/main/extension-filesystem.ts
Normal file
57
src/main/extension-filesystem.ts
Normal 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>();
|
||||||
@ -25,6 +25,7 @@ import { extensionsStore } from "../extensions/extensions-store";
|
|||||||
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
|
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
|
||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
|
import { filesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -59,6 +60,7 @@ app.on("ready", async () => {
|
|||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
workspaceStore.load(),
|
workspaceStore.load(),
|
||||||
extensionsStore.load(),
|
extensionsStore.load(),
|
||||||
|
filesystemProvisionerStore.load(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// find free port
|
// find free port
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { i18nStore } from "./i18n";
|
|||||||
import { themeStore } from "./theme.store";
|
import { themeStore } from "./theme.store";
|
||||||
import { extensionsStore } from "../extensions/extensions-store";
|
import { extensionsStore } from "../extensions/extensions-store";
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
|
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
|
|
||||||
type AppComponent = React.ComponentType & {
|
type AppComponent = React.ComponentType & {
|
||||||
init?(): Promise<void>;
|
init?(): Promise<void>;
|
||||||
@ -39,6 +40,7 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
workspaceStore.load(),
|
workspaceStore.load(),
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
extensionsStore.load(),
|
extensionsStore.load(),
|
||||||
|
filesystemProvisionerStore.load(),
|
||||||
i18nStore.init(),
|
i18nStore.init(),
|
||||||
themeStore.init(),
|
themeStore.init(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -32,7 +32,6 @@
|
|||||||
border: 1px solid var(--drawerSubtitleBackground);
|
border: 1px solid var(--drawerSubtitleBackground);
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@include custom-scrollbar();
|
|
||||||
|
|
||||||
.TableHead {
|
.TableHead {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
.Badge {
|
.Badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
padding: $padding $spacing;
|
padding: $padding $spacing;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export class Preferences extends React.Component {
|
|||||||
formatOptionLabel={this.formatHelmOptionLabel}
|
formatOptionLabel={this.formatHelmOptionLabel}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
/>
|
/>
|
||||||
<div className="repos flex gaps column">
|
<div className="repos flex column">
|
||||||
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
|
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
|
||||||
const tooltipId = `message-${name}`;
|
const tooltipId = `message-${name}`;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
@include custom-scrollbar;
|
overflow: auto;
|
||||||
margin-top: $spacing;
|
margin-top: $spacing;
|
||||||
padding: $spacing * 2;
|
padding: $spacing * 2;
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,6 @@
|
|||||||
|
|
||||||
.theme-light & {
|
.theme-light & {
|
||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
|
|
||||||
.ace_scrollbar {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .editor {
|
> .editor {
|
||||||
@ -51,8 +47,4 @@
|
|||||||
.ace_comment {
|
.ace_comment {
|
||||||
color: #808080;
|
color: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_scrollbar {
|
|
||||||
@include custom-scrollbar;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,15 +1,6 @@
|
|||||||
@import "~flex.box";
|
@import "~flex.box";
|
||||||
@import "fonts";
|
@import "fonts";
|
||||||
|
|
||||||
*, *:before, *:after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: 0;
|
|
||||||
outline: none;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--unit: 8px;
|
--unit: 8px;
|
||||||
--padding: var(--unit);
|
--padding: var(--unit);
|
||||||
@ -27,6 +18,33 @@
|
|||||||
--drag-region-height: 22px
|
--drag-region-height: 22px
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 16px;
|
||||||
|
height: 15px; // Align sizes visually
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollBarColor);
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-right-width: 5px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
.Dialog {
|
.Dialog {
|
||||||
@include custom-scrollbar;
|
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -11,6 +9,7 @@
|
|||||||
padding: $unit * 5;
|
padding: $unit * 5;
|
||||||
z-index: $zIndex-dialog;
|
z-index: $zIndex-dialog;
|
||||||
overscroll-behavior: none; // prevent swiping with touch-pad on MacOSX
|
overscroll-behavior: none; // prevent swiping with touch-pad on MacOSX
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
&.modal {
|
&.modal {
|
||||||
background: transparentize(#222, .5);
|
background: transparentize(#222, .5);
|
||||||
|
|||||||
@ -3,12 +3,7 @@
|
|||||||
--overlay-active-bg: orange;
|
--overlay-active-bg: orange;
|
||||||
|
|
||||||
.logs {
|
.logs {
|
||||||
@include custom-scrollbar;
|
|
||||||
|
|
||||||
// fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight`
|
|
||||||
// `overflow: overlay` don't allow scroll to the last line
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
color: $textColorAccent;
|
color: $textColorAccent;
|
||||||
background: $logsBackground;
|
background: $logsBackground;
|
||||||
|
|||||||
@ -5,17 +5,7 @@
|
|||||||
margin-left: $padding * 2;
|
margin-left: $padding * 2;
|
||||||
margin-top: $padding * 2;
|
margin-top: $padding * 2;
|
||||||
|
|
||||||
.theme-light & {
|
|
||||||
.xterm-viewport {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .xterm {
|
> .xterm {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-viewport {
|
|
||||||
@include custom-scrollbar;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -10,12 +10,6 @@
|
|||||||
box-shadow: 0 0 $unit * 2 $boxShadow;
|
box-shadow: 0 0 $unit * 2 $boxShadow;
|
||||||
z-index: $zIndex-drawer;
|
z-index: $zIndex-drawer;
|
||||||
|
|
||||||
.theme-light & {
|
|
||||||
.drawer-content {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
@ -71,11 +65,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.drawer-content {
|
.drawer-content {
|
||||||
@include custom-scrollbar;
|
overflow: auto;
|
||||||
|
padding: var(--spacing);
|
||||||
> *:not(.Spinner) {
|
|
||||||
padding: var(--spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Table .TableHead {
|
.Table .TableHead {
|
||||||
background-color: $contentColor;
|
background-color: $contentColor;
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .content-wrapper {
|
> .content-wrapper {
|
||||||
@include custom-scrollbar-themed;
|
overflow: auto;
|
||||||
padding: $spacing * 2;
|
padding: $spacing * 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -19,11 +19,7 @@
|
|||||||
|
|
||||||
&.pinned {
|
&.pinned {
|
||||||
.sidebar-nav {
|
.sidebar-nav {
|
||||||
@include custom-scrollbar;
|
overflow: auto;
|
||||||
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,8 +59,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-nav {
|
.sidebar-nav {
|
||||||
@include hidden-scrollbar;
|
|
||||||
|
|
||||||
padding: $padding / 1.5 0;
|
padding: $padding / 1.5 0;
|
||||||
|
|
||||||
.Icon {
|
.Icon {
|
||||||
|
|||||||
@ -9,13 +9,8 @@
|
|||||||
|
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@include custom-scrollbar;
|
|
||||||
$spacing: $margin * 2;
|
$spacing: $margin * 2;
|
||||||
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
overflow-y: scroll; // always reserve space for scrollbar (17px)
|
overflow-y: scroll; // always reserve space for scrollbar (17px)
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@ -9,8 +9,8 @@
|
|||||||
grid-template-columns: 1fr 40%;
|
grid-template-columns: 1fr 40%;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@include custom-scrollbar-themed;
|
|
||||||
--flex-gap: #{$spacing};
|
--flex-gap: #{$spacing};
|
||||||
|
overflow: auto;
|
||||||
padding: $spacing;
|
padding: $spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,8 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
&.light {
|
pre, table {
|
||||||
pre, table {
|
overflow: auto;
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-c {
|
.pl-c {
|
||||||
@ -513,7 +511,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@include custom-scrollbar;
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -581,13 +578,12 @@
|
|||||||
|
|
||||||
.highlight pre,
|
.highlight pre,
|
||||||
pre {
|
pre {
|
||||||
@include custom-scrollbar;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: $helmDescriptionPreBackground;
|
background-color: $helmDescriptionPreBackground;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: auto !important;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import React, { Component } from "react";
|
|||||||
import marked from "marked";
|
import marked from "marked";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { themeStore } from "../../theme.store";
|
|
||||||
|
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
|
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
|
||||||
// Set all elements owning target to target=_blank
|
// Set all elements owning target to target=_blank
|
||||||
@ -29,7 +28,7 @@ export class MarkdownViewer extends Component<Props> {
|
|||||||
const html = DOMPurify.sanitize(marked(markdown));
|
const html = DOMPurify.sanitize(marked(markdown));
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssNames("MarkDownViewer", className, themeStore.activeTheme.type)}
|
className={cssNames("MarkDownViewer", className)}
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,52 +6,6 @@
|
|||||||
@import "table/table.mixins";
|
@import "table/table.mixins";
|
||||||
@import "+network/network-mixins";
|
@import "+network/network-mixins";
|
||||||
|
|
||||||
// todo: re-use in other places with theming
|
|
||||||
@mixin custom-scrollbar-themed($invert: false) {
|
|
||||||
@if ($invert) {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(light);
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// fits better with dark background
|
|
||||||
@include custom-scrollbar(light);
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin custom-scrollbar($theme: light, $size: 7px, $borderSpacing: 5px) {
|
|
||||||
$themes: (
|
|
||||||
light: #5f6064,
|
|
||||||
dark: #bbb,
|
|
||||||
);
|
|
||||||
|
|
||||||
$scrollBarColor: if(map_has_key($themes, $theme), map_get($themes, $theme), none);
|
|
||||||
$scrollBarSize: calc(#{$size} + #{$borderSpacing} * 2);
|
|
||||||
|
|
||||||
overflow: auto; // allow scrolling for container
|
|
||||||
|
|
||||||
// Webkit
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: $scrollBarSize;
|
|
||||||
height: $scrollBarSize;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: $scrollBarColor;
|
|
||||||
background-clip: padding-box;
|
|
||||||
border: $borderSpacing solid transparent;
|
|
||||||
border-radius: $scrollBarSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-corner {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide scrollbar but keep the element scrollable
|
// Hide scrollbar but keep the element scrollable
|
||||||
@mixin hidden-scrollbar {
|
@mixin hidden-scrollbar {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@ -75,7 +75,6 @@ html {
|
|||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
@include custom-scrollbar;
|
|
||||||
padding-right: 1px;
|
padding-right: 1px;
|
||||||
padding-left: 1px;
|
padding-left: 1px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@ -152,10 +151,6 @@ html {
|
|||||||
--select-option-selected-bgc: $textColorSecondary;
|
--select-option-selected-bgc: $textColorSecondary;
|
||||||
|
|
||||||
.Select {
|
.Select {
|
||||||
&__menu-list {
|
|
||||||
@include custom-scrollbar($theme: dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__multi-value {
|
&__multi-value {
|
||||||
background: none;
|
background: none;
|
||||||
box-shadow: 0 0 0 1px $textColorSecondary;
|
box-shadow: 0 0 0 1px $textColorSecondary;
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
.Table {
|
.Table {
|
||||||
&.scrollable {
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include custom-scrollbar();
|
|
||||||
flex: 1 0 0; // hackfix: flex-basis must be "0" for proper work in firefox
|
|
||||||
}
|
|
||||||
|
|
||||||
&.autoSize {
|
&.autoSize {
|
||||||
.TableCell {
|
.TableCell {
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.scrollable {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&.selectable {
|
&.selectable {
|
||||||
.TableHead, .TableRow {
|
.TableHead, .TableRow {
|
||||||
padding: 0 $padding;
|
padding: 0 $padding;
|
||||||
|
|||||||
@ -2,12 +2,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> .list {
|
> .list {
|
||||||
@include custom-scrollbar;
|
overflow-y: overlay!important;
|
||||||
|
|
||||||
.theme-light & {
|
|
||||||
@include custom-scrollbar(dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
overflow-y: overlay !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin scrollableContent() {
|
@mixin scrollableContent() {
|
||||||
@include custom-scrollbar($theme: dark);
|
overflow: auto;
|
||||||
padding: var(--wizard-spacing);
|
padding: var(--wizard-spacing);
|
||||||
height: var(--wizard-content-height);
|
height: var(--wizard-content-height);
|
||||||
max-height: var(--wizard-content-max-height);
|
max-height: var(--wizard-content-max-height);
|
||||||
|
|||||||
@ -107,6 +107,7 @@
|
|||||||
"selectOptionHoveredColor": "#87909c",
|
"selectOptionHoveredColor": "#87909c",
|
||||||
"lineProgressBackground": "#414448",
|
"lineProgressBackground": "#414448",
|
||||||
"radioActiveBackground": "#36393e",
|
"radioActiveBackground": "#36393e",
|
||||||
"menuActiveBackground": "#36393e"
|
"menuActiveBackground": "#36393e",
|
||||||
|
"scrollBarColor": "#5f6064"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,13 +39,13 @@
|
|||||||
"helmImgBackground": "#e8e8e8",
|
"helmImgBackground": "#e8e8e8",
|
||||||
"helmStableRepo": "#3d90ce",
|
"helmStableRepo": "#3d90ce",
|
||||||
"helmIncubatorRepo": "#ff7043",
|
"helmIncubatorRepo": "#ff7043",
|
||||||
"helmDescriptionHr": "#41474a",
|
"helmDescriptionHr": "#dddddd",
|
||||||
"helmDescriptionBlockqouteColor": "#555555",
|
"helmDescriptionBlockqouteColor": "#555555",
|
||||||
"helmDescriptionBlockqouteBorder": "#8a8f93",
|
"helmDescriptionBlockqouteBorder": "#8a8f93",
|
||||||
"helmDescriptionBlockquoteBackground": "#eeeeee",
|
"helmDescriptionBlockquoteBackground": "#eeeeee",
|
||||||
"helmDescriptionHeaders": "#3e4147",
|
"helmDescriptionHeaders": "#3e4147",
|
||||||
"helmDescriptionH6": "#6a737d",
|
"helmDescriptionH6": "#6a737d",
|
||||||
"helmDescriptionTdBorder": "#47494a",
|
"helmDescriptionTdBorder": "#c6c6c6",
|
||||||
"helmDescriptionTrBackground": "#1c2125",
|
"helmDescriptionTrBackground": "#1c2125",
|
||||||
"helmDescriptionCodeBackground": "#ffffff1a",
|
"helmDescriptionCodeBackground": "#ffffff1a",
|
||||||
"helmDescriptionPreBackground": "#eeeeee",
|
"helmDescriptionPreBackground": "#eeeeee",
|
||||||
@ -108,6 +108,7 @@
|
|||||||
"selectOptionHoveredColor": "#ffffff",
|
"selectOptionHoveredColor": "#ffffff",
|
||||||
"lineProgressBackground": "#e8e8e8",
|
"lineProgressBackground": "#e8e8e8",
|
||||||
"radioActiveBackground": "#f1f1f1",
|
"radioActiveBackground": "#f1f1f1",
|
||||||
"menuActiveBackground": "#e8e8e8"
|
"menuActiveBackground": "#e8e8e8",
|
||||||
|
"scrollBarColor": "#bbbbbb"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user