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

Tray icon #833 -- part 4

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-10-01 07:25:00 +03:00
parent 162b4108dd
commit c1dc1e6463
7 changed files with 146 additions and 117 deletions

View File

@ -12,6 +12,7 @@ import isEqual from "lodash/isEqual";
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
autoLoad?: boolean;
syncEnabled?: boolean;
syncDelayMs?: number;
}
export class BaseStore<T = any> extends Singleton {
@ -27,6 +28,7 @@ export class BaseStore<T = any> extends Singleton {
this.params = {
autoLoad: false,
syncEnabled: true,
syncDelayMs: 100,
...params,
}
this.init();
@ -73,7 +75,9 @@ export class BaseStore<T = any> extends Singleton {
enableSync() {
this.syncDisposers.push(
reaction(() => this.toJSON(), model => this.onModelChange(model)),
reaction(() => this.toJSON(), model => this.onModelChange(model), {
delay: this.params.syncDelayMs,
}),
);
if (ipcMain) {
const callback = (event: IpcMainEvent, model: T) => {

View File

@ -1,5 +1,5 @@
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";
@ -108,7 +108,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action
setActive(id: ClusterId) {
this.activeClusterId = id;
this.activeClusterId = this.clusters.has(id) ? id : null;
}
@action
@ -155,7 +155,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)) {

View File

@ -1,7 +1,7 @@
import path from "path"
import sharp from "sharp";
import packageInfo from "../../package.json"
import { app, dialog, Menu, nativeImage, Tray } from "electron"
import { dialog, Menu, nativeImage, Tray } from "electron"
import { isDevelopment, isMac } from "../common/vars";
import { autorun } from "mobx";
import { showAbout } from "./menu";
@ -21,17 +21,39 @@ export const trayIcon = isDevelopment
: path.resolve(__static, "logo.svg") // electron-builder's extraResources
export function initTray(windowManager: WindowManager) {
return autorun(() => buildTrayMenu(windowManager), {
delay: 100
return autorun(() => {
const menu = createTrayMenu(windowManager);
buildTray(menu);
})
}
export async function buildTrayMenu(windowManager: WindowManager) {
// note: browserWindow not available within menuItem.click() as argument[1] when app is not focused / hidden
const trayMenu = Menu.buildFromTemplate([
export async function buildTray(menu: Menu) {
logger.info("[TRAY]: build start");
const iconSize = isMac ? 16 : 32; // todo: verify on windows/linux
const pngIcon = await sharp(trayIcon).png().toBuffer();
const icon = nativeImage.createFromBuffer(pngIcon).resize({
width: iconSize,
height: iconSize
});
if (tray) {
tray.destroy(); // remove old tray on update
}
tray = new Tray(icon)
tray.setToolTip(packageInfo.description)
tray.setIgnoreDoubleClickEvents(true);
tray.setContextMenu(menu);
return tray;
}
export function createTrayMenu(windowManager: WindowManager): Menu {
return Menu.buildFromTemplate([
{
label: "About Lens",
click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden
windowManager.bringToTop();
showAbout(windowManager.mainView);
},
@ -45,22 +67,25 @@ export async function buildTrayMenu(windowManager: WindowManager) {
},
{
label: "Clusters",
submenu: workspaceStore.workspacesList.map(workspace => {
const clusters = clusterStore.getByWorkspaceId(workspace.id);
return {
label: workspace.name,
toolTip: workspace.description,
submenu: clusters.map(({ id: clusterId, contextName: label }) => {
return {
label,
click() {
windowManager.bringToTop();
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
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(({ id: clusterId, preferences: { clusterName: label }, online }) => {
return {
label: `${label}${online ? " (online)" : ""}`,
toolTip: clusterId,
click() {
windowManager.bringToTop();
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
}
}
}
})
}
}),
})
}
}),
},
{
label: "Check for updates",
@ -76,26 +101,4 @@ export async function buildTrayMenu(windowManager: WindowManager) {
},
},
]);
// note: all "await"-s must be defined *AFTER* getting observables for proper mobx reactions
await app.whenReady();
logger.info('[TRAY]: building tray icon and menu');
const iconSize = isMac ? 16 : 32; // todo: verify on windows/linux
const pngIcon = await sharp(trayIcon).png().toBuffer();
const icon = nativeImage.createFromBuffer(pngIcon).resize({
width: iconSize,
height: iconSize
});
if (tray) {
tray.destroy(); // remove old tray on update
}
tray = new Tray(icon)
tray.setToolTip(packageInfo.description)
tray.setIgnoreDoubleClickEvents(true);
tray.setContextMenu(trayMenu);
return tray;
}

View File

@ -2,7 +2,7 @@ import "./add-cluster.scss"
import os from "os";
import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { action, observable, runInAction } from "mobx";
import { action, observable } from "mobx";
import { remote } from "electron";
import { KubeConfig } from "@kubernetes/client-node";
import { _i18n } from "../../i18n";
@ -45,6 +45,7 @@ export class AddCluster extends React.Component {
@observable dropAreaActive = false;
componentDidMount() {
clusterStore.setActive(null);
this.setKubeConfig(userStore.kubeConfigPath);
}
@ -117,6 +118,7 @@ export class AddCluster extends React.Component {
}
}
@action
addClusters = () => {
try {
if (!this.selectedContexts.length) {
@ -125,6 +127,7 @@ export class AddCluster extends React.Component {
}
this.error = ""
this.isWaiting = true
const newClusters: ClusterModel[] = this.selectedContexts.map(context => {
const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context);
@ -142,19 +145,17 @@ export class AddCluster extends React.Component {
},
}
});
runInAction(() => {
clusterStore.addCluster(...newClusters);
if (newClusters.length === 1) {
const clusterId = newClusters[0].id;
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
} else {
Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
);
}
})
this.refreshContexts();
clusterStore.addCluster(...newClusters);
if (newClusters.length === 1) {
const clusterId = newClusters[0].id;
navigate(clusterViewURL({ params: { clusterId } }));
} else {
Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
);
}
} catch (err) {
this.error = String(err);
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
@ -206,7 +207,7 @@ export class AddCluster extends React.Component {
<Tab
value={KubeConfigSourceTab.FILE}
label={<Trans>Select kubeconfig file</Trans>}
active={this.sourceTab == KubeConfigSourceTab.FILE} />
active={this.sourceTab == KubeConfigSourceTab.FILE}/>
<Tab
value={KubeConfigSourceTab.TEXT}
label={<Trans>Paste as text</Trans>}
@ -320,8 +321,8 @@ export class AddCluster extends React.Component {
return (
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
{isNew && <Icon small material="fiber_new" />}
{isSelected && <Icon small material="check" className="box right" />}
{isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right"/>}
</div>
)
};

View File

@ -1,7 +1,9 @@
import "./cluster-settings.scss";
import React from "react";
import { observer, disposeOnUnmount } from "mobx-react";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { Features } from "./features";
import { Removal } from "./removal";
import { Status } from "./status";
@ -13,30 +15,35 @@ import { Icon } from "../icon";
import { navigate } from "../../navigation";
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store";
import { RouteComponentProps } from "react-router";
import { clusterIpc } from "../../../common/cluster-ipc";
import { autorun } from "mobx";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
}
@observer
export class ClusterSettings extends React.Component<Props> {
get clusterId() {
return this.props.match.params.clusterId
}
get cluster(): Cluster {
return clusterStore.getById(this.props.match.params.clusterId);
return clusterStore.getById(this.clusterId);
}
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
disposeOnUnmount(this,
autorun(() => {
this.refreshCluster();
window.addEventListener("keydown", this.onEscapeKey);
disposeOnUnmount(this, [
reaction(() => this.cluster, this.refreshCluster, {
fireImmediately: true,
}),
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
fireImmediately: true,
})
)
])
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
window.removeEventListener("keydown", this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
@ -46,10 +53,9 @@ export class ClusterSettings extends React.Component<Props> {
}
}
refreshCluster = () => {
if(this.cluster) {
clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
}
refreshCluster = (cluster: Cluster) => {
if (!cluster) return;
clusterIpc.refresh.invokeFromRenderer(cluster.id);
}
close() {

View File

@ -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 { 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";
import { RouteComponentProps } from "react-router";
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))
return (
<div className="ClusterView">

View File

@ -1,6 +1,9 @@
import "./clusters-menu.scss"
import type { Cluster } from "../../../main/cluster";
import { remote } from "electron"
import React from "react";
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";
@ -9,7 +12,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { cssNames, IClassName, autobind } from "../../utils";
import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
@ -19,8 +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, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd";
import type { Cluster } from "../../../main/cluster";
interface Props {
className?: IClassName;
@ -29,13 +30,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) => {
@ -45,7 +44,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
@ -110,35 +108,29 @@ 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}
>
<ClusterIcon
key={cluster.id}
showErrors={true}
cluster={cluster}
isActive={isActive}
onClick={() => this.showCluster(cluster.id)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
</div>
)}
</Draggable>
)}
const isActive = cluster.id === clusterStore.activeClusterId;
return (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
<ClusterIcon
key={cluster.id}
showErrors={true}
cluster={cluster}
isActive={isActive}
onClick={() => this.showCluster(cluster.id)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
</div>
)}
</Draggable>
)
}
)}
{provided.placeholder}
{placeholder}
</div>
)}
</Droppable>
@ -148,9 +140,9 @@ 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>