mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Tray icon (#1005)
* Tray icon #833 -- part 1 Signed-off-by: Roman <ixrock@gmail.com> * Tray icon #833 -- part 2 Signed-off-by: Roman <ixrock@gmail.com> * Tray icon #833 -- part 3 Signed-off-by: Roman <ixrock@gmail.com> * Tray icon #833 -- part 4 Signed-off-by: Roman <ixrock@gmail.com> * fix: lint / linux build failed Signed-off-by: Roman <ixrock@gmail.com> * allow to disable tray from preferences Signed-off-by: Roman <ixrock@gmail.com> * allow to tweak svg-icon before applying as tray-icon Signed-off-by: Roman <ixrock@gmail.com> * add checkbox indication, setActive workspace on cluster select Signed-off-by: Roman <ixrock@gmail.com> * fix build version (cannon find module 'react') Signed-off-by: Roman <ixrock@gmail.com> * - switching dark/light icon depending on os-x theme settings - optimization: don't re-create tray icon on menu udpates (avoid blinking) Signed-off-by: Roman <ixrock@gmail.com> * fix: refresh icon after turning on/off + switching dark-mode Signed-off-by: Roman <ixrock@gmail.com> * allow to close main window and re-open from dock or tray icon Signed-off-by: Roman <ixrock@gmail.com> * small fix Signed-off-by: Roman <ixrock@gmail.com> * fix: ensure main-window from global menu Signed-off-by: Roman <ixrock@gmail.com> * chore Signed-off-by: Roman <ixrock@gmail.com> * fix: hide traffic-light buttons for tray window Signed-off-by: Roman <ixrock@gmail.com> * removed redundant tray window Signed-off-by: Roman <ixrock@gmail.com> * removed delay from base-store Signed-off-by: Roman <ixrock@gmail.com> * adding cluster fix (reverted changes from master) Signed-off-by: Roman <ixrock@gmail.com> * - hide icon in dock when main-window closed (mac-os only) - added preferences checkbox to open app at system start-up Signed-off-by: Roman <ixrock@gmail.com> * handle quit app action from tray menu Signed-off-by: Roman <ixrock@gmail.com> * moved generating tray icons to build step Signed-off-by: Roman <ixrock@gmail.com> * Fix integration tests (#1080) * Fix integration tests Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> * Update integration/helpers/utils.ts Co-authored-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> * fix-build: invisible app icon when there are more files within "build/icons/*.png" Signed-off-by: Roman <ixrock@gmail.com> * chore Signed-off-by: Roman <ixrock@gmail.com> * yarn i18n.extract Signed-off-by: Roman <ixrock@gmail.com> * clean-up Signed-off-by: Roman <ixrock@gmail.com> * navigation refactoring, move out `buildUrl` to common/utils so `react` and `react-router` not required as package.json dependecies in runtime (main) Signed-off-by: Roman <ixrock@gmail.com> * Ignore namespace query param on integration tests (#1109) Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> * merge-conflicts fixes Signed-off-by: Roman <ixrock@gmail.com> * support page fixes Signed-off-by: Roman <ixrock@gmail.com> * make eslint happy again Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
74 files changed
+939
-417
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote, webFrame, webContents } from "electron";
|
||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
@@ -113,7 +113,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
@action
|
||||
setActive(id: ClusterId) {
|
||||
this.activeClusterId = id;
|
||||
this.activeClusterId = this.clusters.has(id) ? id : null;
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -160,7 +160,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
if (cluster) {
|
||||
this.clusters.delete(clusterId);
|
||||
if (this.activeClusterId === clusterId) {
|
||||
this.activeClusterId = null;
|
||||
this.setActive(null);
|
||||
}
|
||||
// remove only custom kubeconfigs (pasted as text)
|
||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface UserPreferences {
|
||||
downloadKubectlBinaries?: boolean;
|
||||
downloadBinariesPath?: string;
|
||||
kubectlBinariesPath?: string;
|
||||
openAtLogin?: boolean;
|
||||
}
|
||||
|
||||
export class UserStore extends BaseStore<UserStoreModel> {
|
||||
@@ -38,14 +39,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
migrations: migrations,
|
||||
});
|
||||
|
||||
// track telemetry availability
|
||||
reaction(() => this.preferences.allowTelemetry, allowed => {
|
||||
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
|
||||
});
|
||||
|
||||
// refresh new contexts
|
||||
this.whenLoaded.then(this.refreshNewContexts);
|
||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
||||
this.handleOnLoad();
|
||||
}
|
||||
|
||||
@observable lastSeenAppVersion = "0.0.0"
|
||||
@@ -59,8 +53,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
colorTheme: UserStore.defaultTheme,
|
||||
downloadMirror: "default",
|
||||
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
|
||||
openAtLogin: true,
|
||||
};
|
||||
|
||||
protected async handleOnLoad() {
|
||||
await this.whenLoaded;
|
||||
|
||||
// refresh new contexts
|
||||
this.refreshNewContexts();
|
||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
||||
|
||||
if (app) {
|
||||
// track telemetry availability
|
||||
reaction(() => this.preferences.allowTelemetry, allowed => {
|
||||
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
|
||||
});
|
||||
|
||||
// open at system start-up
|
||||
reaction(() => this.preferences.openAtLogin, open => {
|
||||
app.setLoginItemSettings({ openAtLogin: open });
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get isNewVersion() {
|
||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { compile } from "path-to-regexp"
|
||||
|
||||
export interface IURLParams<P extends object = {}, Q extends object = {}> {
|
||||
params?: P;
|
||||
query?: Q;
|
||||
}
|
||||
|
||||
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
|
||||
const pathBuilder = compile(String(path));
|
||||
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
||||
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""
|
||||
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "")
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import { clusterStore } from "./cluster-store"
|
||||
import { landingURL } from "../renderer/components/+landing-page/landing-page.route";
|
||||
import { navigate } from "../renderer/navigation";
|
||||
import { appEventBus } from "./event-bus";
|
||||
|
||||
export type WorkspaceId = string;
|
||||
@@ -57,18 +55,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) {
|
||||
setActive(id = WorkspaceStore.defaultId, reset = true) {
|
||||
if (id === this.currentWorkspaceId) return;
|
||||
if (!this.getById(id)) {
|
||||
throw new Error(`workspace ${id} doesn't exist`);
|
||||
}
|
||||
this.currentWorkspaceId = id;
|
||||
if (resetActiveCluster) {
|
||||
clusterStore.setActive(null)
|
||||
}
|
||||
if (redirectToLanding) {
|
||||
navigate(landingURL())
|
||||
}
|
||||
clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { app } from "electron";
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
|
||||
export const version = getAppVersion()
|
||||
export { isSnap, isWindows, isMac, isLinux, appName } from "../../common/vars"
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"
|
||||
@@ -10,9 +10,9 @@ 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";
|
||||
import * as ClusterFeature from "./cluster-feature"
|
||||
|
||||
// TODO: allow to expose windowManager.navigate() as Navigation.navigate() in runtime
|
||||
export let windowManager: WindowManager;
|
||||
|
||||
export {
|
||||
@@ -22,5 +22,4 @@ export {
|
||||
Store,
|
||||
Util,
|
||||
Registry,
|
||||
CommonVars,
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import { autoUpdater } from "electron-updater"
|
||||
import logger from "./logger"
|
||||
|
||||
export default class AppUpdater {
|
||||
export class AppUpdater {
|
||||
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day
|
||||
|
||||
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day
|
||||
static checkForUpdates() {
|
||||
return autoUpdater.checkForUpdatesAndNotify()
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
|
||||
autoUpdater.logger = logger
|
||||
}
|
||||
|
||||
public start() {
|
||||
setInterval(() => {
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
}, this.updateInterval)
|
||||
|
||||
return autoUpdater.checkForUpdatesAndNotify()
|
||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval)
|
||||
return AppUpdater.checkForUpdates();
|
||||
}
|
||||
}
|
||||
+35
-28
@@ -10,7 +10,7 @@ import path from "path"
|
||||
import { LensProxy } from "./lens-proxy"
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import AppUpdater from "./app-updater"
|
||||
import { AppUpdater } from "./app-updater"
|
||||
import { shellSync } from "./shell-sync"
|
||||
import { getFreePort } from "./port"
|
||||
import { mangleProxyEnv } from "./proxy-env"
|
||||
@@ -24,45 +24,46 @@ import { extensionLoader } from "../extensions/extension-loader";
|
||||
import logger from "./logger"
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
let proxyPort: number;
|
||||
let proxyServer: LensProxy;
|
||||
let clusterManager: ClusterManager;
|
||||
let windowManager: WindowManager;
|
||||
|
||||
app.setName(appName);
|
||||
if (!process.env.CICD) {
|
||||
app.setPath("userData", workingDir);
|
||||
}
|
||||
|
||||
let clusterManager: ClusterManager;
|
||||
let proxyServer: LensProxy;
|
||||
|
||||
mangleProxyEnv()
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await shellSync();
|
||||
app.on("ready", async () => {
|
||||
logger.info(`🚀 Starting Lens from "${workingDir}"`)
|
||||
await shellSync();
|
||||
|
||||
const updater = new AppUpdater()
|
||||
updater.start();
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
// find free port
|
||||
let proxyPort: number
|
||||
try {
|
||||
proxyPort = await getFreePort()
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// preload configuration from stores
|
||||
// preload isomorphic stores
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
clusterStore.load(),
|
||||
workspaceStore.load(),
|
||||
]);
|
||||
|
||||
// find free port
|
||||
try {
|
||||
proxyPort = await getFreePort()
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// create cluster manager
|
||||
clusterManager = new ClusterManager(proxyPort);
|
||||
|
||||
@@ -72,28 +73,34 @@ async function main() {
|
||||
} catch (error) {
|
||||
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
|
||||
app.quit();
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// create window manager and open app
|
||||
LensExtensionsApi.windowManager = new WindowManager(proxyPort);
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
|
||||
LensExtensionsApi.windowManager = windowManager; // expose to extensions
|
||||
extensionLoader.loadOnMain()
|
||||
extensionLoader.extensions.replace(await extensionManager.load())
|
||||
extensionLoader.broadcastExtensions()
|
||||
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({name: "app", action: "start"})
|
||||
appEventBus.emit({ name: "app", action: "start" })
|
||||
}, 1000)
|
||||
}
|
||||
});
|
||||
|
||||
app.on("ready", main);
|
||||
app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info('APP:ACTIVATE', { hasVisibleWindows })
|
||||
if (!hasVisibleWindows) {
|
||||
windowManager.initMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("will-quit", async (event) => {
|
||||
event.preventDefault(); // To allow mixpanel sending to be executed
|
||||
if (proxyServer) proxyServer.close()
|
||||
if (clusterManager) clusterManager.stop()
|
||||
app.exit();
|
||||
// Quit app on Cmd+Q (MacOS)
|
||||
app.on("will-quit", (event) => {
|
||||
logger.info('APP:QUIT');
|
||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||
clusterManager?.stop(); // close cluster connections
|
||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||
})
|
||||
|
||||
// Extensions-api runtime exports
|
||||
|
||||
+31
-26
@@ -12,11 +12,27 @@ import logger from "./logger";
|
||||
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
|
||||
|
||||
export function initMenu(windowManager: WindowManager) {
|
||||
autorun(() => buildMenu(windowManager), {
|
||||
return autorun(() => buildMenu(windowManager), {
|
||||
delay: 100
|
||||
});
|
||||
}
|
||||
|
||||
export function showAbout(browserWindow: BrowserWindow) {
|
||||
const appInfo = [
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Copyright 2020 Mirantis, Inc.`,
|
||||
]
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
detail: appInfo.join("\r\n")
|
||||
})
|
||||
}
|
||||
|
||||
export function buildMenu(windowManager: WindowManager) {
|
||||
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
|
||||
if (isMac) return [];
|
||||
@@ -32,28 +48,9 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
function navigate(url: string) {
|
||||
async function navigate(url: string) {
|
||||
logger.info(`[MENU]: navigating to ${url}`);
|
||||
windowManager.navigate({
|
||||
channel: "menu:navigate",
|
||||
url: url,
|
||||
})
|
||||
}
|
||||
|
||||
function showAbout(browserWindow: BrowserWindow) {
|
||||
const appInfo = [
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Copyright 2020 Mirantis, Inc.`,
|
||||
]
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
detail: appInfo.join("\r\n")
|
||||
})
|
||||
await windowManager.navigate(url);
|
||||
}
|
||||
|
||||
const macAppMenu: MenuItemConstructorOptions = {
|
||||
@@ -80,7 +77,13 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Cmd+Q',
|
||||
click() {
|
||||
app.exit(); // force quit since might be blocked within app.on("will-quit")
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -118,7 +121,9 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
])
|
||||
]),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' } // close current window
|
||||
]
|
||||
};
|
||||
|
||||
@@ -158,7 +163,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click() {
|
||||
windowManager.reload({ channel: "menu:reload" });
|
||||
windowManager.reload();
|
||||
}
|
||||
},
|
||||
{ role: 'toggleDevTools' },
|
||||
@@ -209,7 +214,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
// Modify menu from extensions-api
|
||||
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
|
||||
try {
|
||||
const topMenu = appMenu[parentId].submenu as MenuItemConstructorOptions[];
|
||||
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
|
||||
topMenu.push(menuItem);
|
||||
} catch (err) {
|
||||
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import path from "path"
|
||||
import packageInfo from "../../package.json"
|
||||
import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
|
||||
import { autorun } from "mobx";
|
||||
import { showAbout } from "./menu";
|
||||
import { AppUpdater } from "./app-updater";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment } from "../common/vars";
|
||||
|
||||
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
||||
export let tray: Tray;
|
||||
|
||||
// refresh icon when MacOS dark/light theme has changed
|
||||
nativeTheme.on("updated", () => tray?.setImage(getTrayIcon()));
|
||||
|
||||
export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string {
|
||||
return path.resolve(
|
||||
__static,
|
||||
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
|
||||
`tray_icon${isDark ? "_dark" : ""}.png`
|
||||
)
|
||||
}
|
||||
|
||||
export function initTray(windowManager: WindowManager) {
|
||||
const dispose = autorun(() => {
|
||||
try {
|
||||
const menu = createTrayMenu(windowManager);
|
||||
buildTray(getTrayIcon(), menu);
|
||||
} catch (err) {
|
||||
logger.error(`[TRAY]: building failed: ${err}`);
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
dispose();
|
||||
tray?.destroy();
|
||||
tray = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildTray(icon: string | NativeImage, menu: Menu) {
|
||||
if (!tray) {
|
||||
tray = new Tray(icon)
|
||||
tray.setToolTip(packageInfo.description)
|
||||
tray.setIgnoreDoubleClickEvents(true);
|
||||
}
|
||||
|
||||
tray.setImage(icon);
|
||||
tray.setContextMenu(menu);
|
||||
|
||||
return tray;
|
||||
}
|
||||
|
||||
export function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "About Lens",
|
||||
async click() {
|
||||
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
||||
const browserWindow = await windowManager.ensureMainWindow();
|
||||
showAbout(browserWindow);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: "Open Lens",
|
||||
async click() {
|
||||
await windowManager.ensureMainWindow()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Preferences",
|
||||
click() {
|
||||
windowManager.navigate(preferencesURL());
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Clusters",
|
||||
submenu: workspaceStore.workspacesList
|
||||
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
|
||||
.map(workspace => {
|
||||
const clusters = clusterStore.getByWorkspaceId(workspace.id);
|
||||
return {
|
||||
label: workspace.name,
|
||||
toolTip: workspace.description,
|
||||
submenu: clusters.map(cluster => {
|
||||
const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster;
|
||||
return {
|
||||
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
|
||||
toolTip: clusterId,
|
||||
async click() {
|
||||
workspaceStore.setActive(workspace);
|
||||
clusterStore.setActive(clusterId);
|
||||
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Check for updates",
|
||||
async click() {
|
||||
const result = await AppUpdater.checkForUpdates();
|
||||
if (!result) {
|
||||
const browserWindow = await windowManager.ensureMainWindow();
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
message: "No updates available",
|
||||
type: "info",
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit App',
|
||||
click() {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
+109
-59
@@ -1,95 +1,140 @@
|
||||
import type { ClusterId } from "../common/cluster-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { observable } from "mobx";
|
||||
import { initMenu } from "./menu";
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { appEventBus } from "../common/event-bus"
|
||||
import { initMenu } from "./menu";
|
||||
import { initTray } from "./tray";
|
||||
|
||||
export class WindowManager {
|
||||
protected mainView: BrowserWindow;
|
||||
protected mainWindow: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected windowState: windowStateKeeper.State;
|
||||
protected disposers: Record<string, Function> = {};
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
|
||||
constructor(protected proxyPort: number) {
|
||||
this.bindEvents();
|
||||
this.initMenu();
|
||||
this.initTray();
|
||||
this.initMainWindow();
|
||||
}
|
||||
|
||||
get mainUrl() {
|
||||
return `http://localhost:${this.proxyPort}`
|
||||
}
|
||||
|
||||
async initMainWindow(showSplash = true) {
|
||||
// Manage main window size and position with state persistence
|
||||
this.windowState = windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
if (!this.windowState) {
|
||||
this.windowState = windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
}
|
||||
if (!this.mainWindow) {
|
||||
// show icon in dock (mac-os only)
|
||||
app.dock?.show();
|
||||
|
||||
const { width, height, x, y } = this.windowState;
|
||||
this.mainView = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainView);
|
||||
const { width, height, x, y } = this.windowState;
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainWindow);
|
||||
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
this.mainView.webContents.on("new-window", (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
this.mainView.webContents.on("dom-ready", () => {
|
||||
extensionLoader.broadcastExtensions()
|
||||
})
|
||||
this.mainView.on("focus", () => {
|
||||
appEventBus.emit({name: "app", action: "focus"})
|
||||
})
|
||||
this.mainView.on("blur", () => {
|
||||
appEventBus.emit({name: "app", action: "blur"})
|
||||
})
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
this.mainWindow.webContents.on("new-window", (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
this.mainWindow.webContents.on("dom-ready", () => {
|
||||
extensionLoader.broadcastExtensions()
|
||||
})
|
||||
this.mainWindow.on("focus", () => {
|
||||
appEventBus.emit({name: "app", action: "focus"})
|
||||
})
|
||||
this.mainWindow.on("blur", () => {
|
||||
appEventBus.emit({name: "app", action: "blur"})
|
||||
})
|
||||
|
||||
// clean up
|
||||
this.mainWindow.on("closed", () => {
|
||||
this.windowState.unmanage();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||
})
|
||||
}
|
||||
try {
|
||||
if (showSplash) await this.showSplash();
|
||||
await this.mainWindow.loadURL(this.mainUrl);
|
||||
this.mainWindow.show();
|
||||
this.splashWindow?.close();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox("ERROR!", err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
protected async initMenu() {
|
||||
this.disposers.menuAutoUpdater = initMenu(this);
|
||||
}
|
||||
|
||||
protected initTray() {
|
||||
this.disposers.trayAutoUpdater = initTray(this);
|
||||
}
|
||||
|
||||
protected bindEvents() {
|
||||
// track visible cluster from ui
|
||||
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
|
||||
this.activeClusterId = clusterId;
|
||||
});
|
||||
|
||||
// load & show app
|
||||
this.showMain();
|
||||
initMenu(this);
|
||||
}
|
||||
|
||||
navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) {
|
||||
async ensureMainWindow(): Promise<BrowserWindow> {
|
||||
if (!this.mainWindow) await this.initMainWindow();
|
||||
this.mainWindow.show();
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) {
|
||||
if (frameId) {
|
||||
this.mainView.webContents.sendToFrame(frameId, channel, url);
|
||||
this.mainWindow.webContents.sendToFrame(frameId, channel, ...data);
|
||||
} else {
|
||||
this.mainView.webContents.send(channel, url);
|
||||
this.mainWindow.webContents.send(channel, ...data);
|
||||
}
|
||||
}
|
||||
|
||||
reload({ channel }: { channel: string }) {
|
||||
async navigate(url: string, frameId?: number) {
|
||||
await this.ensureMainWindow();
|
||||
this.sendToView({
|
||||
channel: "menu:navigate",
|
||||
frameId: frameId,
|
||||
data: [url],
|
||||
})
|
||||
}
|
||||
|
||||
reload() {
|
||||
const frameId = clusterStore.getById(this.activeClusterId)?.frameId;
|
||||
if (frameId) {
|
||||
this.mainView.webContents.sendToFrame(frameId, channel);
|
||||
this.sendToView({ channel: "menu:reload", frameId });
|
||||
} else {
|
||||
webContents.getFocusedWebContents()?.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async showMain() {
|
||||
try {
|
||||
await this.showSplash();
|
||||
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
|
||||
this.mainView.show()
|
||||
this.splashWindow.close()
|
||||
} catch (err) {
|
||||
dialog.showErrorBox("ERROR!", err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
async showSplash() {
|
||||
if (!this.splashWindow) {
|
||||
this.splashWindow = new BrowserWindow({
|
||||
@@ -110,8 +155,13 @@ export class WindowManager {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.windowState.unmanage();
|
||||
this.mainWindow.destroy();
|
||||
this.splashWindow.destroy();
|
||||
this.mainView.destroy();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
||||
dispose();
|
||||
delete this.disposers[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const addClusterRoute: RouteProps = {
|
||||
path: "/add-cluster"
|
||||
|
||||
@@ -46,6 +46,7 @@ export class AddCluster extends React.Component {
|
||||
@observable dropAreaActive = false;
|
||||
|
||||
componentDidMount() {
|
||||
clusterStore.setActive(null);
|
||||
this.setKubeConfig(userStore.kubeConfigPath);
|
||||
}
|
||||
|
||||
@@ -118,17 +119,16 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
addClusters = () => {
|
||||
const configValidationErrors:string[] = [];
|
||||
let newClusters: ClusterModel[] = [];
|
||||
|
||||
try {
|
||||
if (!this.selectedContexts.length) {
|
||||
this.error = <Trans>Please select at least one cluster context</Trans>
|
||||
return;
|
||||
}
|
||||
this.error = ""
|
||||
this.isWaiting = true
|
||||
this.isWaiting = true
|
||||
|
||||
newClusters = this.selectedContexts.filter(context => {
|
||||
try {
|
||||
@@ -138,8 +138,8 @@ export class AddCluster extends React.Component {
|
||||
} catch (err) {
|
||||
this.error = String(err.message)
|
||||
if (err instanceof ExecValidationNotFoundError ) {
|
||||
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
|
||||
return false;
|
||||
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
|
||||
return false;
|
||||
} else {
|
||||
throw new Error(err);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export class AddCluster extends React.Component {
|
||||
clusterStore.setActive(clusterId);
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
} else {
|
||||
if (newClusters.length > 1) {
|
||||
if (newClusters.length > 1) {
|
||||
Notifications.ok(
|
||||
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
import { appsRoute } from "../+apps/apps.route";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const helmChartsRoute: RouteProps = {
|
||||
path: appsRoute.path + "/charts/:repo?/:chartName?"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
import { appsRoute } from "../+apps/apps.route";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const releaseRoute: RouteProps = {
|
||||
path: appsRoute.path + "/releases/:namespace?/:name?"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const appsRoute: RouteProps = {
|
||||
path: "/apps",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import "./cluster-settings.scss";
|
||||
|
||||
import React from "react";
|
||||
import { autorun } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { reaction } from "mobx";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { Features } from "./features";
|
||||
import { Removal } from "./removal";
|
||||
import { Status } from "./status";
|
||||
@@ -11,7 +12,6 @@ import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterIcon } from "../cluster-icon";
|
||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
|
||||
@@ -20,16 +20,23 @@ interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
||||
|
||||
@observer
|
||||
export class ClusterSettings extends React.Component<Props> {
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.props.match.params.clusterId);
|
||||
get clusterId() {
|
||||
return this.props.match.params.clusterId
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
disposeOnUnmount(this,
|
||||
autorun(() => {
|
||||
this.refreshCluster();
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.clusterId);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.cluster, this.refreshCluster, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
||||
fireImmediately: true,
|
||||
})
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
refreshCluster = async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const clusterRoute: RouteProps = {
|
||||
path: "/cluster"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const hpaRoute: RouteProps = {
|
||||
path: "/hpa"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const configMapsRoute: RouteProps = {
|
||||
path: "/configmaps"
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const pdbRoute: RouteProps = {
|
||||
path: "/poddisruptionbudgets"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const resourceQuotaRoute: RouteProps = {
|
||||
path: "/resourcequotas"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const secretsRoute: RouteProps = {
|
||||
path: "/secrets"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { configMapsURL } from "../+config-maps";
|
||||
import { Config } from "./config";
|
||||
import { IURLParams } from "../../navigation";
|
||||
import { IURLParams } from "../../../common/utils/buildUrl";
|
||||
import { configMapsURL } from "../+config-maps/config-maps.route";
|
||||
|
||||
export const configRoute: RouteProps = {
|
||||
get path() {
|
||||
|
||||
@@ -10,8 +10,8 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
|
||||
import { PodDisruptionBudgets, pdbRoute, pdbURL } from "../+config-pod-disruption-budgets";
|
||||
import { configURL } from "./config.route";
|
||||
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
||||
import { buildURL } from "../../navigation";
|
||||
import { isAllowedResource } from "../../../common/rbac"
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const certificatesURL = buildURL("/certificates");
|
||||
export const issuersURL = buildURL("/issuers");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const crdRoute: RouteProps = {
|
||||
path: "/crd"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const eventRoute: RouteProps = {
|
||||
path: "/events"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const landingRoute: RouteProps = {
|
||||
path: "/landing"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const namespacesRoute: RouteProps = {
|
||||
path: "/namespaces"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const endpointRoute: RouteProps = {
|
||||
path: "/endpoints"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const ingressRoute: RouteProps = {
|
||||
path: "/ingresses"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const networkPoliciesRoute: RouteProps = {
|
||||
path: "/network-policies"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const servicesRoute: RouteProps = {
|
||||
path: "/services"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { Network } from "./network";
|
||||
import { servicesURL } from "../+network-services";
|
||||
import { IURLParams } from "../../navigation";
|
||||
import { IURLParams } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const networkRoute: RouteProps = {
|
||||
get path() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const nodesRoute: RouteProps = {
|
||||
path: "/nodes"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const podSecurityPoliciesRoute: RouteProps = {
|
||||
path: "/pod-security-policies"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const preferencesRoute: RouteProps = {
|
||||
path: "/preferences"
|
||||
|
||||
@@ -21,4 +21,8 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox {
|
||||
align-self: start; // limit clickable area to checkbox + text
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,13 @@ export class Preferences extends React.Component {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h2><Trans>Auto start-up</Trans></h2>
|
||||
<Checkbox
|
||||
label={<Trans>Automatically start Lens on login</Trans>}
|
||||
value={preferences.openAtLogin}
|
||||
onChange={v => preferences.openAtLogin = v}
|
||||
/>
|
||||
|
||||
<h2><Trans>Certificate Trust</Trans></h2>
|
||||
<Checkbox
|
||||
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const storageClassesRoute: RouteProps = {
|
||||
path: "/storage-classes"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const volumeClaimsRoute: RouteProps = {
|
||||
path: "/persistent-volume-claims"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const volumesRoute: RouteProps = {
|
||||
path: "/persistent-volumes"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { volumeClaimsURL } from "../+storage-volume-claims";
|
||||
import { Storage } from "./storage";
|
||||
import { IURLParams } from "../../navigation";
|
||||
import { IURLParams } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const storageRoute: RouteProps = {
|
||||
get path() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
||||
import { UserManagement } from "./user-management"
|
||||
import { buildURL, IURLParams } from "../../navigation";
|
||||
|
||||
export const usersManagementRoute: RouteProps = {
|
||||
get path() {
|
||||
@@ -30,9 +30,7 @@ export interface IRolesRouteParams {
|
||||
}
|
||||
|
||||
// URL-builders
|
||||
export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(params);
|
||||
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path)
|
||||
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path)
|
||||
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path)
|
||||
export const usersManagementURL = (params?: IURLParams) => {
|
||||
return serviceAccountsURL(params);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const whatsNewRoute: RouteProps = {
|
||||
path: "/what-s-new"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { Workloads } from "./workloads";
|
||||
import { buildURL, IURLParams } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
||||
import { KubeResource } from "../../../common/rbac";
|
||||
import { Workloads } from "./workloads";
|
||||
|
||||
export const workloadsRoute: RouteProps = {
|
||||
get path() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export const workspacesRoute: RouteProps = {
|
||||
path: "/workspaces"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./cluster-manager.scss"
|
||||
|
||||
import React from "react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { comparer, reaction } from "mobx";
|
||||
@@ -11,14 +12,17 @@ import { Workspaces, workspacesRoute } from "../+workspaces";
|
||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||
import { ClusterView } from "./cluster-view";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
|
||||
import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
componentDidMount() {
|
||||
const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId());
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
reaction(getMatchedClusterId, initView, {
|
||||
fireImmediately: true
|
||||
@@ -55,7 +59,7 @@ export class ClusterManager extends React.Component {
|
||||
return (
|
||||
<div className="ClusterManager">
|
||||
<main>
|
||||
<div id="lens-views" />
|
||||
<div id="lens-views"/>
|
||||
<Switch>
|
||||
<Route component={LandingPage} {...landingRoute} />
|
||||
<Route component={Preferences} {...preferencesRoute} />
|
||||
@@ -66,11 +70,11 @@ export class ClusterManager extends React.Component {
|
||||
{globalPageRegistry.getItems().map(({ path, url = String(path), components: { Page } }) => {
|
||||
return <Route key={url} path={path} component={Page}/>
|
||||
})}
|
||||
<Redirect exact to={this.startUrl} />
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
</main>
|
||||
<ClustersMenu />
|
||||
<BottomBar />
|
||||
<ClustersMenu/>
|
||||
<BottomBar/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { reaction } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { matchPath, RouteProps } from "react-router";
|
||||
import { buildURL, navigation } from "../../navigation";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export interface IClusterViewRouteParams {
|
||||
clusterId: string;
|
||||
@@ -14,33 +11,3 @@ export const clusterViewRoute: RouteProps = {
|
||||
}
|
||||
|
||||
export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute.path)
|
||||
|
||||
export function getMatchedClusterId(): string {
|
||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||
exact: true,
|
||||
path: clusterViewRoute.path
|
||||
})
|
||||
if (matched) {
|
||||
return matched.params.clusterId;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMatchedCluster() {
|
||||
return clusterStore.getById(getMatchedClusterId())
|
||||
}
|
||||
|
||||
if (ipcRenderer) {
|
||||
if (process.isMainFrame) {
|
||||
// Keep track of active cluster-id for handling IPC/menus/etc.
|
||||
reaction(() => getMatchedClusterId(), clusterId => {
|
||||
ipcRenderer.send("cluster-view:current-id", clusterId);
|
||||
}, {
|
||||
fireImmediately: true
|
||||
})
|
||||
}
|
||||
|
||||
// Reload dashboard
|
||||
ipcRenderer.on("menu:reload", () => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,37 @@
|
||||
import "./cluster-view.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { getMatchedCluster } from "./cluster-view.route";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { IClusterViewRouteParams } from "./cluster-view.route";
|
||||
import { ClusterStatus } from "./cluster-status";
|
||||
import { hasLoadedView } from "./lens-views";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterView extends React.Component {
|
||||
export class ClusterView extends React.Component<Props> {
|
||||
get clusterId() {
|
||||
return this.props.match.params.clusterId;
|
||||
}
|
||||
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.clusterId);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
||||
fireImmediately: true,
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
render() {
|
||||
const cluster = getMatchedCluster();
|
||||
const { cluster } = this;
|
||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready)
|
||||
return (
|
||||
<div className="ClusterView">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import "./clusters-menu.scss"
|
||||
|
||||
import { remote } from "electron"
|
||||
import React from "react";
|
||||
import { remote } from "electron"
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||
import { observer } from "mobx-react";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
@@ -21,7 +22,6 @@ import { Tooltip } from "../tooltip";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { clusterViewURL } from "./cluster-view.route";
|
||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||
|
||||
interface Props {
|
||||
@@ -31,13 +31,11 @@ interface Props {
|
||||
@observer
|
||||
export class ClustersMenu extends React.Component<Props> {
|
||||
showCluster = (clusterId: ClusterId) => {
|
||||
clusterStore.setActive(clusterId);
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
|
||||
addCluster = () => {
|
||||
navigate(addClusterURL());
|
||||
clusterStore.setActive(null);
|
||||
}
|
||||
|
||||
showContextMenu = (cluster: Cluster) => {
|
||||
@@ -47,7 +45,6 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Settings`),
|
||||
click: () => {
|
||||
clusterStore.setActive(cluster.id);
|
||||
navigate(clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: cluster.id
|
||||
@@ -112,21 +109,14 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
<div className="clusters flex column gaps">
|
||||
<DragDropContext onDragEnd={this.swapClusterIconOrder}>
|
||||
<Droppable droppableId="cluster-menu" type="CLUSTER">
|
||||
{(provided: DroppableProvided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
|
||||
<div ref={innerRef} {...droppableProps}>
|
||||
{clusters.map((cluster, index) => {
|
||||
const isActive = cluster.id === clusterStore.activeClusterId;
|
||||
return (
|
||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
||||
{(provided: DraggableProvided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
|
||||
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
|
||||
<ClusterIcon
|
||||
key={cluster.id}
|
||||
showErrors={true}
|
||||
@@ -138,9 +128,9 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)}
|
||||
)}
|
||||
{provided.placeholder}
|
||||
)
|
||||
})}
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
@@ -150,16 +140,14 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
<Tooltip targetId="add-cluster-icon">
|
||||
<Trans>Add Cluster</Trans>
|
||||
</Tooltip>
|
||||
<Icon big material="add" id="add-cluster-icon" />
|
||||
<Icon big material="add" id="add-cluster-icon"/>
|
||||
{newContexts.size > 0 && (
|
||||
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
|
||||
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
|
||||
)}
|
||||
</div>
|
||||
<div className="extensions">
|
||||
{globalPageRegistry.getItems().map(({ path, url = String(path), hideInMenu, components: { MenuIcon } }) => {
|
||||
if (!MenuIcon || hideInMenu) {
|
||||
return;
|
||||
}
|
||||
if (!MenuIcon || hideInMenu) return;
|
||||
return <MenuIcon key={url} onClick={() => navigate(url)}/>
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observable, when } from "mobx";
|
||||
import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||
import { getMatchedCluster } from "./cluster-view.route"
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
import logger from "../../../main/logger";
|
||||
|
||||
export interface LensView {
|
||||
@@ -51,7 +51,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
|
||||
}
|
||||
|
||||
export function refreshViews() {
|
||||
const cluster = getMatchedCluster();
|
||||
const cluster = clusterStore.getById(getMatchedClusterId());
|
||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
||||
const isCurrent = clusterId === cluster?.id;
|
||||
const isReady = cluster?.available && cluster?.ready;
|
||||
|
||||
File renamed without changes.
@@ -1,7 +0,0 @@
|
||||
<svg id="Layer_1" viewBox="0 0 194 219" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#fff">
|
||||
<polygon points="98 12.4 71 28.3 44 44.1 17 60 17 91.8 17 123.6 17 155.4 44 171.3 44 139.5 44 107.7 44 75.9 71 60 71 91.8 71 91.8 71 123.6 98 107.7 98 107.7 125 91.8 125 60 152 44.1 125 28.3 98 12.4"/>
|
||||
<polygon points="152 44.1 152 75.9 152 107.7 125 123.6 152 139.5 152 171.3 179 155.4 179 123.6 179 91.8 179 60 152 44.1"/>
|
||||
<polygon points="98 139.5 71 155.4 71 155.4 71 187.2 98 203.1 125 187.2 125 155.4 98 139.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 538 B |
@@ -80,7 +80,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
|
||||
<div className="header flex align-center">
|
||||
<NavLink exact to="/" className="box grow">
|
||||
<Icon svg="logo-full" className="logo-icon"/>
|
||||
<Icon svg="logo-lens" className="logo-icon"/>
|
||||
<div className="logo-text">Lens</div>
|
||||
</NavLink>
|
||||
<Icon
|
||||
|
||||
+34
-24
@@ -1,22 +1,16 @@
|
||||
// Navigation helpers
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { compile } from "path-to-regexp"
|
||||
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
|
||||
import { matchPath } from "react-router";
|
||||
import { reaction } from "mobx";
|
||||
import { createObservableHistory } from "mobx-observable-history";
|
||||
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
|
||||
import logger from "../main/logger";
|
||||
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
|
||||
|
||||
export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory();
|
||||
export const navigation = createObservableHistory(history);
|
||||
|
||||
// handle navigation from other process (e.g. system menus in main, common->cluster view interactions)
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => {
|
||||
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
|
||||
navigate(location);
|
||||
})
|
||||
}
|
||||
|
||||
export function navigate(location: LocationDescriptor) {
|
||||
const currentLocation = navigation.getPath();
|
||||
navigation.push(location);
|
||||
@@ -25,20 +19,6 @@ export function navigate(location: LocationDescriptor) {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IURLParams<P = {}, Q = {}> {
|
||||
params?: P;
|
||||
query?: IQueryParams & Q;
|
||||
}
|
||||
|
||||
// todo: extract building urls to commons (also used in menu.ts)
|
||||
// fixme: missing types validation for params & query
|
||||
export function buildURL<P extends object, Q = object>(path: string | string[]) {
|
||||
const pathBuilder = compile(path.toString());
|
||||
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
||||
return pathBuilder(params) + (query ? getQueryString(query, false) : "")
|
||||
}
|
||||
}
|
||||
|
||||
// common params for all pages
|
||||
export interface IQueryParams {
|
||||
namespaces?: string[]; // selected context namespaces
|
||||
@@ -100,3 +80,33 @@ export function setSearch(text: string) {
|
||||
export function getSearch() {
|
||||
return navigation.searchParams.get("search") || "";
|
||||
}
|
||||
|
||||
export function getMatchedClusterId(): string {
|
||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||
exact: true,
|
||||
path: clusterViewRoute.path
|
||||
});
|
||||
return matched?.params.clusterId;
|
||||
}
|
||||
|
||||
//-- EVENTS
|
||||
|
||||
if (process.isMainFrame) {
|
||||
// Keep track of active cluster-id for handling IPC/menus/etc.
|
||||
reaction(() => getMatchedClusterId(), clusterId => {
|
||||
ipcRenderer.send("cluster-view:current-id", clusterId);
|
||||
}, {
|
||||
fireImmediately: true
|
||||
})
|
||||
}
|
||||
|
||||
// Handle navigation via IPC (e.g. from top menu)
|
||||
ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => {
|
||||
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
|
||||
navigate(location);
|
||||
});
|
||||
|
||||
// Reload dashboard window
|
||||
ipcRenderer.on("menu:reload", () => {
|
||||
location.reload();
|
||||
});
|
||||
Reference in new issue
Block a user