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

Extension support page (#1112)

Signed-off-by: Roman <ixrock@gmail.com>
Co-authored-by: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com>
This commit is contained in:
Roman 2020-10-24 09:24:54 +03:00 committed by GitHub
parent ce995f3deb
commit f3a0059355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 4511 additions and 553 deletions

2
.gitignore vendored
View File

@ -12,6 +12,6 @@ binaries/client/
binaries/server/ binaries/server/
src/extensions/*/*.js src/extensions/*/*.js
src/extensions/*/*.d.ts src/extensions/*/*.d.ts
src/extensions/example-extension/src/**
types/extension-api.d.ts types/extension-api.d.ts
types/extension-renderer-api.d.ts types/extension-renderer-api.d.ts
extensions/*/dist

View File

@ -10,7 +10,7 @@ export default class ExampleExtension extends LensRendererExtension {
registerPages(registry: Registry.PageRegistry) { registerPages(registry: Registry.PageRegistry) {
this.disposers.push( this.disposers.push(
registry.add({ registry.add({
type: Registry.DynamicPageType.CLUSTER, type: Registry.PageRegistryType.CLUSTER,
path: "/extension-example", path: "/extension-example",
title: "Example Extension", title: "Example Extension",
components: { components: {

View File

@ -0,0 +1,23 @@
import { LensMainExtension, Registry, windowManager } from "@k8slens/extensions";
import { supportPageURL } from "./src/support.route";
export default class SupportPageMainExtension extends LensMainExtension {
async onActivate() {
console.log("support page extension activated")
}
async registerAppMenus(registry: Registry.MenuRegistry) {
this.disposers.push(
registry.add({
parentId: "help",
label: "Support",
click() {
windowManager.navigate({
channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR
url: supportPageURL(),
});
}
})
)
}
}

3623
extensions/support-page/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "lens-support-page",
"version": "0.1.0",
"description": "Lens support page",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^14.11.11",
"@types/react": "^16.9.53",
"@types/react-router": "^5.1.8",
"@types/webpack": "^4.41.17",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -0,0 +1,37 @@
import React from "react";
import { Component, LensRendererExtension, Navigation, Registry } from "@k8slens/extensions";
import { supportPageRoute, supportPageURL } from "./src/support.route";
import { Support } from "./src/support";
export default class SupportPageRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("support page extension activated")
}
registerPages(registry: Registry.PageRegistry) {
this.disposers.push(
registry.add({
...supportPageRoute,
type: Registry.PageRegistryType.GLOBAL,
url: supportPageURL(),
components: {
Page: Support,
}
})
)
}
registerStatusBarIcon(registry: Registry.StatusBarRegistry) {
this.disposers.push(
registry.add({
icon: (
<Component.Icon
material="support"
tooltip="Support"
onClick={() => Navigation.navigate(supportPageURL())}
/>
)
})
)
}
}

View File

@ -0,0 +1,7 @@
import type { RouteProps } from "react-router";
export const supportPageRoute: RouteProps = {
path: "/support"
}
export const supportPageURL = () => supportPageRoute.path.toString();

View File

@ -0,0 +1,29 @@
// TODO: figure out how to consume styles / handle import "./support.scss"
// TODO: support localization / figure out how to extract / consume i18n strings
import React from "react"
import { observer } from "mobx-react"
import { CommonVars, Component } from "@k8slens/extensions";
@observer
export class Support extends React.Component {
render() {
const { PageLayout } = Component;
const { slackUrl, issuesTrackerUrl } = CommonVars;
return (
<PageLayout showOnTop className="Support" header={<h2>Support</h2>}>
<h2>Community Slack Channel</h2>
<p>
Ask a question, see what's being discussed, join the conversation <a href={slackUrl} target="_blank">here</a>
</p>
<h2>Report an Issue</h2>
<p>
Review existing issues or open a new one <a href={issuesTrackerUrl} target="_blank">here</a>
</p>
{/*<h2><Trans>Commercial Support</Trans></h2>*/}
</PageLayout>
);
}
}

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react",
"paths": {
"*": [
"node_modules/*",
"../../types/*"
]
}
},
"include": [
"renderer.ts",
"../../src/extensions/npm/**/*.d.ts",
"src/**/*"
]
}

View File

@ -0,0 +1,68 @@
import path from "path"
const outputPath = path.resolve(__dirname, 'dist');
// TODO: figure out how to share base TS and Webpack configs from Lens (npm, filesystem, etc?)
const lensExternals = {
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact",
};
export default [
{
entry: './main.ts',
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
lensExternals,
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'main.js',
path: outputPath,
},
},
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
lensExternals,
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: outputPath,
},
},
];

View File

@ -9,8 +9,8 @@
"styles": [] "styles": []
}, },
"scripts": { "scripts": {
"build": "webpack --config webpack.config.js", "build": "webpack -p",
"dev": "npm run build --watch" "dev": "webpack --watch"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {

View File

@ -26,5 +26,5 @@
"renderer.ts", "renderer.ts",
"../../src/extensions/npm/**/*.d.ts", "../../src/extensions/npm/**/*.d.ts",
"src/**/*" "src/**/*"
], ]
} }

View File

@ -176,7 +176,8 @@
"extensions": [ "extensions": [
"telemetry", "telemetry",
"pod-menu", "pod-menu",
"node-menu" "node-menu",
"support-page"
] ]
}, },
"dependencies": { "dependencies": {
@ -314,7 +315,6 @@
"jest": "^26.0.1", "jest": "^26.0.1",
"jest-mock-extended": "^1.0.10", "jest-mock-extended": "^1.0.10",
"make-plural": "^6.2.1", "make-plural": "^6.2.1",
"material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"mobx-react": "^6.2.2", "mobx-react": "^6.2.2",
"moment": "^2.26.0", "moment": "^2.26.0",

View File

@ -21,11 +21,6 @@ export const rendererDir = path.join(contextDir, "src/renderer");
export const htmlTemplate = path.resolve(rendererDir, "template.html"); export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
// Extensions
export const extensionsLibName = `${appName}-extensions.api`
export const extensionsRendererLibName = `${appName}-extensions-renderer.api`
export const extensionsDir = path.join(contextDir, "src/extensions");
// Special runtime paths // Special runtime paths
defineGlobal("__static", { defineGlobal("__static", {
get() { get() {

View File

@ -1,27 +0,0 @@
import { observable } from "mobx"
import React from "react"
export interface AppPreferenceComponents {
Hint: React.ComponentType<any>;
Input: React.ComponentType<any>;
}
export interface AppPreferenceRegistration {
title: string;
components: AppPreferenceComponents;
}
export class AppPreferenceRegistry {
preferences = observable.array<AppPreferenceRegistration>([], { deep: false });
add(preference: AppPreferenceRegistration) {
this.preferences.push(preference)
return () => {
this.preferences.replace(
this.preferences.filter(c => c !== preference)
)
};
}
}
export const appPreferenceRegistry = new AppPreferenceRegistry()

View File

@ -0,0 +1,22 @@
// Lens-extensions api developer's kit
export * from "../lens-main-extension"
export * from "../lens-renderer-extension"
import type { WindowManager } from "../../main/window-manager";
// APIs
import * as EventBus from "./event-bus"
import * as Store from "./stores"
import * as Util from "./utils"
import * as Registry from "../registries"
import * as CommonVars from "../../common/vars";
export let windowManager: WindowManager;
export {
EventBus,
Store,
Util,
Registry,
CommonVars,
}

View File

@ -1,4 +0,0 @@
export type { DynamicPageType, PageRegistry } from "../page-registry"
export type { AppPreferenceRegistry } from "../app-preference-registry"
export type { KubeObjectMenuRegistry } from "../../renderer/api/kube-object-menu-registry"
export type { KubeObjectDetailRegistry } from "../../renderer/api/kube-object-detail-registry"

View File

@ -1,17 +0,0 @@
// Lens-extensions api developer's kit
export type { LensExtensionRuntimeEnv } from "./lens-runtime";
export * from "./lens-main-extension"
export * from "./lens-renderer-extension"
// APIs
import * as EventBus from "./core-api/event-bus"
import * as Store from "./core-api/stores"
import * as Util from "./core-api/utils"
import * as Registry from "./core-api/registries"
export {
EventBus,
Registry,
Store,
Util
}

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { cssNames } from "../renderer/utils"; import { cssNames } from "../renderer/utils";
import { TabLayout } from "../renderer/components/layout/tab-layout"; import { TabLayout } from "../renderer/components/layout/tab-layout";
import { PageRegistration } from "./page-registry" import { PageRegistration } from "./registries/page-registry"
export class DynamicPage extends React.Component<{ page: PageRegistration }> { export class DynamicPage extends React.Component<{ page: PageRegistration }> {
render() { render() {

View File

@ -1,2 +1,4 @@
export * from "./core-extension-api" // Extension-api types generation bundle (used by rollup.js)
export * from "./renderer-extension-api"
export * from "./core-api"
export * from "./renderer-api"

View File

@ -1,14 +1,12 @@
import type { ExtensionId, LensExtension, ExtensionManifest, ExtensionModel } from "./lens-extension" import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
import type { LensMainExtension } from "./lens-main-extension"
import type { LensRendererExtension } from "./lens-renderer-extension" import type { LensRendererExtension } from "./lens-renderer-extension"
import { broadcastIpc } from "../common/ipc"
import type { LensExtensionRuntimeEnv } from "./lens-runtime"
import path from "path" import path from "path"
import { broadcastIpc } from "../common/ipc"
import { observable, reaction, toJS, } from "mobx" import { observable, reaction, toJS, } from "mobx"
import logger from "../main/logger" import logger from "../main/logger"
import { app, remote, ipcRenderer } from "electron" import { app, ipcRenderer, remote } from "electron"
import { pageRegistry } from "./page-registry"; import { appPreferenceRegistry, kubeObjectMenuRegistry, menuRegistry, pageRegistry, statusBarRegistry } from "./registries";
import { appPreferenceRegistry } from "./app-preference-registry"
import { kubeObjectMenuRegistry } from "../renderer/api/kube-object-menu-registry"
export interface InstalledExtension extends ExtensionModel { export interface InstalledExtension extends ExtensionModel {
manifestPath: string; manifestPath: string;
@ -36,33 +34,34 @@ export class ExtensionLoader {
} }
} }
loadOnClusterRenderer(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) { loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer') logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensRendererExtension) => { this.autoloadExtensions((instance: LensMainExtension) => {
instance.registerAppMenus(menuRegistry);
})
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerPages(pageRegistry)
instance.registerAppPreferences(appPreferenceRegistry)
instance.registerStatusBarIcon(statusBarRegistry)
})
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerPages(pageRegistry) instance.registerPages(pageRegistry)
instance.registerKubeObjectMenus(kubeObjectMenuRegistry) instance.registerKubeObjectMenus(kubeObjectMenuRegistry)
}) })
} }
loadOnMainRenderer(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) { protected autoloadExtensions(callback: (instance: LensExtension) => void) {
logger.info('[EXTENSIONS-LOADER]: load on main renderer')
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensRendererExtension) => {
instance.registerPages(pageRegistry)
instance.registerAppPreferences(appPreferenceRegistry)
})
}
loadOnMain(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensExtension) => {
// todo
})
}
protected autoloadExtensions(getLensRuntimeEnv: () => LensExtensionRuntimeEnv, callback: (instance: LensExtension) => void) {
return reaction(() => this.extensions.toJS(), (installedExtensions) => { return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for(const [id, ext] of installedExtensions) { for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath) let instance = this.instances.get(ext.name)
if (!instance) { if (!instance) {
const extensionModule = this.requireExtension(ext) const extensionModule = this.requireExtension(ext)
if (!extensionModule) { if (!extensionModule) {
@ -70,9 +69,9 @@ export class ExtensionLoader {
} }
const LensExtensionClass = extensionModule.default; const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest); instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
instance.enable(getLensRuntimeEnv()) instance.enable();
callback(instance) callback(instance)
this.instances.set(ext.id, instance) this.instances.set(ext.name, instance)
} }
} }
}, { }, {
@ -106,7 +105,9 @@ export class ExtensionLoader {
const extension = this.getById(id); const extension = this.getById(id);
if (extension) { if (extension) {
const instance = this.instances.get(extension.id) const instance = this.instances.get(extension.id)
if (instance) { await instance.disable() } if (instance) {
await instance.disable()
}
this.extensions.delete(id); this.extensions.delete(id);
} }
} }

View File

@ -1,4 +1,3 @@
import type { LensExtensionRuntimeEnv } from "./lens-runtime";
import { readJsonSync } from "fs-extra"; import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx"; import { action, observable, toJS } from "mobx";
import logger from "../main/logger"; import logger from "../main/logger";
@ -34,7 +33,6 @@ export class LensExtension implements ExtensionModel {
@observable manifest: ExtensionManifest; @observable manifest: ExtensionManifest;
@observable manifestPath: string; @observable manifestPath: string;
@observable isEnabled = false; @observable isEnabled = false;
@observable.ref runtime: LensExtensionRuntimeEnv;
constructor(model: ExtensionModel, manifest: ExtensionManifest) { constructor(model: ExtensionModel, manifest: ExtensionManifest) {
this.importModel(model, manifest); this.importModel(model, manifest);
@ -56,9 +54,8 @@ export class LensExtension implements ExtensionModel {
// mock // mock
} }
async enable(runtime: LensExtensionRuntimeEnv) { async enable() {
this.isEnabled = true; this.isEnabled = true;
this.runtime = runtime;
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
this.onActivate(); this.onActivate();
} }
@ -66,7 +63,6 @@ export class LensExtension implements ExtensionModel {
async disable() { async disable() {
this.onDeactivate(); this.onDeactivate();
this.isEnabled = false; this.isEnabled = false;
this.runtime = null;
this.disposers.forEach(cleanUp => cleanUp()); this.disposers.forEach(cleanUp => cleanUp());
this.disposers.length = 0; this.disposers.length = 0;
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);

View File

@ -1,11 +1,17 @@
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension"
import type { MenuRegistry } from "./registries/menu-registry";
import type { StatusBarRegistry } from "./registries/status-bar-registry";
export class LensMainExtension extends LensExtension { export class LensMainExtension extends LensExtension {
async registerAppMenus() { registerAppMenus(registry: MenuRegistry) {
// //
} }
async registerPrometheusProviders(registry: any) { registerStatusBarIcon(registry: StatusBarRegistry) {
//
}
registerPrometheusProviders(registry: any) {
// //
} }
} }

View File

@ -1,10 +1,7 @@
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension"
import type { PageRegistry } from "./page-registry" import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry } from "./registries"
import type { AppPreferenceRegistry } from "./app-preference-registry";
import type { KubeObjectMenuRegistry } from "../renderer/api/kube-object-menu-registry";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
registerPages(registry: PageRegistry) { registerPages(registry: PageRegistry) {
return return
} }
@ -13,6 +10,10 @@ export class LensRendererExtension extends LensExtension {
return return
} }
registerStatusBarIcon(registry: StatusBarRegistry) {
return
}
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) { registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
return return
} }

View File

@ -1,13 +0,0 @@
// Lens extension runtime params available to extensions after activation
import logger from "../main/logger";
export interface LensExtensionRuntimeEnv {
logger: typeof logger;
}
export function getLensRuntime(): LensExtensionRuntimeEnv {
return {
logger
}
}

View File

@ -1,52 +0,0 @@
// Extensions-api -> Dynamic pages
import { computed, observable } from "mobx";
import React from "react";
import { RouteProps } from "react-router";
import { IconProps } from "../renderer/components/icon";
import { IClassName } from "../renderer/utils";
import { TabRoute } from "../renderer/components/layout/tab-layout";
export enum DynamicPageType {
GLOBAL = "lens-scope",
CLUSTER = "cluster-view-scope",
}
export interface PageRegistration extends RouteProps {
className?: IClassName;
url?: string; // initial url to be used for building menus and tabs, otherwise "path" applied by default
path: string; // route-path
title: React.ReactNode; // used in sidebar's & tabs-layout
type: DynamicPageType;
components: PageComponents;
subPages?: (PageRegistration & TabRoute)[];
}
export interface PageComponents {
Page: React.ComponentType<any>;
MenuIcon: React.ComponentType<IconProps>;
}
export class PageRegistry {
protected pages = observable.array<PageRegistration>([], { deep: false });
@computed get globalPages() {
return this.pages.filter(page => page.type === DynamicPageType.GLOBAL);
}
@computed get clusterPages() {
return this.pages.filter(page => page.type === DynamicPageType.CLUSTER);
}
// todo: verify paths to avoid collision with existing pages
add(params: PageRegistration) {
this.pages.push(params);
return () => {
this.pages.replace(
this.pages.filter(page => page.components !== params.components)
)
};
}
}
export const pageRegistry = new PageRegistry();

View File

@ -0,0 +1,17 @@
import type React from "react"
import { BaseRegistry } from "./base-registry";
export interface AppPreferenceComponents {
Hint: React.ComponentType<any>;
Input: React.ComponentType<any>;
}
export interface AppPreferenceRegistration {
title: string;
components: AppPreferenceComponents;
}
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration> {
}
export const appPreferenceRegistry = new AppPreferenceRegistry()

View File

@ -0,0 +1,17 @@
// Base class for extensions-api registries
import { observable } from "mobx";
export class BaseRegistry<T = any> {
protected items = observable<T>([], { deep: false });
getItems(): T[] {
return this.items.toJS();
}
add(item: T) {
this.items.push(item);
return () => {
this.items.remove(item); // works because of {deep: false};
}
}
}

View File

@ -0,0 +1,7 @@
// All registries managed by extensions api
export * from "./page-registry"
export * from "./menu-registry"
export * from "./app-preference-registry"
export * from "./status-bar-registry"
export * from "./kube-object-menu-registry";

View File

@ -1,5 +1,5 @@
import { observable } from "mobx"
import React from "react" import React from "react"
import { BaseRegistry } from "./base-registry";
export interface KubeObjectMenuComponents { export interface KubeObjectMenuComponents {
MenuItem: React.ComponentType<any>; MenuItem: React.ComponentType<any>;
@ -11,18 +11,7 @@ export interface KubeObjectMenuRegistration {
components: KubeObjectMenuComponents; components: KubeObjectMenuComponents;
} }
export class KubeObjectMenuRegistry { export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> {
items = observable.array<KubeObjectMenuRegistration>([], { deep: false });
add(item: KubeObjectMenuRegistration) {
this.items.push(item)
return () => {
this.items.replace(
this.items.filter(c => c !== item)
)
};
}
getItemsForKind(kind: string, apiVersion: string) { getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => { return this.items.filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion) return item.kind === kind && item.apiVersions.includes(apiVersion)

View File

@ -0,0 +1,14 @@
// Extensions API -> Global menu customizations
import type { MenuTopId } from "../../main/menu";
import type { MenuItemConstructorOptions } from "electron";
import { BaseRegistry } from "./base-registry";
export interface MenuRegistration extends MenuItemConstructorOptions {
parentId?: MenuTopId;
}
export class MenuRegistry extends BaseRegistry<MenuRegistration> {
}
export const menuRegistry = new MenuRegistry();

View File

@ -0,0 +1,40 @@
// Extensions-api -> Dynamic pages
import type React from "react";
import type { RouteProps } from "react-router";
import type { IconProps } from "../../renderer/components/icon";
import type { IClassName } from "../../renderer/utils";
import type { TabRoute } from "../../renderer/components/layout/tab-layout";
import { BaseRegistry } from "./base-registry";
import { computed } from "mobx";
export enum PageRegistryType {
GLOBAL = "lens-scope",
CLUSTER = "cluster-view-scope",
}
export interface PageRegistration extends RouteProps {
type: PageRegistryType;
components: PageComponents;
className?: IClassName;
url?: string; // initial url to be used for building menus and tabs, otherwise "path" applied by default
title?: React.ReactNode; // used in sidebar's & tabs-layout if provided
subPages?: (PageRegistration & TabRoute)[];
}
export interface PageComponents {
Page: React.ComponentType<any>;
MenuIcon?: React.ComponentType<IconProps>;
}
export class PageRegistry extends BaseRegistry<PageRegistration> {
@computed get globalPages() {
return this.items.filter(page => page.type === PageRegistryType.GLOBAL);
}
@computed get clusterPages() {
return this.items.filter(page => page.type === PageRegistryType.CLUSTER);
}
}
export const pageRegistry = new PageRegistry();

View File

@ -0,0 +1,13 @@
// Extensions API -> Status bar customizations
import React from "react";
import { BaseRegistry } from "./base-registry";
export interface StatusBarRegistration {
icon?: React.ReactNode;
}
export class StatusBarRegistry extends BaseRegistry<StatusBarRegistration> {
}
export const statusBarRegistry = new StatusBarRegistry();

View File

@ -1,10 +1,12 @@
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps) // TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
export * from "../../renderer/components/icon" export * from "../../renderer/components/icon"
export * from "../../renderer/components/checkbox" export * from "../../renderer/components/checkbox"
export * from "../../renderer/components/tooltip" export * from "../../renderer/components/tooltip"
export * from "../../renderer/components/button" export * from "../../renderer/components/button"
export * from "../../renderer/components/tabs" export * from "../../renderer/components/tabs"
export * from "../../renderer/components/badge" export * from "../../renderer/components/badge"
export * from "../../renderer/components/layout/page-layout"
export * from "../../renderer/components/drawer" export * from "../../renderer/components/drawer"
// kube helpers // kube helpers

View File

@ -0,0 +1,12 @@
// Lens-extensions apis, required in renderer process runtime
// APIs
import * as Component from "./components"
import * as K8sApi from "./k8s-api"
import * as Navigation from "./navigation"
export {
Component,
K8sApi,
Navigation,
}

View File

@ -1,10 +0,0 @@
// APIs
import * as Component from "./renderer-api/components"
import * as K8sApi from "./renderer-api/k8s-api"
import * as Navigation from "./renderer-api/navigation"
export {
Component,
K8sApi,
Navigation,
}

View File

@ -2,6 +2,8 @@
import "../common/system-ca" import "../common/system-ca"
import "../common/prometheus-providers" import "../common/prometheus-providers"
import * as Mobx from "mobx"
import * as LensExtensions from "../extensions/core-api";
import { app, dialog } from "electron" import { app, dialog } from "electron"
import { appName } from "../common/vars"; import { appName } from "../common/vars";
import path from "path" import path from "path"
@ -17,17 +19,9 @@ import { clusterStore } from "../common/cluster-store"
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store"; import { workspaceStore } from "../common/workspace-store";
import { appEventBus } from "../common/event-bus" import { appEventBus } from "../common/event-bus"
import * as LensExtensions from "../extensions/core-extension-api";
import { extensionManager } from "../extensions/extension-manager"; import { extensionManager } from "../extensions/extension-manager";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import { getLensRuntime } from "../extensions/lens-runtime";
import logger from "./logger" import logger from "./logger"
import * as Mobx from "mobx"
export {
LensExtensions,
Mobx
}
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
app.setName(appName); app.setName(appName);
@ -35,7 +29,6 @@ if (!process.env.CICD) {
app.setPath("userData", workingDir); app.setPath("userData", workingDir);
} }
let windowManager: WindowManager;
let clusterManager: ClusterManager; let clusterManager: ClusterManager;
let proxyServer: LensProxy; let proxyServer: LensProxy;
@ -83,9 +76,9 @@ async function main() {
} }
// create window manager and open app // create window manager and open app
windowManager = new WindowManager(proxyPort); LensExtensionsApi.windowManager = new WindowManager(proxyPort);
extensionLoader.loadOnMain(getLensRuntime) extensionLoader.loadOnMain()
extensionLoader.extensions.replace(await extensionManager.load()) extensionLoader.extensions.replace(await extensionManager.load())
extensionLoader.broadcastExtensions() extensionLoader.broadcastExtensions()
@ -102,3 +95,13 @@ app.on("will-quit", async (event) => {
if (clusterManager) clusterManager.stop() if (clusterManager) clusterManager.stop()
app.exit(); app.exit();
}) })
// Extensions-api runtime exports
export const LensExtensionsApi = {
...LensExtensions,
};
export {
Mobx,
LensExtensionsApi as LensExtensions,
}

View File

@ -6,8 +6,11 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r
import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
import { menuRegistry } from "../extensions/registries/menu-registry";
import logger from "./logger"; import logger from "./logger";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
export function initMenu(windowManager: WindowManager) { export function initMenu(windowManager: WindowManager) {
autorun(() => buildMenu(windowManager), { autorun(() => buildMenu(windowManager), {
delay: 100 delay: 100
@ -53,8 +56,6 @@ export function buildMenu(windowManager: WindowManager) {
}) })
} }
const mt: MenuItemConstructorOptions[] = [];
const macAppMenu: MenuItemConstructorOptions = { const macAppMenu: MenuItemConstructorOptions = {
label: app.getName(), label: app.getName(),
submenu: [ submenu: [
@ -83,10 +84,6 @@ export function buildMenu(windowManager: WindowManager) {
] ]
}; };
if (isMac) {
mt.push(macAppMenu);
}
const fileMenu: MenuItemConstructorOptions = { const fileMenu: MenuItemConstructorOptions = {
label: "File", label: "File",
submenu: [ submenu: [
@ -124,7 +121,6 @@ export function buildMenu(windowManager: WindowManager) {
]) ])
] ]
}; };
mt.push(fileMenu)
const editMenu: MenuItemConstructorOptions = { const editMenu: MenuItemConstructorOptions = {
label: 'Edit', label: 'Edit',
@ -140,7 +136,7 @@ export function buildMenu(windowManager: WindowManager) {
{ role: 'selectAll' }, { role: 'selectAll' },
] ]
}; };
mt.push(editMenu)
const viewMenu: MenuItemConstructorOptions = { const viewMenu: MenuItemConstructorOptions = {
label: 'View', label: 'View',
submenu: [ submenu: [
@ -174,7 +170,6 @@ export function buildMenu(windowManager: WindowManager) {
{ role: 'togglefullscreen' } { role: 'togglefullscreen' }
] ]
}; };
mt.push(viewMenu)
const helpMenu: MenuItemConstructorOptions = { const helpMenu: MenuItemConstructorOptions = {
role: 'help', role: 'help',
@ -214,7 +209,29 @@ export function buildMenu(windowManager: WindowManager) {
] ]
}; };
mt.push(helpMenu) // Prepare menu items order
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
mac: macAppMenu,
file: fileMenu,
edit: editMenu,
view: viewMenu,
help: helpMenu,
}
Menu.setApplicationMenu(Menu.buildFromTemplate(mt)); // Modify menu from extensions-api
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
try {
const topMenu = appMenu[parentId].submenu as MenuItemConstructorOptions[];
topMenu.push(menuItem);
} catch (err) {
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })
}
})
if (!isMac) {
delete appMenu.mac
}
const menu = Menu.buildFromTemplate(Object.values(appMenu));
Menu.setApplicationMenu(menu);
} }

View File

@ -1,4 +1,5 @@
import "./components/app.scss" import "./components/app.scss"
import React from "react"; import React from "react";
import * as Mobx from "mobx" import * as Mobx from "mobx"
import * as MobxReact from "mobx-react" import * as MobxReact from "mobx-react"

View File

@ -1,87 +1,51 @@
.ClusterSettings { .ClusterSettings {
.WizardLayout { $spacing: $padding * 3;
grid-template-columns: unset;
grid-template-rows: 76px 1fr;
padding: 0;
.head-col { > .content-wrapper {
justify-content: space-between; --flex-gap: #{$spacing};
}
:nth-child(2) { // TODO: move sub-component styles to separate files
flex: 1 0 0; .admin-note {
} font-size: small;
} opacity: 0.5;
margin-left: $margin;
}
.content-col { .button-area {
margin: 0; margin-top: $margin * 2;
padding-top: $padding * 3; }
background-color: $clusterSettingsBackground;
.SubTitle { .file-loader {
text-transform: none; margin-top: $margin * 2;
} }
> div { .status-table {
margin-top: $margin * 5; margin: $spacing 0;
}
.admin-note { .Table {
font-size: small; border: 1px solid var(--drawerSubtitleBackground);
opacity: 0.5; border-radius: $radius;
margin-left: $margin;
}
.button-area { .TableRow {
margin-top: $margin * 2; &:not(:last-of-type) {
} border-bottom: 1px solid var(--drawerSubtitleBackground);
}
.file-loader { .value {
margin-top: $margin * 2; flex-grow: 2;
} word-break: break-word;
color: var(--textColorSecondary);
}
.hint { .link {
font-size: smaller; @include pseudo-link;
opacity: 0.8;
}
p + p, .hint + p {
padding-top: $padding;
}
}
.status-table {
margin: $margin * 3 0;
.Table {
border: 1px solid var(--drawerSubtitleBackground);
border-radius: $radius;
.TableRow {
&:not(:last-of-type) {
border-bottom: 1px solid var(--drawerSubtitleBackground);
}
.value {
flex-grow: 2;
word-break: break-word;
color: var(--textColorSecondary);
}
.link {
@include pseudo-link;
}
} }
} }
} }
}
.Input, .Select { .Input, .Select {
margin-top: 10px; margin-top: $padding;
}
.Select {
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
} }
} }

View File

@ -1,21 +1,19 @@
import "./cluster-settings.scss"; import "./cluster-settings.scss";
import React from "react"; import React from "react";
import { observer, disposeOnUnmount } from "mobx-react"; import { autorun } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { Features } from "./features"; import { Features } from "./features";
import { Removal } from "./removal"; import { Removal } from "./removal";
import { Status } from "./status"; import { Status } from "./status";
import { General } from "./general"; import { General } from "./general";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { WizardLayout } from "../layout/wizard-layout";
import { ClusterIcon } from "../cluster-icon"; import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { navigate } from "../../navigation";
import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterIpc } from "../../../common/cluster-ipc";
import { autorun } from "mobx"; import { PageLayout } from "../layout/page-layout";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> { interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
} }
@ -27,7 +25,6 @@ export class ClusterSettings extends React.Component<Props> {
} }
async componentDidMount() { async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
disposeOnUnmount(this, disposeOnUnmount(this,
autorun(() => { autorun(() => {
this.refreshCluster(); this.refreshCluster();
@ -35,51 +32,29 @@ export class ClusterSettings extends React.Component<Props> {
) )
} }
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (evt.code === "Escape") {
evt.stopPropagation();
this.close();
}
}
refreshCluster = async () => { refreshCluster = async () => {
if(this.cluster) { if (this.cluster) {
await clusterIpc.activate.invokeFromRenderer(this.cluster.id); await clusterIpc.activate.invokeFromRenderer(this.cluster.id);
await clusterIpc.refresh.invokeFromRenderer(this.cluster.id); await clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
} }
} }
close() {
navigate("/");
}
render() { render() {
const cluster = this.cluster const cluster = this.cluster
if (!cluster) return null; if (!cluster) return null;
const header = ( const header = (
<> <>
<ClusterIcon <ClusterIcon cluster={cluster} showErrors={false} showTooltip={false}/>
cluster={cluster}
showErrors={false}
showTooltip={false}
/>
<h2>{cluster.preferences.clusterName}</h2> <h2>{cluster.preferences.clusterName}</h2>
<Icon material="close" onClick={this.close} big/>
</> </>
); );
return ( return (
<div className="ClusterSettings"> <PageLayout className="ClusterSettings" header={header}>
<WizardLayout header={header} centered> <Status cluster={cluster}></Status>
<Status cluster={cluster}></Status> <General cluster={cluster}></General>
<General cluster={cluster}></General> <Features cluster={cluster}></Features>
<Features cluster={cluster}></Features> <Removal cluster={cluster}></Removal>
<Removal cluster={cluster}></Removal> </PageLayout>
</WizardLayout>
</div>
); );
} }
} }

View File

@ -41,10 +41,10 @@ export class ClusterHomeDirSetting extends React.Component<Props> {
onBlur={this.save} onBlur={this.save}
placeholder="$HOME" placeholder="$HOME"
/> />
<span className="hint"> <small className="hint">
An explicit start path where the terminal will be launched,{" "} An explicit start path where the terminal will be launched,{" "}
this is used as the current working directory (cwd) for the shell process. this is used as the current working directory (cwd) for the shell process.
</span> </small>
</> </>
); );
} }

View File

@ -90,7 +90,7 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
}} }}
options={options} options={options}
/> />
<span className="hint">What query format is used to fetch metrics from Prometheus</span> <small className="hint">What query format is used to fetch metrics from Prometheus</small>
{this.canEditPrometheusPath && ( {this.canEditPrometheusPath && (
<> <>
<p>Prometheus service address.</p> <p>Prometheus service address.</p>
@ -101,10 +101,10 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
onBlur={this.onSavePath} onBlur={this.onSavePath}
placeholder="<namespace>/<service>:<port>" placeholder="<namespace>/<service>:<port>"
/> />
<span className="hint"> <small className="hint">
An address to an existing Prometheus installation{" "} An address to an existing Prometheus installation{" "}
({'<namespace>/<service>:<port>'}). Lens tries to auto-detect address if left empty. ({'<namespace>/<service>:<port>'}). Lens tries to auto-detect address if left empty.
</span> </small>
</> </>
)} )}
</> </>

View File

@ -55,7 +55,7 @@ export class Nodes extends React.Component<Props> {
max={cores} max={cores}
value={usage} value={usage}
tooltip={{ tooltip={{
position: TooltipPosition.BOTTOM, preferredPositions: TooltipPosition.BOTTOM,
children: _i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}` children: _i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}`
}} }}
/> />
@ -72,7 +72,7 @@ export class Nodes extends React.Component<Props> {
max={capacity} max={capacity}
value={usage} value={usage}
tooltip={{ tooltip={{
position: TooltipPosition.BOTTOM, preferredPositions: TooltipPosition.BOTTOM,
children: _i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}` children: _i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`
}} }}
/> />
@ -89,7 +89,7 @@ export class Nodes extends React.Component<Props> {
max={capacity} max={capacity}
value={usage} value={usage}
tooltip={{ tooltip={{
position: TooltipPosition.BOTTOM, preferredPositions: TooltipPosition.BOTTOM,
children: _i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}` children: _i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`
}} }}
/> />

View File

@ -1,60 +1,24 @@
.Preferences { .Preferences {
position: fixed!important; // Allows to cover ClustersMenu $spacing: $padding * 2;
z-index: 1;
.WizardLayout { .repos {
grid-template-columns: unset; position: relative;
grid-template-rows: 76px 1fr;
padding: 0;
.content-col { .Badge {
padding: $padding * 8 0; display: flex;
background-color: $clusterSettingsBackground; margin: 0;
margin-bottom: 1px;
h2 { padding: $padding $spacing;
margin-bottom: $margin * 2;
&:not(:first-child) {
margin-top: $margin * 3;
}
}
.SubTitle {
text-transform: none;
margin: 0!important;
}
.repos {
position: relative;
.Badge {
display: flex;
margin: 0;
margin-bottom: 1px;
padding: $padding $padding * 2;
}
}
.hint {
margin-top: -$margin;
}
} }
} }
.is-mac & { .extensions {
.WizardLayout .head-col { h2 {
padding-top: 32px; margin: $spacing 0;
overflow: hidden;
.Icon {
margin-top: -$margin * 2;
}
} }
}
.Select { &:empty {
&__control { display: none;
box-shadow: 0 0 0 1px $borderFaintColor;
} }
} }
} }

View File

@ -1,10 +1,10 @@
import "./preferences.scss" import "./preferences.scss"
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { action, computed, observable } from "mobx"; import { action, computed, observable } from "mobx";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { WizardLayout } from "../layout/wizard-layout";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { userStore } from "../../../common/user-store"; import { userStore } from "../../../common/user-store";
@ -14,10 +14,10 @@ import { Checkbox } from "../checkbox";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { history } from "../../navigation";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { KubectlBinaries } from "./kubectl-binaries"; import { KubectlBinaries } from "./kubectl-binaries";
import { appPreferenceRegistry } from "../../../extensions/app-preference-registry"; import { appPreferenceRegistry } from "../../../extensions/registries/app-preference-registry";
import { PageLayout } from "../layout/page-layout";
@observer @observer
export class Preferences extends React.Component { export class Preferences extends React.Component {
@ -41,21 +41,9 @@ export class Preferences extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
await this.loadHelmRepos(); await this.loadHelmRepos();
} }
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (evt.code === "Escape") {
evt.stopPropagation();
history.goBack();
}
}
@action @action
async loadHelmRepos() { async loadHelmRepos() {
this.helmLoading = true; this.helmLoading = true;
@ -115,91 +103,85 @@ export class Preferences extends React.Component {
render() { render() {
const { preferences } = userStore; const { preferences } = userStore;
const extensionPreferences = appPreferenceRegistry.preferences const header = <h2><Trans>Preferences</Trans></h2>;
const header = (
<>
<h2>Preferences</h2>
<Icon material="close" big onClick={history.goBack}/>
</>
);
return ( return (
<div className="Preferences"> <PageLayout showOnTop className="Preferences" header={header}>
<WizardLayout header={header} centered> <h2><Trans>Color Theme</Trans></h2>
<h2><Trans>Color Theme</Trans></h2> <Select
<Select options={this.themeOptions}
options={this.themeOptions} value={preferences.colorTheme}
value={preferences.colorTheme} onChange={({ value }: SelectOption) => preferences.colorTheme = value}
onChange={({ value }: SelectOption) => preferences.colorTheme = value} />
/>
<h2><Trans>HTTP Proxy</Trans></h2> <h2><Trans>HTTP Proxy</Trans></h2>
<Input <Input
theme="round-black" theme="round-black"
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)} placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={this.httpProxy} value={this.httpProxy}
onChange={v => this.httpProxy = v} onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy} onBlur={() => preferences.httpsProxy = this.httpProxy}
/> />
<small className="hint"> <small className="hint">
<Trans>Proxy is used only for non-cluster communication.</Trans> <Trans>Proxy is used only for non-cluster communication.</Trans>
</small> </small>
<KubectlBinaries preferences={preferences} /> <KubectlBinaries preferences={preferences}/>
<h2><Trans>Helm</Trans></h2> <h2><Trans>Helm</Trans></h2>
<Select <Select
placeholder={<Trans>Repositories</Trans>} placeholder={<Trans>Repositories</Trans>}
isLoading={this.helmLoading} isLoading={this.helmLoading}
isDisabled={this.helmLoading} isDisabled={this.helmLoading}
options={this.helmOptions} options={this.helmOptions}
onChange={this.onRepoSelect} onChange={this.onRepoSelect}
formatOptionLabel={this.formatHelmOptionLabel} formatOptionLabel={this.formatHelmOptionLabel}
controlShouldRenderValue={false} controlShouldRenderValue={false}
/> />
<div className="repos flex gaps column"> <div className="repos flex gaps column">
{Array.from(this.helmAddedRepos).map(([name, repo]) => { {Array.from(this.helmAddedRepos).map(([name, repo]) => {
const tooltipId = `message-${name}`; const tooltipId = `message-${name}`;
return (
<Badge key={name} className="added-repo flex gaps align-center justify-space-between">
<span id={tooltipId} className="repo">{name}</span>
<Icon
material="delete"
onClick={() => this.removeRepo(repo)}
tooltip={<Trans>Remove</Trans>}
/>
<Tooltip targetId={tooltipId} formatters={{ narrow: true }}>
{repo.url}
</Tooltip>
</Badge>
)
})}
</div>
<h2><Trans>Certificate Trust</Trans></h2>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
value={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v}
/>
<small className="hint">
<Trans>This will make Lens to trust ANY certificate authority without any validations.</Trans>{" "}
<Trans>Needed with some corporate proxies that do certificate re-writing.</Trans>{" "}
<Trans>Does not affect cluster communications!</Trans>
</small>
{extensionPreferences.map(({title, components: { Hint, Input}}) => {
return ( return (
<div key={title}> <Badge key={name} className="added-repo flex gaps align-center justify-space-between">
<span id={tooltipId} className="repo">{name}</span>
<Icon
material="delete"
onClick={() => this.removeRepo(repo)}
tooltip={<Trans>Remove</Trans>}
/>
<Tooltip targetId={tooltipId} formatters={{ narrow: true }}>
{repo.url}
</Tooltip>
</Badge>
)
})}
</div>
<h2><Trans>Certificate Trust</Trans></h2>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
value={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v}
/>
<small className="hint">
<Trans>This will make Lens to trust ANY certificate authority without any validations.</Trans>{" "}
<Trans>Needed with some corporate proxies that do certificate re-writing.</Trans>{" "}
<Trans>Does not affect cluster communications!</Trans>
</small>
<div className="extensions flex column gaps">
{appPreferenceRegistry.getItems().map(({ title, components: { Hint, Input } }, index) => {
return (
<div key={index} className="preference">
<h2>{title}</h2> <h2>{title}</h2>
<Input /> <Input/>
<small className="hint"> <small className="hint">
<Hint /> <Hint/>
</small> </small>
</div> </div>
) )
})} })}
</WizardLayout> </div>
</div> </PageLayout>
); );
} }
} }

View File

@ -3,9 +3,9 @@ import "./service-accounts.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { ServiceAccount, serviceAccountsApi } from "../../api/endpoints/service-accounts.api"; import { ServiceAccount } from "../../api/endpoints/service-accounts.api";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { openServiceAccountKubeConfig } from "../kubeconfig-dialog"; import { openServiceAccountKubeConfig } from "../kubeconfig-dialog";
import { Icon } from "../icon"; import { Icon } from "../icon";
@ -13,7 +13,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { IServiceAccountsRouteParams } from "../+user-management"; import { IServiceAccountsRouteParams } from "../+user-management";
import { serviceAccountsStore } from "./service-accounts.store"; import { serviceAccountsStore } from "./service-accounts.store";
import { CreateServiceAccountDialog } from "./create-service-account-dialog"; import { CreateServiceAccountDialog } from "./create-service-account-dialog";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
enum sortBy { enum sortBy {
name = "name", name = "name",

View File

@ -4,19 +4,19 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { CronJob, cronJobApi } from "../../api/endpoints/cron-job.api"; import { CronJob } from "../../api/endpoints/cron-job.api";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { cronJobStore } from "./cronjob.store"; import { cronJobStore } from "./cronjob.store";
import { jobStore } from "../+workloads-jobs/job.store"; import { jobStore } from "../+workloads-jobs/job.store";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { ICronJobsRouteParams } from "../+workloads"; import { ICronJobsRouteParams } from "../+workloads";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { KubeEventIcon } from "../+events/kube-event-icon"; import { KubeEventIcon } from "../+events/kube-event-icon";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { CronJobTriggerDialog } from "./cronjob-trigger-dialog"; import { CronJobTriggerDialog } from "./cronjob-trigger-dialog";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
enum sortBy { enum sortBy {
name = "name", name = "name",

View File

@ -4,8 +4,8 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { Deployment, deploymentApi } from "../../api/endpoints"; import { Deployment } from "../../api/endpoints";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { DeploymentScaleDialog } from "./deployment-scale-dialog"; import { DeploymentScaleDialog } from "./deployment-scale-dialog";
@ -21,8 +21,7 @@ import { cssNames } from "../../utils";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import { KubeEventIcon } from "../+events/kube-event-icon"; import { KubeEventIcon } from "../+events/kube-event-icon";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
import { DeploymentDetails } from "./deployment-details";
enum sortBy { enum sortBy {
name = "name", name = "name",

View File

@ -35,11 +35,10 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
import logger from "../../main/logger"; import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc"; import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron"; import { webFrame } from "electron";
import { pageRegistry } from "../../extensions/page-registry"; import { pageRegistry } from "../../extensions/registries/page-registry";
import { DynamicPage } from "../../extensions/dynamic-page"; import { DynamicPage } from "../../extensions/dynamic-page";
import { extensionLoader } from "../../extensions/extension-loader"; import { extensionLoader } from "../../extensions/extension-loader";
import { getLensRuntime } from "../../extensions/lens-runtime"; import { appEventBus } from "../../common/event-bus"
import { appEventBus } from "../../common/event-bus"
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@ -51,7 +50,7 @@ export class App extends React.Component {
await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId); await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId);
await getHostedCluster().whenReady; // cluster.activate() is done at this point await getHostedCluster().whenReady; // cluster.activate() is done at this point
extensionLoader.loadOnClusterRenderer(getLensRuntime) extensionLoader.loadOnClusterRenderer();
appEventBus.emit({name: "cluster", action: "open", params: { appEventBus.emit({name: "cluster", action: "open", params: {
clusterId: clusterId clusterId: clusterId
}}) }})
@ -83,7 +82,7 @@ export class App extends React.Component {
<Route component={UserManagement} {...usersManagementRoute}/> <Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/> <Route component={Apps} {...appsRoute}/>
{pageRegistry.clusterPages.map(page => { {pageRegistry.clusterPages.map(page => {
return <Route {...page} key={page.path} render={() => <DynamicPage page={page}/>}/> return <Route {...page} key={String(page.path)} render={() => <DynamicPage page={page}/>}/>
})} })}
<Redirect exact from="/" to={this.startURL}/> <Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/> <Route component={NotFound}/>

View File

@ -1,9 +1,11 @@
import "./bottom-bar.scss" import "./bottom-bar.scss"
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { WorkspaceMenu } from "../+workspaces/workspace-menu"; import { WorkspaceMenu } from "../+workspaces/workspace-menu";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { statusBarRegistry } from "../../../extensions/registries";
@observer @observer
export class BottomBar extends React.Component { export class BottomBar extends React.Component {
@ -11,11 +13,19 @@ export class BottomBar extends React.Component {
const { currentWorkspace } = workspaceStore; const { currentWorkspace } = workspaceStore;
return ( return (
<div className="BottomBar flex gaps"> <div className="BottomBar flex gaps">
<div id="current-workspace" className="flex gaps align-center box"> <div id="current-workspace" className="flex gaps align-center">
<Icon small material="layers"/> <Icon small material="layers"/>
<span className="workspace-name">{currentWorkspace.name}</span> <span className="workspace-name">{currentWorkspace.name}</span>
</div> </div>
<WorkspaceMenu htmlFor="current-workspace"/> <WorkspaceMenu
htmlFor="current-workspace"
/>
<div className="extensions box grow flex gaps justify-flex-end">
{statusBarRegistry.getItems().map(({ icon }, index) => {
if (!icon) return;
return <React.Fragment key={index}>{icon}</React.Fragment>
})}
</div>
</div> </div>
) )
} }

View File

@ -14,7 +14,7 @@ import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route"; import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { pageRegistry } from "../../../extensions/page-registry"; import { pageRegistry } from "../../../extensions/registries/page-registry";
@observer @observer
export class ClusterManager extends React.Component { export class ClusterManager extends React.Component {
@ -63,8 +63,8 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} /> <Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} /> <Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} /> <Route component={ClusterSettings} {...clusterSettingsRoute} />
{pageRegistry.globalPages.map(({ path, components: { Page } }) => { {pageRegistry.globalPages.map(({ path, url = String(path), components: { Page } }) => {
return <Route key={path} path={path} component={Page}/> return <Route key={url} path={path} component={Page}/>
})} })}
<Redirect exact to={this.startUrl} /> <Redirect exact to={this.startUrl} />
</Switch> </Switch>

View File

@ -67,7 +67,7 @@
} }
} }
> .dynamic-pages { > .extensions {
&:not(:empty) { &:not(:empty) {
padding-top: $spacing; padding-top: $spacing;
} }

View File

@ -11,7 +11,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon"; import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { cssNames, IClassName, autobind } from "../../utils"; import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster"; import { addClusterURL } from "../+add-cluster";
@ -21,8 +21,8 @@ import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL } from "./cluster-view.route"; import { clusterViewURL } from "./cluster-view.route";
import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { pageRegistry } from "../../../extensions/page-registry"; import { pageRegistry } from "../../../extensions/registries/page-registry";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -155,9 +155,10 @@ export class ClustersMenu extends React.Component<Props> {
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} /> <Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
)} )}
</div> </div>
<div className="dynamic-pages"> <div className="extensions">
{pageRegistry.globalPages.map(({ path, components: { MenuIcon } }) => { {pageRegistry.globalPages.map(({ path, url = String(path), components: { MenuIcon } }) => {
return <MenuIcon key={path} onClick={() => navigate(path)}/> if (!MenuIcon) return;
return <MenuIcon key={url} onClick={() => navigate(url)}/>
})} })}
</div> </div>
</div> </div>

View File

@ -1,7 +1,15 @@
// Custom fonts // Custom fonts
@import "~material-design-icons/iconfont/material-icons.css";
@import "~typeface-roboto/index.css"; @import "~typeface-roboto/index.css";
// Material Design Icons, used primarily in icon.tsx
// Latest: https://github.com/google/material-design-icons/tree/master/font
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url("../fonts/MaterialIcons-Regular.ttf") format("truetype");
}
// Patched RobotoMono font with icons // Patched RobotoMono font with icons
// RobotoMono Windows Compatible for using in terminal // RobotoMono Windows Compatible for using in terminal
// https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono // https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono

View File

@ -6,7 +6,7 @@ import { editResourceTab } from "../dock/edit-resource.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { hideDetails } from "../../navigation"; import { hideDetails } from "../../navigation";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
export interface KubeObjectMenuProps<T extends KubeObject = any> extends MenuActionsProps { export interface KubeObjectMenuProps<T extends KubeObject = any> extends MenuActionsProps {
object: T; object: T;

View File

@ -0,0 +1,64 @@
.PageLayout {
$spacing: $padding * 2;
position: relative;
height: 100%;
display: grid !important;
grid-template-rows: min-content 1fr;
// covers whole app view area
&.top {
position: fixed !important; // allow to cover ClustersMenu
z-index: 1;
// adds extra space for traffic-light top buttons (mac only)
.is-mac & > .header {
padding-top: $spacing * 2;
}
}
> .header {
position: sticky;
padding: $spacing;
background-color: $layoutTabsBackground;
}
> .content-wrapper {
@include custom-scrollbar-themed;
padding: $spacing * 2;
> .content {
margin: 0 auto;
width: 60%;
min-width: 570px;
max-width: 1000px;
}
}
h2:not(:first-of-type) {
margin-top: $spacing;
}
p {
line-height: 140%;
}
a {
color: $colorInfo;
}
.SubTitle {
text-transform: none;
margin-bottom: 0 !important;
+ * + .hint {
margin-top: -$padding / 2;
}
}
.Select {
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
}

View File

@ -0,0 +1,82 @@
import "./page-layout.scss"
import React from "react";
import { observer } from "mobx-react";
import { autobind, cssNames, IClassName } from "../../utils";
import { Icon } from "../icon";
import { navigation } from "../../navigation";
export interface PageLayoutProps extends React.DOMAttributes<any> {
className?: IClassName;
header: React.ReactNode;
headerClass?: IClassName;
contentClass?: IClassName;
provideBackButtonNavigation?: boolean;
contentGaps?: boolean;
showOnTop?: boolean; // covers whole app view
back?: (evt: React.MouseEvent | KeyboardEvent) => void;
}
const defaultProps: Partial<PageLayoutProps> = {
provideBackButtonNavigation: true,
contentGaps: true,
}
@observer
export class PageLayout extends React.Component<PageLayoutProps> {
static defaultProps = defaultProps as object;
@autobind()
back(evt?: React.MouseEvent | KeyboardEvent) {
if (this.props.back) {
this.props.back(evt);
} else {
navigation.goBack();
}
}
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (!this.props.provideBackButtonNavigation) {
return;
}
if (evt.code === "Escape") {
evt.stopPropagation();
this.back(evt);
}
}
render() {
const {
contentClass, header, headerClass, provideBackButtonNavigation,
contentGaps, showOnTop, children, ...elemProps
} = this.props;
const className = cssNames("PageLayout", { top: showOnTop }, this.props.className);
return (
<div {...elemProps} className={className}>
<div className={cssNames("header flex gaps align-center", headerClass)}>
{header}
{provideBackButtonNavigation && (
<Icon
big material="close"
className="back box right"
onClick={this.back}
/>
)}
</div>
<div className="content-wrapper">
<div className={cssNames("content", contentGaps && "flex column gaps", contentClass)}>
{children}
</div>
</div>
</div>
)
}
}

View File

@ -28,7 +28,7 @@ import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resourc
import { CustomResources } from "../+custom-resources/custom-resources"; import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac" import { isAllowedResource } from "../../../common/rbac"
import { pageRegistry } from "../../../extensions/page-registry"; import { pageRegistry } from "../../../extensions/registries/page-registry";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false }); const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = { type SidebarContextValue = {
@ -80,7 +80,7 @@ export class Sidebar extends React.Component<Props> {
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}> <div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
<div className="header flex align-center"> <div className="header flex align-center">
<NavLink exact to="/" className="box grow"> <NavLink exact to="/" className="box grow">
<Icon svg="logo-full" className="logo-icon" /> <Icon svg="logo-full" className="logo-icon"/>
<div className="logo-text">Lens</div> <div className="logo-text">Lens</div>
</NavLink> </NavLink>
<Icon <Icon
@ -97,14 +97,14 @@ export class Sidebar extends React.Component<Props> {
isHidden={!isAllowedResource("nodes")} isHidden={!isAllowedResource("nodes")}
url={clusterURL()} url={clusterURL()}
text={<Trans>Cluster</Trans>} text={<Trans>Cluster</Trans>}
icon={<Icon svg="kube" />} icon={<Icon svg="kube"/>}
/> />
<SidebarNavItem <SidebarNavItem
id="nodes" id="nodes"
isHidden={!isAllowedResource("nodes")} isHidden={!isAllowedResource("nodes")}
url={nodesURL()} url={nodesURL()}
text={<Trans>Nodes</Trans>} text={<Trans>Nodes</Trans>}
icon={<Icon svg="nodes" />} icon={<Icon svg="nodes"/>}
/> />
<SidebarNavItem <SidebarNavItem
id="workloads" id="workloads"
@ -113,7 +113,7 @@ export class Sidebar extends React.Component<Props> {
routePath={workloadsRoute.path} routePath={workloadsRoute.path}
subMenus={Workloads.tabRoutes} subMenus={Workloads.tabRoutes}
text={<Trans>Workloads</Trans>} text={<Trans>Workloads</Trans>}
icon={<Icon svg="workloads" />} icon={<Icon svg="workloads"/>}
/> />
<SidebarNavItem <SidebarNavItem
id="config" id="config"
@ -122,7 +122,7 @@ export class Sidebar extends React.Component<Props> {
routePath={configRoute.path} routePath={configRoute.path}
subMenus={Config.tabRoutes} subMenus={Config.tabRoutes}
text={<Trans>Configuration</Trans>} text={<Trans>Configuration</Trans>}
icon={<Icon material="list" />} icon={<Icon material="list"/>}
/> />
<SidebarNavItem <SidebarNavItem
id="networks" id="networks"
@ -131,7 +131,7 @@ export class Sidebar extends React.Component<Props> {
routePath={networkRoute.path} routePath={networkRoute.path}
subMenus={Network.tabRoutes} subMenus={Network.tabRoutes}
text={<Trans>Network</Trans>} text={<Trans>Network</Trans>}
icon={<Icon material="device_hub" />} icon={<Icon material="device_hub"/>}
/> />
<SidebarNavItem <SidebarNavItem
id="storage" id="storage"
@ -139,14 +139,14 @@ export class Sidebar extends React.Component<Props> {
url={storageURL({ query })} url={storageURL({ query })}
routePath={storageRoute.path} routePath={storageRoute.path}
subMenus={Storage.tabRoutes} subMenus={Storage.tabRoutes}
icon={<Icon svg="storage" />} icon={<Icon svg="storage"/>}
text={<Trans>Storage</Trans>} text={<Trans>Storage</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
id="namespaces" id="namespaces"
isHidden={!isAllowedResource("namespaces")} isHidden={!isAllowedResource("namespaces")}
url={namespacesURL()} url={namespacesURL()}
icon={<Icon material="layers" />} icon={<Icon material="layers"/>}
text={<Trans>Namespaces</Trans>} text={<Trans>Namespaces</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -154,7 +154,7 @@ export class Sidebar extends React.Component<Props> {
isHidden={!isAllowedResource("events")} isHidden={!isAllowedResource("events")}
url={eventsURL({ query })} url={eventsURL({ query })}
routePath={eventRoute.path} routePath={eventRoute.path}
icon={<Icon material="access_time" />} icon={<Icon material="access_time"/>}
text={<Trans>Events</Trans>} text={<Trans>Events</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -162,7 +162,7 @@ export class Sidebar extends React.Component<Props> {
url={appsURL({ query })} url={appsURL({ query })}
subMenus={Apps.tabRoutes} subMenus={Apps.tabRoutes}
routePath={appsRoute.path} routePath={appsRoute.path}
icon={<Icon material="apps" />} icon={<Icon material="apps"/>}
text={<Trans>Apps</Trans>} text={<Trans>Apps</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -170,7 +170,7 @@ export class Sidebar extends React.Component<Props> {
url={usersManagementURL({ query })} url={usersManagementURL({ query })}
routePath={usersManagementRoute.path} routePath={usersManagementRoute.path}
subMenus={UserManagement.tabRoutes} subMenus={UserManagement.tabRoutes}
icon={<Icon material="security" />} icon={<Icon material="security"/>}
text={<Trans>Access Control</Trans>} text={<Trans>Access Control</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -179,17 +179,17 @@ export class Sidebar extends React.Component<Props> {
url={crdURL()} url={crdURL()}
subMenus={CustomResources.tabRoutes} subMenus={CustomResources.tabRoutes}
routePath={crdRoute.path} routePath={crdRoute.path}
icon={<Icon material="extension" />} icon={<Icon material="extension"/>}
text={<Trans>Custom Resources</Trans>} text={<Trans>Custom Resources</Trans>}
> >
{this.renderCustomResources()} {this.renderCustomResources()}
</SidebarNavItem> </SidebarNavItem>
{pageRegistry.clusterPages.map(({ path, title, components: { MenuIcon } }) => { {pageRegistry.clusterPages.map(({ path, title, url = String(path), components: { MenuIcon } }) => {
if (!MenuIcon) return;
return ( return (
<SidebarNavItem <SidebarNavItem
key={path} key={url} id={`sidebar_item_${url}`}
id={`extension-${path}`} url={url}
url={path}
routePath={path} routePath={path}
text={title} text={title}
icon={<MenuIcon/>} icon={<MenuIcon/>}
@ -255,7 +255,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}> <div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
{icon} {icon}
<span className="link-text">{text}</span> <span className="link-text">{text}</span>
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} /> <Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}/>
</div> </div>
<ul className={cssNames("sub-menu", { active: isActive })}> <ul className={cssNames("sub-menu", { active: isActive })}>
{subMenus.map(({ title, url }) => ( {subMenus.map(({ title, url }) => (

View File

@ -8,13 +8,9 @@
grid-template-columns: 1fr 40%; grid-template-columns: 1fr 40%;
> * { > * {
@include custom-scrollbar; @include custom-scrollbar-themed;
--flex-gap: #{$spacing}; --flex-gap: #{$spacing};
padding: $spacing; padding: $spacing;
.theme-light & {
@include custom-scrollbar(dark);
}
} }
> .head-col { > .head-col {

View File

@ -6,6 +6,22 @@
@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) { @mixin custom-scrollbar($theme: light, $size: 7px, $borderSpacing: 5px) {
$themes: ( $themes: (
light: #5f6064, light: #5f6064,

View File

@ -11,6 +11,10 @@ export enum TooltipPosition {
BOTTOM = "bottom", BOTTOM = "bottom",
LEFT = "left", LEFT = "left",
RIGHT = "right", RIGHT = "right",
TOP_LEFT = "top_left",
TOP_RIGHT = "top_right",
BOTTOM_LEFT = "bottom_left",
BOTTOM_RIGHT = "bottom_right",
} }
export interface TooltipProps { export interface TooltipProps {
@ -19,7 +23,7 @@ export interface TooltipProps {
visible?: boolean; // initial visibility visible?: boolean; // initial visibility
offset?: number; // offset from target element in pixels (all sides) offset?: number; // offset from target element in pixels (all sides)
usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true
position?: TooltipPosition; preferredPositions?: TooltipPosition | TooltipPosition[];
className?: IClassName; className?: IClassName;
formatters?: TooltipContentFormatters; formatters?: TooltipContentFormatters;
style?: React.CSSProperties; style?: React.CSSProperties;
@ -82,17 +86,25 @@ export class Tooltip extends React.Component<TooltipProps> {
@autobind() @autobind()
refreshPosition() { refreshPosition() {
const { position } = this.props; const { preferredPositions } = this.props;
const { elem, targetElem } = this; const { elem, targetElem } = this;
const positionPreference = new Set<TooltipPosition>(); let positions = new Set<TooltipPosition>([
if (typeof position !== "undefined") { TooltipPosition.RIGHT,
positionPreference.add(position); TooltipPosition.BOTTOM,
TooltipPosition.TOP,
TooltipPosition.LEFT,
TooltipPosition.TOP_RIGHT,
TooltipPosition.TOP_LEFT,
TooltipPosition.BOTTOM_RIGHT,
TooltipPosition.BOTTOM_LEFT,
]);
if (preferredPositions) {
positions = new Set([
...[preferredPositions].flat(),
...positions,
])
} }
positionPreference.add(TooltipPosition.RIGHT)
.add(TooltipPosition.BOTTOM)
.add(TooltipPosition.TOP)
.add(TooltipPosition.LEFT)
// reset position first and get all possible client-rect area for tooltip element // reset position first and get all possible client-rect area for tooltip element
this.setPosition({ left: 0, top: 0 }); this.setPosition({ left: 0, top: 0 });
@ -102,20 +114,20 @@ export class Tooltip extends React.Component<TooltipProps> {
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window; const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
// find proper position // find proper position
for (const pos of positionPreference) { for (const pos of positions) {
const { left, top, right, bottom } = this.getPosition(pos, selfBounds, targetBounds) const { left, top, right, bottom } = this.getPosition(pos, selfBounds, targetBounds)
const fitsToWindow = left >= 0 && top >= 0 && right <= viewportWidth && bottom <= viewportHeight; const fitsToWindow = left >= 0 && top >= 0 && right <= viewportWidth && bottom <= viewportHeight;
if (fitsToWindow) { if (fitsToWindow) {
this.activePosition = pos; this.activePosition = pos;
this.setPosition({ top, left }); this.setPosition({ top, left });
return; return;
} }
} }
const preferedPosition = Array.from(positionPreference)[0]; // apply fallback position if nothing helped from above
const { left, top } = this.getPosition(preferedPosition, selfBounds, targetBounds) const fallbackPosition = Array.from(positions)[0];
this.activePosition = preferedPosition; const { left, top } = this.getPosition(fallbackPosition, selfBounds, targetBounds)
this.activePosition = fallbackPosition;
this.setPosition({ left, top }); this.setPosition({ left, top });
} }
@ -125,35 +137,54 @@ export class Tooltip extends React.Component<TooltipProps> {
elemStyle.top = pos.top + "px" elemStyle.top = pos.top + "px"
} }
protected getPosition(position: TooltipPosition, selfBounds: DOMRect, targetBounds: DOMRect) { protected getPosition(position: TooltipPosition, tooltipBounds: DOMRect, targetBounds: DOMRect) {
let left: number let left: number;
let top: number let top: number;
const offset = this.props.offset; const offset = this.props.offset;
const horizontalCenter = targetBounds.left + (targetBounds.width - selfBounds.width) / 2; const horizontalCenter = targetBounds.left + (targetBounds.width - tooltipBounds.width) / 2;
const verticalCenter = targetBounds.top + (targetBounds.height - selfBounds.height) / 2; const verticalCenter = targetBounds.top + (targetBounds.height - tooltipBounds.height) / 2;
const topCenter = targetBounds.top - tooltipBounds.height - offset;
const bottomCenter = targetBounds.bottom + offset;
switch (position) { switch (position) {
case "top": case "top":
left = horizontalCenter; left = horizontalCenter;
top = targetBounds.top - selfBounds.height - offset; top = topCenter;
break; break;
case "bottom": case "bottom":
left = horizontalCenter; left = horizontalCenter;
top = targetBounds.bottom + offset; top = bottomCenter;
break; break;
case "left": case "left":
top = verticalCenter; top = verticalCenter;
left = targetBounds.left - selfBounds.width - offset; left = targetBounds.left - tooltipBounds.width - offset;
break; break;
case "right": case "right":
top = verticalCenter; top = verticalCenter;
left = targetBounds.right + offset; left = targetBounds.right + offset;
break; break;
case "top_left":
left = targetBounds.left;
top = topCenter;
break;
case "top_right":
default:
left = targetBounds.right - tooltipBounds.width;
top = topCenter;
break;
case "bottom_left":
top = bottomCenter;
left = targetBounds.left;
break;
case "bottom_right":
top = bottomCenter;
left = targetBounds.right - tooltipBounds.width;
break;
} }
return { return {
left: left, left: left,
top: top, top: top,
right: left + selfBounds.width, right: left + tooltipBounds.width,
bottom: top + selfBounds.height, bottom: top + tooltipBounds.height,
}; };
} }

Binary file not shown.

View File

@ -14,7 +14,7 @@ export interface ILanguage {
export const _i18n = setupI18n({ export const _i18n = setupI18n({
missing: (message, id) => { missing: (message, id) => {
console.warn('Missing localization:', message, id); // console.warn('Missing localization:', message, id);
return id; return id;
} }
}); });

View File

@ -12,12 +12,11 @@ import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
import { Notifications } from "./components/notifications"; import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog"; import { ConfirmDialog } from "./components/confirm-dialog";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import { getLensRuntime } from "../extensions/lens-runtime";
@observer @observer
export class LensApp extends React.Component { export class LensApp extends React.Component {
static async init() { static async init() {
extensionLoader.loadOnMainRenderer(getLensRuntime) extensionLoader.loadOnClusterManagerRenderer();
} }
render() { render() {

View File

@ -1,4 +1,4 @@
import { appName, buildDir, extensionsDir, extensionsLibName, extensionsRendererLibName, htmlTemplate, isDevelopment, isProduction, publicPath, rendererDir, sassCommonVars } from "./src/common/vars"; import { appName, buildDir, htmlTemplate, isDevelopment, isProduction, publicPath, rendererDir, sassCommonVars } from "./src/common/vars";
import path from "path"; import path from "path";
import webpack from "webpack"; import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin";

View File

@ -8644,11 +8644,6 @@ matcher@^3.0.0:
dependencies: dependencies:
escape-string-regexp "^4.0.0" escape-string-regexp "^4.0.0"
material-design-icons@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf"
integrity sha1-mnHEh0chjrylHlGmbaaCA4zct78=
md5-file@^5.0.0: md5-file@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-5.0.0.tgz#e519f631feca9c39e7f9ea1780b63c4745012e20" resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-5.0.0.tgz#e519f631feca9c39e7f9ea1780b63c4745012e20"