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

View File

@ -1,5 +1,5 @@
import path from "path"; 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 { unlink } from "fs-extra";
import { action, computed, observable, toJS } from "mobx"; import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
@ -108,7 +108,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
setActive(id: ClusterId) { setActive(id: ClusterId) {
this.activeClusterId = id; this.activeClusterId = this.clusters.has(id) ? id : null;
} }
@action @action
@ -155,7 +155,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
if (cluster) { if (cluster) {
this.clusters.delete(clusterId); this.clusters.delete(clusterId);
if (this.activeClusterId === clusterId) { if (this.activeClusterId === clusterId) {
this.activeClusterId = null; this.setActive(null);
} }
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {

View File

@ -1,7 +1,7 @@
import path from "path" import path from "path"
import sharp from "sharp"; import sharp from "sharp";
import packageInfo from "../../package.json" 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 { isDevelopment, isMac } from "../common/vars";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { showAbout } from "./menu"; import { showAbout } from "./menu";
@ -21,17 +21,39 @@ export const trayIcon = isDevelopment
: path.resolve(__static, "logo.svg") // electron-builder's extraResources : path.resolve(__static, "logo.svg") // electron-builder's extraResources
export function initTray(windowManager: WindowManager) { export function initTray(windowManager: WindowManager) {
return autorun(() => buildTrayMenu(windowManager), { return autorun(() => {
delay: 100 const menu = createTrayMenu(windowManager);
buildTray(menu);
}) })
} }
export async function buildTrayMenu(windowManager: WindowManager) { export async function buildTray(menu: Menu) {
// note: browserWindow not available within menuItem.click() as argument[1] when app is not focused / hidden logger.info("[TRAY]: build start");
const trayMenu = Menu.buildFromTemplate([ 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", label: "About Lens",
click() { click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden
windowManager.bringToTop(); windowManager.bringToTop();
showAbout(windowManager.mainView); showAbout(windowManager.mainView);
}, },
@ -45,22 +67,25 @@ export async function buildTrayMenu(windowManager: WindowManager) {
}, },
{ {
label: "Clusters", label: "Clusters",
submenu: workspaceStore.workspacesList.map(workspace => { submenu: workspaceStore.workspacesList
const clusters = clusterStore.getByWorkspaceId(workspace.id); .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
return { .map(workspace => {
label: workspace.name, const clusters = clusterStore.getByWorkspaceId(workspace.id);
toolTip: workspace.description, return {
submenu: clusters.map(({ id: clusterId, contextName: label }) => { label: workspace.name,
return { toolTip: workspace.description,
label, submenu: clusters.map(({ id: clusterId, preferences: { clusterName: label }, online }) => {
click() { return {
windowManager.bringToTop(); label: `${label}${online ? " (online)" : ""}`,
windowManager.navigate(clusterViewURL({ params: { clusterId } })); toolTip: clusterId,
click() {
windowManager.bringToTop();
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
}
} }
} })
}) }
} }),
}),
}, },
{ {
label: "Check for updates", 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 os from "os";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { action, observable, runInAction } from "mobx"; import { action, observable } from "mobx";
import { remote } from "electron"; import { remote } from "electron";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
@ -45,6 +45,7 @@ export class AddCluster extends React.Component {
@observable dropAreaActive = false; @observable dropAreaActive = false;
componentDidMount() { componentDidMount() {
clusterStore.setActive(null);
this.setKubeConfig(userStore.kubeConfigPath); this.setKubeConfig(userStore.kubeConfigPath);
} }
@ -117,6 +118,7 @@ export class AddCluster extends React.Component {
} }
} }
@action
addClusters = () => { addClusters = () => {
try { try {
if (!this.selectedContexts.length) { if (!this.selectedContexts.length) {
@ -125,6 +127,7 @@ export class AddCluster extends React.Component {
} }
this.error = "" this.error = ""
this.isWaiting = true this.isWaiting = true
const newClusters: ClusterModel[] = this.selectedContexts.map(context => { const newClusters: ClusterModel[] = this.selectedContexts.map(context => {
const clusterId = uuid(); const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context); const kubeConfig = this.kubeContexts.get(context);
@ -142,19 +145,17 @@ export class AddCluster extends React.Component {
}, },
} }
}); });
runInAction(() => {
clusterStore.addCluster(...newClusters); clusterStore.addCluster(...newClusters);
if (newClusters.length === 1) {
const clusterId = newClusters[0].id; if (newClusters.length === 1) {
clusterStore.setActive(clusterId); const clusterId = newClusters[0].id;
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
} else { } else {
Notifications.ok( Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans> <Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
); );
} }
})
this.refreshContexts();
} catch (err) { } catch (err) {
this.error = String(err); this.error = String(err);
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>); Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
@ -206,7 +207,7 @@ export class AddCluster extends React.Component {
<Tab <Tab
value={KubeConfigSourceTab.FILE} value={KubeConfigSourceTab.FILE}
label={<Trans>Select kubeconfig file</Trans>} label={<Trans>Select kubeconfig file</Trans>}
active={this.sourceTab == KubeConfigSourceTab.FILE} /> active={this.sourceTab == KubeConfigSourceTab.FILE}/>
<Tab <Tab
value={KubeConfigSourceTab.TEXT} value={KubeConfigSourceTab.TEXT}
label={<Trans>Paste as text</Trans>} label={<Trans>Paste as text</Trans>}
@ -320,8 +321,8 @@ export class AddCluster extends React.Component {
return ( return (
<div className={cssNames("kube-context flex gaps align-center", context)}> <div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span> <span>{context}</span>
{isNew && <Icon small material="fiber_new" />} {isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right" />} {isSelected && <Icon small material="check" className="box right"/>}
</div> </div>
) )
}; };

View File

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

View File

@ -1,14 +1,37 @@
import "./cluster-view.scss" import "./cluster-view.scss"
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { reaction } from "mobx";
import { getMatchedCluster } from "./cluster-view.route"; import { disposeOnUnmount, observer } from "mobx-react";
import { IClusterViewRouteParams } from "./cluster-view.route";
import { ClusterStatus } from "./cluster-status"; import { ClusterStatus } from "./cluster-status";
import { hasLoadedView } from "./lens-views"; 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 @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() { render() {
const cluster = getMatchedCluster(); const { cluster } = this;
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id)) const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id))
return ( return (
<div className="ClusterView"> <div className="ClusterView">

View File

@ -1,6 +1,9 @@
import "./clusters-menu.scss" import "./clusters-menu.scss"
import type { Cluster } from "../../../main/cluster";
import { remote } from "electron" import { remote } from "electron"
import React from "react"; import React from "react";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
@ -9,7 +12,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";
@ -19,8 +22,6 @@ 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 type { Cluster } from "../../../main/cluster";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -29,13 +30,11 @@ interface Props {
@observer @observer
export class ClustersMenu extends React.Component<Props> { export class ClustersMenu extends React.Component<Props> {
showCluster = (clusterId: ClusterId) => { showCluster = (clusterId: ClusterId) => {
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
} }
addCluster = () => { addCluster = () => {
navigate(addClusterURL()); navigate(addClusterURL());
clusterStore.setActive(null);
} }
showContextMenu = (cluster: Cluster) => { showContextMenu = (cluster: Cluster) => {
@ -45,7 +44,6 @@ export class ClustersMenu extends React.Component<Props> {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: _i18n._(t`Settings`), label: _i18n._(t`Settings`),
click: () => { click: () => {
clusterStore.setActive(cluster.id);
navigate(clusterSettingsURL({ navigate(clusterSettingsURL({
params: { params: {
clusterId: cluster.id clusterId: cluster.id
@ -110,35 +108,29 @@ export class ClustersMenu extends React.Component<Props> {
<div className="clusters flex column gaps"> <div className="clusters flex column gaps">
<DragDropContext onDragEnd={this.swapClusterIconOrder}> <DragDropContext onDragEnd={this.swapClusterIconOrder}>
<Droppable droppableId="cluster-menu" type="CLUSTER"> <Droppable droppableId="cluster-menu" type="CLUSTER">
{(provided: DroppableProvided) => ( {({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
<div <div ref={innerRef} {...droppableProps}>
ref={provided.innerRef}
{...provided.droppableProps}
>
{clusters.map((cluster, index) => { {clusters.map((cluster, index) => {
const isActive = cluster.id === clusterStore.activeClusterId; const isActive = cluster.id === clusterStore.activeClusterId;
return ( return (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}> <Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{(provided: DraggableProvided) => ( {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
<div <div ref={innerRef} {...draggableProps} {...dragHandleProps}>
ref={provided.innerRef} <ClusterIcon
{...provided.draggableProps} key={cluster.id}
{...provided.dragHandleProps} showErrors={true}
> cluster={cluster}
<ClusterIcon isActive={isActive}
key={cluster.id} onClick={() => this.showCluster(cluster.id)}
showErrors={true} onContextMenu={() => this.showContextMenu(cluster)}
cluster={cluster} />
isActive={isActive} </div>
onClick={() => this.showCluster(cluster.id)} )}
onContextMenu={() => this.showContextMenu(cluster)} </Draggable>
/> )
</div> }
)}
</Draggable>
)}
)} )}
{provided.placeholder} {placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>
@ -148,9 +140,9 @@ export class ClustersMenu extends React.Component<Props> {
<Tooltip targetId="add-cluster-icon"> <Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans> <Trans>Add Cluster</Trans>
</Tooltip> </Tooltip>
<Icon big material="add" id="add-cluster-icon" /> <Icon big material="add" id="add-cluster-icon"/>
{newContexts.size > 0 && ( {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>
</div> </div>