mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'extensions-api' into extension-cluster-features
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
commit
d2e0c40ea0
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
23
extensions/support-page/main.ts
Normal file
23
extensions/support-page/main.ts
Normal 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
3623
extensions/support-page/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
extensions/support-page/package.json
Normal file
24
extensions/support-page/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
extensions/support-page/renderer.tsx
Normal file
37
extensions/support-page/renderer.tsx
Normal 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())}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
extensions/support-page/src/support.route.ts
Normal file
7
extensions/support-page/src/support.route.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { RouteProps } from "react-router";
|
||||||
|
|
||||||
|
export const supportPageRoute: RouteProps = {
|
||||||
|
path: "/support"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportPageURL = () => supportPageRoute.path.toString();
|
||||||
29
extensions/support-page/src/support.tsx
Normal file
29
extensions/support-page/src/support.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
extensions/support-page/tsconfig.json
Normal file
30
extensions/support-page/tsconfig.json
Normal 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/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
68
extensions/support-page/webpack.config.ts
Normal file
68
extensions/support-page/webpack.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -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": {
|
||||||
|
|||||||
@ -26,5 +26,5 @@
|
|||||||
"renderer.ts",
|
"renderer.ts",
|
||||||
"../../src/extensions/npm/**/*.d.ts",
|
"../../src/extensions/npm/**/*.d.ts",
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -177,7 +177,8 @@
|
|||||||
"telemetry",
|
"telemetry",
|
||||||
"pod-menu",
|
"pod-menu",
|
||||||
"node-menu",
|
"node-menu",
|
||||||
"metrics-cluster-feature"
|
"metrics-cluster-feature",
|
||||||
|
"support-page"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -315,7 +316,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",
|
||||||
|
|||||||
@ -90,13 +90,19 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
const callback = (event: IpcRendererEvent, model: T) => {
|
const callback = (event: IpcRendererEvent, model: T) => {
|
||||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||||
this.onSync(model);
|
this.onSyncFromMain(model);
|
||||||
};
|
};
|
||||||
ipcRenderer.on(this.syncChannel, callback);
|
ipcRenderer.on(this.syncChannel, callback);
|
||||||
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback));
|
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onSyncFromMain(model: T) {
|
||||||
|
this.applyWithoutSync(() => {
|
||||||
|
this.onSync(model)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
unregisterIpcListener() {
|
unregisterIpcListener() {
|
||||||
ipcRenderer.removeAllListeners(this.syncChannel)
|
ipcRenderer.removeAllListeners(this.syncChannel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { createIpcChannel } from "./ipc";
|
|||||||
import { ClusterId, clusterStore } from "./cluster-store";
|
import { ClusterId, clusterStore } from "./cluster-store";
|
||||||
import { extensionLoader } from "../extensions/extension-loader"
|
import { extensionLoader } from "../extensions/extension-loader"
|
||||||
import { appEventBus } from "./event-bus"
|
import { appEventBus } from "./event-bus"
|
||||||
import { clusterFeatureRegistry } from "../extensions/cluster-feature-registry";
|
|
||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
|
|
||||||
export const clusterIpc = {
|
export const clusterIpc = {
|
||||||
@ -32,7 +31,7 @@ export const clusterIpc = {
|
|||||||
channel: "cluster:refresh",
|
channel: "cluster:refresh",
|
||||||
handle: (clusterId: ClusterId) => {
|
handle: (clusterId: ClusterId) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
if (cluster) return cluster.refresh();
|
if (cluster) return cluster.refresh({ refreshMetadata: true })
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,10 @@ export interface ClusterIconUpload {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClusterMetadata {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClusterStoreModel {
|
export interface ClusterStoreModel {
|
||||||
activeCluster?: ClusterId; // last opened cluster
|
activeCluster?: ClusterId; // last opened cluster
|
||||||
clusters?: ClusterModel[]
|
clusters?: ClusterModel[]
|
||||||
@ -32,6 +36,7 @@ export interface ClusterModel {
|
|||||||
workspace?: WorkspaceId;
|
workspace?: WorkspaceId;
|
||||||
contextName?: string;
|
contextName?: string;
|
||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
|
metadata?: ClusterMetadata;
|
||||||
kubeConfigPath: string;
|
kubeConfigPath: string;
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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()
|
|
||||||
22
src/extensions/core-api/index.ts
Normal file
22
src/extensions/core-api/index.ts
Normal 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,
|
||||||
|
}
|
||||||
@ -1,5 +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"
|
|
||||||
export type { ClusterFeatureRegistry } from "../cluster-feature-registry"
|
|
||||||
@ -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() {
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -1,15 +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, clusterFeatureRegistry } from "./registries";
|
||||||
import { appPreferenceRegistry } from "./app-preference-registry"
|
|
||||||
import { kubeObjectMenuRegistry } from "../renderer/api/kube-object-menu-registry"
|
|
||||||
import { clusterFeatureRegistry } from "./cluster-feature-registry"
|
|
||||||
|
|
||||||
export interface InstalledExtension extends ExtensionModel {
|
export interface InstalledExtension extends ExtensionModel {
|
||||||
manifestPath: string;
|
manifestPath: string;
|
||||||
@ -37,31 +34,32 @@ 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.registerClusterFeatures(clusterFeatureRegistry)
|
||||||
|
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)
|
|
||||||
instance.registerClusterFeatures(clusterFeatureRegistry)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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.id)
|
let instance = this.instances.get(ext.id)
|
||||||
@ -73,7 +71,7 @@ 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);
|
||||||
try {
|
try {
|
||||||
instance.enable(getLensRuntimeEnv())
|
instance.enable()
|
||||||
callback(instance)
|
callback(instance)
|
||||||
} finally {
|
} finally {
|
||||||
this.instances.set(ext.id, instance)
|
this.instances.set(ext.id, instance)
|
||||||
@ -111,7 +109,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}`);
|
||||||
|
|||||||
@ -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) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { LensExtension } from "./lens-extension"
|
import { LensExtension } from "./lens-extension"
|
||||||
import type { PageRegistry } from "./page-registry"
|
import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
|
||||||
import type { AppPreferenceRegistry } from "./app-preference-registry";
|
|
||||||
import type { KubeObjectMenuRegistry } from "../renderer/api/kube-object-menu-registry";
|
|
||||||
import type { ClusterFeatureRegistry } from "./core-api/registries";
|
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
|
|
||||||
registerPages(registry: PageRegistry) {
|
registerPages(registry: PageRegistry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -18,6 +14,10 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerStatusBarIcon(registry: StatusBarRegistry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
|
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
17
src/extensions/registries/app-preference-registry.ts
Normal file
17
src/extensions/registries/app-preference-registry.ts
Normal 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()
|
||||||
17
src/extensions/registries/base-registry.ts
Normal file
17
src/extensions/registries/base-registry.ts
Normal 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};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { observable } from "mobx"
|
import { observable } from "mobx"
|
||||||
import { ClusterFeature } from "./cluster-feature";
|
import { ClusterFeature } from "../cluster-feature";
|
||||||
|
|
||||||
export interface ClusterFeatureComponents {
|
export interface ClusterFeatureComponents {
|
||||||
Description: React.ComponentType<any>;
|
Description: React.ComponentType<any>;
|
||||||
8
src/extensions/registries/index.ts
Normal file
8
src/extensions/registries/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 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";
|
||||||
|
export * from "./cluster-feature-registry"
|
||||||
@ -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)
|
||||||
14
src/extensions/registries/menu-registry.ts
Normal file
14
src/extensions/registries/menu-registry.ts
Normal 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();
|
||||||
40
src/extensions/registries/page-registry.ts
Normal file
40
src/extensions/registries/page-registry.ts
Normal 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();
|
||||||
13
src/extensions/registries/status-bar-registry.ts
Normal file
13
src/extensions/registries/status-bar-registry.ts
Normal 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();
|
||||||
@ -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
|
||||||
|
|||||||
12
src/extensions/renderer-api/index.ts
Normal file
12
src/extensions/renderer-api/index.ts
Normal 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,
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -38,6 +38,7 @@ import { getFreePort } from "../port";
|
|||||||
import { V1ResourceAttributes } from "@kubernetes/client-node";
|
import { V1ResourceAttributes } from "@kubernetes/client-node";
|
||||||
import { apiResources } from "../../common/rbac";
|
import { apiResources } from "../../common/rbac";
|
||||||
import request from "request-promise-native"
|
import request from "request-promise-native"
|
||||||
|
import { Kubectl } from "../kubectl";
|
||||||
|
|
||||||
const mockedRequest = request as jest.MockedFunction<typeof request>
|
const mockedRequest = request as jest.MockedFunction<typeof request>
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ describe("create clusters", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
mockFs(mockOpts)
|
mockFs(mockOpts)
|
||||||
|
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true))
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -116,7 +118,7 @@ describe("create clusters", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true))
|
||||||
jest.spyOn(Cluster.prototype, "canI")
|
jest.spyOn(Cluster.prototype, "canI")
|
||||||
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default")
|
expect(attr.namespace).toBe("default")
|
||||||
@ -159,7 +161,7 @@ describe("create clusters", () => {
|
|||||||
expect(c.accessible).toBe(true)
|
expect(c.accessible).toBe(true)
|
||||||
expect(c.allowedNamespaces.length).toBe(1)
|
expect(c.allowedNamespaces.length).toBe(1)
|
||||||
expect(c.allowedResources.length).toBe(apiResources.length)
|
expect(c.allowedResources.length).toBe(apiResources.length)
|
||||||
|
c.disconnect()
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
33
src/main/cluster-detectors/base-cluster-detector.ts
Normal file
33
src/main/cluster-detectors/base-cluster-detector.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||||
|
import { Cluster } from "../cluster";
|
||||||
|
|
||||||
|
export type ClusterDetectionResult = {
|
||||||
|
value: string | number | boolean
|
||||||
|
accuracy: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseClusterDetector {
|
||||||
|
cluster: Cluster
|
||||||
|
key: string
|
||||||
|
|
||||||
|
constructor(cluster: Cluster) {
|
||||||
|
this.cluster = cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
detect(): Promise<ClusterDetectionResult> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||||
|
const apiUrl = this.cluster.kubeProxyUrl + path;
|
||||||
|
return request(apiUrl, {
|
||||||
|
json: true,
|
||||||
|
timeout: 30000,
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/cluster-detectors/cluster-id-detector.ts
Normal file
23
src/main/cluster-detectors/cluster-id-detector.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
|
import { createHash } from "crypto"
|
||||||
|
import { ClusterMetadataKey } from "../cluster";
|
||||||
|
|
||||||
|
export class ClusterIdDetector extends BaseClusterDetector {
|
||||||
|
key = ClusterMetadataKey.CLUSTER_ID
|
||||||
|
|
||||||
|
public async detect() {
|
||||||
|
let id: string
|
||||||
|
try {
|
||||||
|
id = await this.getDefaultNamespaceId()
|
||||||
|
} catch(_) {
|
||||||
|
id = this.cluster.apiUrl
|
||||||
|
}
|
||||||
|
const value = createHash("sha256").update(id).digest("hex")
|
||||||
|
return { value: value, accuracy: 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getDefaultNamespaceId() {
|
||||||
|
const response = await this.k8sRequest("/api/v1/namespaces/default")
|
||||||
|
return response.metadata.uid
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main/cluster-detectors/detector-registry.ts
Normal file
45
src/main/cluster-detectors/detector-registry.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
|
import { ClusterMetadata } from "../../common/cluster-store";
|
||||||
|
import { Cluster } from "../cluster";
|
||||||
|
import { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||||
|
import { ClusterIdDetector } from "./cluster-id-detector";
|
||||||
|
import { DistributionDetector } from "./distribution-detector";
|
||||||
|
import { LastSeenDetector } from "./last-seen-detector";
|
||||||
|
import { NodesCountDetector } from "./nodes-count-detector";
|
||||||
|
import { VersionDetector } from "./version-detector";
|
||||||
|
|
||||||
|
export class DetectorRegistry {
|
||||||
|
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
|
||||||
|
|
||||||
|
add(detectorClass: typeof BaseClusterDetector) {
|
||||||
|
this.registry.push(detectorClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
|
||||||
|
const results: {[key: string]: ClusterDetectionResult } = {}
|
||||||
|
for (const detectorClass of this.registry) {
|
||||||
|
const detector = new detectorClass(cluster)
|
||||||
|
try {
|
||||||
|
const data = await detector.detect()
|
||||||
|
if (!data) continue;
|
||||||
|
const existingValue = results[detector.key]
|
||||||
|
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
|
||||||
|
results[detector.key] = data
|
||||||
|
} catch (e) {
|
||||||
|
// detector raised error, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const metadata: ClusterMetadata = {}
|
||||||
|
for (const [key, result] of Object.entries(results)) {
|
||||||
|
metadata[key] = result.value
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const detectorRegistry = new DetectorRegistry()
|
||||||
|
detectorRegistry.add(ClusterIdDetector)
|
||||||
|
detectorRegistry.add(LastSeenDetector)
|
||||||
|
detectorRegistry.add(VersionDetector)
|
||||||
|
detectorRegistry.add(DistributionDetector)
|
||||||
|
detectorRegistry.add(NodesCountDetector)
|
||||||
80
src/main/cluster-detectors/distribution-detector.ts
Normal file
80
src/main/cluster-detectors/distribution-detector.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
|
import { ClusterMetadataKey } from "../cluster";
|
||||||
|
|
||||||
|
export class DistributionDetector extends BaseClusterDetector {
|
||||||
|
key = ClusterMetadataKey.DISTRIBUTION
|
||||||
|
version: string
|
||||||
|
|
||||||
|
public async detect() {
|
||||||
|
this.version = await this.getKubernetesVersion()
|
||||||
|
if (await this.isRancher()) {
|
||||||
|
return { value: "rancher", accuracy: 80}
|
||||||
|
}
|
||||||
|
if (this.isGKE()) {
|
||||||
|
return { value: "gke", accuracy: 80}
|
||||||
|
}
|
||||||
|
if (this.isEKS()) {
|
||||||
|
return { value: "eks", accuracy: 80}
|
||||||
|
}
|
||||||
|
if (this.isIKS()) {
|
||||||
|
return { value: "iks", accuracy: 80}
|
||||||
|
}
|
||||||
|
if (this.isAKS()) {
|
||||||
|
return { value: "aks", accuracy: 80}
|
||||||
|
}
|
||||||
|
if (this.isDigitalOcean()) {
|
||||||
|
return { value: "digitalocean", accuracy: 90}
|
||||||
|
}
|
||||||
|
if (this.isMinikube()) {
|
||||||
|
return { value: "minikube", accuracy: 80}
|
||||||
|
}
|
||||||
|
if (this.isCustom()) {
|
||||||
|
return { value: "custom", accuracy: 10}
|
||||||
|
}
|
||||||
|
return { value: "vanilla", accuracy: 10}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getKubernetesVersion() {
|
||||||
|
if (this.cluster.version) return this.cluster.version
|
||||||
|
|
||||||
|
const response = await this.k8sRequest("/version")
|
||||||
|
return response.gitVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isGKE() {
|
||||||
|
return this.version.includes("gke")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isEKS() {
|
||||||
|
return this.version.includes("eks")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isIKS() {
|
||||||
|
return this.version.includes("IKS")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isAKS() {
|
||||||
|
return this.cluster.apiUrl.endsWith("azmk8s.io")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isDigitalOcean() {
|
||||||
|
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isMinikube() {
|
||||||
|
return this.cluster.contextName.startsWith("minikube")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isCustom() {
|
||||||
|
return this.version.includes("+")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async isRancher() {
|
||||||
|
try {
|
||||||
|
const response = await this.k8sRequest("")
|
||||||
|
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/cluster-detectors/last-seen-detector.ts
Normal file
13
src/main/cluster-detectors/last-seen-detector.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
|
import { ClusterMetadataKey } from "../cluster";
|
||||||
|
|
||||||
|
export class LastSeenDetector extends BaseClusterDetector {
|
||||||
|
key = ClusterMetadataKey.LAST_SEEN
|
||||||
|
|
||||||
|
public async detect() {
|
||||||
|
if (!this.cluster.accessible) return null;
|
||||||
|
|
||||||
|
await this.k8sRequest("/version")
|
||||||
|
return { value: new Date().toJSON(), accuracy: 100 }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/cluster-detectors/nodes-count-detector.ts
Normal file
18
src/main/cluster-detectors/nodes-count-detector.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
|
import { ClusterMetadataKey } from "../cluster";
|
||||||
|
|
||||||
|
export class NodesCountDetector extends BaseClusterDetector {
|
||||||
|
key = ClusterMetadataKey.NODES_COUNT
|
||||||
|
|
||||||
|
public async detect() {
|
||||||
|
const nodeCount = await this.getNodeCount()
|
||||||
|
return { value: nodeCount, accuracy: 100}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getNodeCount(): Promise<number> {
|
||||||
|
if (!this.cluster.accessible) return null;
|
||||||
|
|
||||||
|
const response = await this.k8sRequest("/api/v1/nodes")
|
||||||
|
return response.items.length
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/cluster-detectors/version-detector.ts
Normal file
17
src/main/cluster-detectors/version-detector.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
|
import { ClusterMetadataKey } from "../cluster";
|
||||||
|
|
||||||
|
export class VersionDetector extends BaseClusterDetector {
|
||||||
|
key = ClusterMetadataKey.VERSION
|
||||||
|
value: string
|
||||||
|
|
||||||
|
public async detect() {
|
||||||
|
const version = await this.getKubernetesVersion()
|
||||||
|
return { value: version, accuracy: 100}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getKubernetesVersion() {
|
||||||
|
const response = await this.k8sRequest("/version")
|
||||||
|
return response.gitVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||||
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||||
import type { WorkspaceId } from "../common/workspace-store";
|
import type { WorkspaceId } from "../common/workspace-store";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
@ -12,6 +12,8 @@ import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/ku
|
|||||||
import request, { RequestPromiseOptions } from "request-promise-native"
|
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||||
import { apiResources } from "../common/rbac";
|
import { apiResources } from "../common/rbac";
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||||
|
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
@ -19,6 +21,18 @@ export enum ClusterStatus {
|
|||||||
Offline = 0
|
Offline = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ClusterMetadataKey {
|
||||||
|
VERSION = "version",
|
||||||
|
CLUSTER_ID = "id",
|
||||||
|
DISTRIBUTION = "distribution",
|
||||||
|
NODES_COUNT = "nodes",
|
||||||
|
LAST_SEEN = "lastSeen"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClusterRefreshOptions = {
|
||||||
|
refreshMetadata?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClusterState extends ClusterModel {
|
export interface ClusterState extends ClusterModel {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
@ -27,10 +41,7 @@ export interface ClusterState extends ClusterModel {
|
|||||||
accessible: boolean;
|
accessible: boolean;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
failureReason: string;
|
failureReason: string;
|
||||||
nodes: number;
|
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
version: string;
|
|
||||||
distribution: string;
|
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
allowedNamespaces: string[]
|
allowedNamespaces: string[]
|
||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
@ -60,18 +71,19 @@ export class Cluster implements ClusterModel {
|
|||||||
@observable reconnecting = false;
|
@observable reconnecting = false;
|
||||||
@observable disconnected = true;
|
@observable disconnected = true;
|
||||||
@observable failureReason: string;
|
@observable failureReason: string;
|
||||||
@observable nodes = 0;
|
|
||||||
@observable version: string;
|
|
||||||
@observable distribution = "unknown";
|
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
@observable eventCount = 0;
|
@observable eventCount = 0;
|
||||||
@observable preferences: ClusterPreferences = {};
|
@observable preferences: ClusterPreferences = {};
|
||||||
|
@observable metadata: ClusterMetadata = {};
|
||||||
@observable allowedNamespaces: string[] = [];
|
@observable allowedNamespaces: string[] = [];
|
||||||
@observable allowedResources: string[] = [];
|
@observable allowedResources: string[] = [];
|
||||||
|
|
||||||
@computed get available() {
|
@computed get available() {
|
||||||
return this.accessible && !this.disconnected;
|
return this.accessible && !this.disconnected;
|
||||||
}
|
}
|
||||||
|
get version(): string {
|
||||||
|
return String(this.metadata?.version) || ""
|
||||||
|
}
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
@ -109,10 +121,14 @@ export class Cluster implements ClusterModel {
|
|||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||||
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
||||||
|
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
|
||||||
|
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
reaction(this.getState, this.pushState),
|
reaction(this.getState, this.pushState),
|
||||||
() => clearInterval(refreshTimer),
|
() => {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
clearInterval(refreshMetadataTimer);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +154,7 @@ export class Cluster implements ClusterModel {
|
|||||||
await this.refreshConnectionStatus()
|
await this.refreshConnectionStatus()
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAllowedResources()
|
await this.refreshAllowedResources()
|
||||||
|
this.isAdmin = await this.isClusterAdmin()
|
||||||
this.ready = true
|
this.ready = true
|
||||||
this.kubeCtl = new Kubectl(this.version)
|
this.kubeCtl = new Kubectl(this.version)
|
||||||
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
|
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
|
||||||
@ -168,27 +185,32 @@ export class Cluster implements ClusterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async refresh() {
|
async refresh(opts: ClusterRefreshOptions = {}) {
|
||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
const [isAdmin, nodesCount] = await Promise.all([
|
|
||||||
this.isClusterAdmin(),
|
|
||||||
this.getNodeCount(),
|
|
||||||
]);
|
|
||||||
this.isAdmin = isAdmin;
|
|
||||||
this.nodes = nodesCount;
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.refreshEvents(),
|
this.refreshEvents(),
|
||||||
this.refreshAllowedResources(),
|
this.refreshAllowedResources(),
|
||||||
]);
|
]);
|
||||||
|
if (opts.refreshMetadata) {
|
||||||
|
this.refreshMetadata()
|
||||||
|
}
|
||||||
this.ready = true
|
this.ready = true
|
||||||
}
|
}
|
||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async refreshMetadata() {
|
||||||
|
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
|
const metadata = await detectorRegistry.detectForCluster(this)
|
||||||
|
const existingMetadata = this.metadata
|
||||||
|
this.metadata = Object.assign(existingMetadata, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async refreshConnectionStatus() {
|
async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
@ -245,9 +267,9 @@ export class Cluster implements ClusterModel {
|
|||||||
|
|
||||||
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
||||||
try {
|
try {
|
||||||
const response = await this.k8sRequest("/version")
|
const versionDetector = new VersionDetector(this)
|
||||||
this.version = response.gitVersion
|
const versionData = await versionDetector.detect()
|
||||||
this.failureReason = null
|
this.metadata.version = versionData.value
|
||||||
return ClusterStatus.AccessGranted;
|
return ClusterStatus.AccessGranted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`)
|
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`)
|
||||||
@ -296,27 +318,6 @@ export class Cluster implements ClusterModel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
protected detectKubernetesDistribution(kubernetesVersion: string): string {
|
|
||||||
if (kubernetesVersion.includes("gke")) return "gke"
|
|
||||||
if (kubernetesVersion.includes("eks")) return "eks"
|
|
||||||
if (kubernetesVersion.includes("IKS")) return "iks"
|
|
||||||
if (this.apiUrl.endsWith("azmk8s.io")) return "aks"
|
|
||||||
if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
|
|
||||||
if (this.contextName.startsWith("minikube")) return "minikube"
|
|
||||||
if (kubernetesVersion.includes("+")) return "custom"
|
|
||||||
return "vanilla"
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getNodeCount(): Promise<number> {
|
|
||||||
try {
|
|
||||||
const response = await this.k8sRequest("/api/v1/nodes")
|
|
||||||
return response.items.length
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(`failed to request node list: ${error.message}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getEventCount(): Promise<number> {
|
protected async getEventCount(): Promise<number> {
|
||||||
if (!this.isAdmin) {
|
if (!this.isAdmin) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -359,6 +360,7 @@ export class Cluster implements ClusterModel {
|
|||||||
kubeConfigPath: this.kubeConfigPath,
|
kubeConfigPath: this.kubeConfigPath,
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
|
metadata: this.metadata,
|
||||||
};
|
};
|
||||||
return toJS(model, {
|
return toJS(model, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
@ -376,9 +378,6 @@ export class Cluster implements ClusterModel {
|
|||||||
disconnected: this.disconnected,
|
disconnected: this.disconnected,
|
||||||
accessible: this.accessible,
|
accessible: this.accessible,
|
||||||
failureReason: this.failureReason,
|
failureReason: this.failureReason,
|
||||||
nodes: this.nodes,
|
|
||||||
version: this.version,
|
|
||||||
distribution: this.distribution,
|
|
||||||
isAdmin: this.isAdmin,
|
isAdmin: this.isAdmin,
|
||||||
eventCount: this.eventCount,
|
eventCount: this.eventCount,
|
||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { InstallFeature } from "./components/install-feature";
|
import { InstallFeature } from "./components/install-feature";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { clusterFeatureRegistry } from "../../../extensions/cluster-feature-registry";
|
import { clusterFeatureRegistry } from "../../../extensions/registries/cluster-feature-registry";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
|
|||||||
@ -21,10 +21,10 @@ export class Status extends React.Component<Props> {
|
|||||||
const { cluster } = this.props;
|
const { cluster } = this.props;
|
||||||
const rows = [
|
const rows = [
|
||||||
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
|
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
|
||||||
["Distribution", cluster.distribution],
|
["Distribution", cluster.metadata.distribution ? String(cluster.metadata.distribution) : "N/A"],
|
||||||
["Kernel Version", cluster.version],
|
["Kernel Version", cluster.metadata.version ? String(cluster.metadata.version) : "N/A"],
|
||||||
["API Address", cluster.apiUrl],
|
["API Address", cluster.apiUrl || "N/A"],
|
||||||
["Nodes Count", cluster.nodes || "0"]
|
["Nodes Count", cluster.metadata.nodes ? String(cluster.metadata.nodes) : "N/A"]
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Table scrollable={false}>
|
<Table scrollable={false}>
|
||||||
|
|||||||
@ -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)}`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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}/>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -67,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .dynamic-pages {
|
> .extensions {
|
||||||
&:not(:empty) {
|
&:not(:empty) {
|
||||||
padding-top: $spacing;
|
padding-top: $spacing;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
64
src/renderer/components/layout/page-layout.scss
Normal file
64
src/renderer/components/layout/page-layout.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/renderer/components/layout/page-layout.tsx
Normal file
82
src/renderer/components/layout/page-layout.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 }) => (
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
src/renderer/fonts/MaterialIcons-Regular.ttf
Normal file
BIN
src/renderer/fonts/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user