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

Introduce a tray item for updating application

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-05-09 09:18:04 +03:00
parent 8d3bc40aa9
commit 84ac22904d
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
13 changed files with 359 additions and 2 deletions

View File

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`trigger updating using tray given no update available, when started renders 1`] = `
<body>
<div />
</body>
`;
exports[`trigger updating using tray given no update available, when started when an update becomes available renders 1`] = `
<body>
<div />
</body>
`;
exports[`trigger updating using tray given no update available, when started when an update becomes available when triggering installation of the update renders 1`] = `
<body>
<div />
</body>
`;
exports[`trigger updating using tray given no update available, when started when an update becomes available when update becomes unavailable renders 1`] = `
<body>
<div />
</body>
`;

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable";
import type { RenderResult } from "@testing-library/react";
describe("trigger updating using tray", () => {
let applicationBuilder: ApplicationBuilder;
let quitAndInstallUpdateMock: jest.Mock;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
quitAndInstallUpdateMock = jest.fn();
mainDi.override(quitAndInstallUpdateInjectable, () => quitAndInstallUpdateMock);
});
});
describe("given no update available, when started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not quit and install update yet", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("does not have possibility to trigger installation of an update", () => {
const trayItem = applicationBuilder.tray.get("trigger-application-update");
expect(trayItem).toBe(undefined);
});
describe("when an update becomes available", () => {
beforeEach(() => {
applicationBuilder.applicationUpdater.makeUpdateAvailable(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not quit and install update yet", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("has possibility to trigger installation of the update", () => {
const trayItem = applicationBuilder.tray.get("trigger-application-update");
expect(trayItem).not.toBe(undefined);
});
describe("when triggering installation of the update", () => {
beforeEach(() => {
applicationBuilder.tray.click("trigger-application-update");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("quits application and installs update", () => {
expect(quitAndInstallUpdateMock).toHaveBeenCalled();
});
});
describe("when update becomes unavailable", () => {
beforeEach(async () => {
applicationBuilder.applicationUpdater.makeUpdateAvailable(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not have possibility to trigger installation of the update", () => {
const trayItem = applicationBuilder.tray.get("trigger-application-update");
expect(trayItem).toBe(undefined);
});
});
});
});
});

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { autoUpdater } from "electron-updater";
const electronUpdaterInjectable = getInjectable({
id: "electron-updater",
instantiate: () => autoUpdater,
causesSideEffects: true,
});
export default electronUpdaterInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { autoUpdater } from "electron-updater";
const quitAndInstallUpdateInjectable = getInjectable({
id: "quit-and-install-update",
instantiate: () => () => {
autoUpdater.quitAndInstall(true, true);
},
causesSideEffects: true,
});
export default quitAndInstallUpdateInjectable;

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import synchronizeUpdateIsAvailableStateInjectable from "./synchronize-update-is-available-state.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
const startSynchronizingUpdateIsAvailableStateInjectable = getInjectable({
id: "start-synchronizing-update-is-available-state",
instantiate: (di) => {
const synchronizeUpdateIsAvailableState = di.inject(synchronizeUpdateIsAvailableStateInjectable);
return {
run: () => {
synchronizeUpdateIsAvailableState.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startSynchronizingUpdateIsAvailableStateInjectable;

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import synchronizeUpdateIsAvailableStateInjectable from "./synchronize-update-is-available-state.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopSynchronizingUpdateIsAvailableStateInjectable = getInjectable({
id: "stop-synchronizing-update-is-available-state",
instantiate: (di) => {
const synchronizeUpdateIsAvailableState = di.inject(synchronizeUpdateIsAvailableStateInjectable);
return {
run: () => {
synchronizeUpdateIsAvailableState.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopSynchronizingUpdateIsAvailableStateInjectable;

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable";
import electronUpdaterInjectable from "../../features/electron-updater.injectable";
import updateIsAvailableStateInjectable from "../../../update-app/update-is-available-state.injectable";
const synchronizeUpdateIsAvailableStateInjectable = getInjectable({
id: "synchronize-update-is-available-state",
instantiate: (di) => {
const electronUpdater = di.inject(electronUpdaterInjectable);
const updateIsAvailableState = di.inject(updateIsAvailableStateInjectable);
const makeUpdateAvailableFor = (available: boolean) => () => {
updateIsAvailableState.set(available);
};
return getStartableStoppable(
"synchronize-update-is-available-state",
() => {
const makeUpdateAvailable = makeUpdateAvailableFor(true);
const makeUpdateUnavailable = makeUpdateAvailableFor(false);
electronUpdater.on("update-downloaded", makeUpdateAvailable);
electronUpdater.on("update-not-available", makeUpdateUnavailable);
return () => {
electronUpdater.off("update-downloaded", makeUpdateAvailable);
electronUpdater.off("update-not-available", makeUpdateUnavailable);
};
},
);
},
});
export default synchronizeUpdateIsAvailableStateInjectable;

View File

@ -78,6 +78,8 @@ import getElectronThemeInjectable from "./electron-app/features/get-electron-the
import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable";
import platformInjectable from "../common/vars/platform.injectable";
import productNameInjectable from "./app-paths/app-name/product-name.injectable";
import synchronizeUpdateIsAvailableStateInjectable from "./electron-app/runnables/update-application/synchronize-update-is-available-state.injectable";
import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-install-update.injectable";
export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
const {
@ -220,6 +222,8 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(ipcMainInjectable, () => ({}));
di.override(getElectronThemeInjectable, () => () => "dark");
di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(synchronizeUpdateIsAvailableStateInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(quitAndInstallUpdateInjectable, () => () => {});
di.override(createElectronWindowForInjectable, () => () => async () => ({
show: () => {},

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token";
import updateIsAvailableInjectable from "./update-is-available.injectable";
import triggerApplicationUpdateInjectable from "./trigger-application-update.injectable";
const triggerApplicationUpdateTrayItemInjectable = getInjectable({
id: "trigger-application-update-tray-item",
instantiate: (di) => {
const updateIsAvailable = di.inject(updateIsAvailableInjectable);
const triggerApplicationUpdate = di.inject(triggerApplicationUpdateInjectable);
return {
id: "trigger-application-update",
parentId: null,
orderNumber: 50,
label: "Trigger update",
enabled: computed(() => true),
visible: computed(() => updateIsAvailable.get()),
click: () => {
triggerApplicationUpdate();
},
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default triggerApplicationUpdateTrayItemInjectable;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import quitAndInstallUpdateInjectable from "../electron-app/features/quit-and-install-update.injectable";
const triggerApplicationUpdateInjectable = getInjectable({
id: "trigger-application-update",
instantiate: (di) => {
const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable);
return () => {
quitAndInstallUpdate();
};
},
});
export default triggerApplicationUpdateInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
const updateIsAvailableState = getInjectable({
id: "update-is-available-state",
instantiate: () => observable.box<boolean>(false),
});
export default updateIsAvailableState;

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import updateIsAvailableStateInjectable from "./update-is-available-state.injectable";
const updateIsAvailableInjectable = getInjectable({
id: "update-is-available",
instantiate: (di) => {
const updateIsAvailableState = di.inject(updateIsAvailableStateInjectable);
return computed(() => updateIsAvailableState.get());
},
});
export default updateIsAvailableInjectable;

View File

@ -7,7 +7,7 @@ import rendererExtensionsInjectable from "../../../extensions/renderer-extension
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { IObservableArray } from "mobx";
import { computed, observable, runInAction } from "mobx";
import { action, computed, observable, runInAction } from "mobx";
import { renderFor } from "./renderFor";
import React from "react";
import { Router } from "react-router";
@ -25,7 +25,7 @@ import type { ClusterStore } from "../../../common/cluster-store/cluster-store";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
import { pipeline } from "@ogre-tools/fp";
import { flatMap, compact, join, get, filter, find, map } from "lodash/fp";
import { flatMap, compact, join, get, filter, find, map, matches } from "lodash/fp";
import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable";
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable";
@ -45,6 +45,8 @@ import type { NamespaceStore } from "../+namespaces/store";
import namespaceStoreInjectable from "../+namespaces/store.injectable";
import historyInjectable from "../../navigation/history.injectable";
import trayMenuItemsInjectable from "../../../main/tray/tray-menu-item/tray-menu-items.injectable";
import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import updateIsAvailableStateInjectable from "../../../main/update-app/update-is-available-state.injectable";
type Callback = (dis: DiContainers) => void | Promise<void>;
@ -57,8 +59,13 @@ export interface ApplicationBuilder {
beforeRender: (callback: Callback) => ApplicationBuilder;
render: () => Promise<RenderResult>;
applicationUpdater: {
makeUpdateAvailable: (available: boolean) => void;
};
tray: {
click: (id: string) => Promise<void>;
get: (id: string) => TrayMenuItem | undefined;
};
applicationMenu: {
@ -149,6 +156,14 @@ export const getApplicationBuilder = () => {
const builder: ApplicationBuilder = {
dis,
applicationUpdater: {
makeUpdateAvailable: action((available: boolean) => {
const updateIsAvailableState = mainDi.inject(updateIsAvailableStateInjectable);
updateIsAvailableState.set(available);
}),
},
applicationMenu: {
click: async (path: string) => {
const applicationMenuItems = mainDi.inject(
@ -186,6 +201,14 @@ export const getApplicationBuilder = () => {
},
tray: {
get: (id: string) => {
const trayMenuItems = mainDi.inject(
trayMenuItemsInjectable,
);
return trayMenuItems.get().find(matches({ id }));
},
click: async (id: string) => {
const trayMenuItems = mainDi.inject(
trayMenuItemsInjectable,