auto-update notifications on the status bar (#5361)

* auto-update notifications on the status bar

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* integrated with new and improved autoupdate code

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* lint

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* refactored to include AutoUpdateComponent in the statusBarItemsInjectable

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* refactor to use a registrator

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* fix lint, remove unused code

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* refactored for the simplified logic for injecting many instances PR

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* using the update syncBox injectables directly for status bar notifications

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* removed update broadcasting/listening, updated snapshots

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* fixing tests

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* fixed tests

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* cleanup after rebase to master

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* address review comments

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* fixed incorrect injectable id

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* address review comments

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* Tweak code-style

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Jim EhrismannandJanne Savolainen authored and GitHub committed 2022-07-01 14:09:00 +03:00
1 parent 165bf57380
commit 3480b517c1
47 files changed
+1999 -792

No files matched your search

@@ -1,57 +0,0 @@
/**
* 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 type { ApplicationUpdateStatusChannel, ApplicationUpdateStatusEventId } from "../../common/application-update/application-update-status-channel.injectable";
import applicationUpdateStatusChannelInjectable from "../../common/application-update/application-update-status-channel.injectable";
import showInfoNotificationInjectable from "../components/notifications/show-info-notification.injectable";
import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token";
import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token";
const applicationUpdateStatusListenerInjectable = getInjectable({
id: "application-update-status-listener",
instantiate: (di): MessageChannelListener<ApplicationUpdateStatusChannel> => {
const channel = di.inject(applicationUpdateStatusChannelInjectable);
const showInfoNotification = di.inject(showInfoNotificationInjectable);
const eventHandlers: Record<ApplicationUpdateStatusEventId, { handle: (version?: string) => void }> = {
"checking-for-updates": {
handle: () => {
showInfoNotification("Checking for updates...");
},
},
"no-updates-available": {
handle: () => {
showInfoNotification("No new updates available");
},
},
"download-for-update-started": {
handle: (version) => {
showInfoNotification(`Download for version ${version} started...`);
},
},
"download-for-update-failed": {
handle: () => {
showInfoNotification("Download of update failed");
},
},
};
return {
channel,
handler: ({ eventId, version }) => {
eventHandlers[eventId].handle(version);
},
};
},
injectionToken: messageChannelListenerInjectionToken,
});
export default applicationUpdateStatusListenerInjectable;
@@ -0,0 +1,107 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import assert from "assert";
import React, { useState } from "react";
import { Spinner } from "../spinner";
import type { ProgressOfUpdateDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
import progressOfUpdateDownloadInjectable from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
import type { DiscoveredUpdateVersion } from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import type { UpdateIsBeingDownloaded } from "../../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
import updateIsBeingDownloadedInjectable from "../../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
import type { UpdatesAreBeingDiscovered } from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable";
import updatesAreBeingDiscoveredInjectable from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable";
import { now as reactiveDateNow } from "mobx-utils";
interface Dependencies {
progressOfUpdateDownload: ProgressOfUpdateDownload;
discoveredVersionState: DiscoveredUpdateVersion;
downloadingUpdateState: UpdateIsBeingDownloaded;
checkingForUpdatesState: UpdatesAreBeingDiscovered;
}
interface EndNoteProps {
version?: string;
note: (version: string) => JSX.Element;
}
const EndNote = observer(({ version, note }: EndNoteProps) => {
const [start] = useState(Date.now());
if (start + 5000 <= reactiveDateNow()) {
return idle();
}
return note(version ?? "");
});
const checking = () => (
<>
<Spinner/>
<div data-testid="app-update-checking">Checking for updates...</div>
</>
);
const available = (version: string) => <div data-testid="app-update-available">{`${version ?? "Update"} is available`}</div>;
const notAvailable = () => <div data-testid="app-update-not-available">No new updates available</div>;
const downloading = (version: string) => {
return (
<>
<div data-testid="app-update-downloading">{`Downloading version ${version}...`}</div>
<Spinner/>
</>
);
};
const downloadFailed = (errMsg: string) => <div data-testid="app-update-download-failed">{errMsg}</div>;
const idle = () => <div data-testid="app-update-idle"></div>;
export const NonInjectedAutoUpdateComponent = observer(({
progressOfUpdateDownload,
discoveredVersionState,
downloadingUpdateState,
checkingForUpdatesState,
}: Dependencies) => {
const discoveredVersion = discoveredVersionState.value.get();
const { failed } = progressOfUpdateDownload.value.get();
if (downloadingUpdateState.value.get()) {
assert(discoveredVersion);
return downloading(discoveredVersion.version);
}
if (checkingForUpdatesState.value.get()) {
return checking();
}
if ( discoveredVersion) {
return <EndNote note={available} version={discoveredVersion.version} />;
}
if ( failed ) {
return <EndNote note={downloadFailed} version={failed} />;
}
return <EndNote note={notAvailable} />;
});
export const AutoUpdateComponent = withInjectables<Dependencies>(NonInjectedAutoUpdateComponent, {
getProps: (di, props) => ({
progressOfUpdateDownload: di.inject(progressOfUpdateDownloadInjectable),
discoveredVersionState: di.inject(discoveredUpdateVersionInjectable),
downloadingUpdateState: di.inject(updateIsBeingDownloadedInjectable),
checkingForUpdatesState: di.inject(updatesAreBeingDiscoveredInjectable),
...props,
}),
});
@@ -0,0 +1,22 @@
/**
* 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 { AutoUpdateComponent } from "./auto-update-component";
import { statusBarItemInjectionToken } from "./status-bar-item-injection-token";
const autoUpdateStatusBarItemInjectable = getInjectable({
id: "auto-update-status-bar-item",
instantiate: () => ({
component: AutoUpdateComponent,
position: "left" as const,
visible: computed(() => true),
}),
injectionToken: statusBarItemInjectionToken,
});
export default autoUpdateStatusBarItemInjectable;
@@ -1,78 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type {
StatusBarItemProps,
StatusBarRegistration,
} from "./status-bar-registration";
import statusBarItemsInjectable from "./status-bar-items.injectable";
export interface RegisteredStatusBarItems {
right: React.ComponentType<StatusBarItemProps>[];
left: React.ComponentType<StatusBarItemProps>[];
}
interface Dependencies {
registrations: IComputedValue<StatusBarRegistration[]>;
}
function getRegisteredStatusBarItems({ registrations }: Dependencies): IComputedValue<RegisteredStatusBarItems> {
return computed(() => {
const res: RegisteredStatusBarItems = {
left: [],
right: [],
};
for (const registration of registrations.get()) {
if (!registration || typeof registration !== "object") {
continue;
}
if (registration.item) {
const { item } = registration;
// default for old API is "right"
res.right.push(
() => (
<>
{
typeof item === "function"
? item()
: item
}
</>
),
);
} else if (registration.components) {
const { position = "right", Item } = registration.components;
if (position !== "left" && position !== "right") {
throw new TypeError("StatusBarRegistration.components.position must be either 'right' or 'left'");
}
res[position].push(Item);
}
}
// This is done so that the first ones registered are closest to the corner
res.right.reverse();
return res;
});
}
const registeredStatusBarItemsInjectable = getInjectable({
id: "registered-status-bar-items",
instantiate: (di) => getRegisteredStatusBarItems({
registrations: di.inject(statusBarItemsInjectable),
}),
});
export default registeredStatusBarItemsInjectable;
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type React from "react";
export interface StatusBarItem {
component: React.ComponentType<any>;
position: "left" | "right";
visible: IComputedValue<boolean>;
}
export const statusBarItemInjectionToken = getInjectionToken<StatusBarItem>({
id: "status-bar-item",
});
@@ -0,0 +1,85 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import type { StatusBarItem } from "./status-bar-item-injection-token";
import { statusBarItemInjectionToken } from "./status-bar-item-injection-token";
import type { StatusBarRegistration } from "./status-bar-registration";
import React from "react";
import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { Logger } from "../../../common/logger";
const statusBarItemRegistratorInjectable = getInjectable({
id: "status-bar-item-registrator",
instantiate: (di) => (extension) => {
const rendererExtension = extension as LensRendererExtension;
const getRandomId = di.inject(getRandomIdInjectable);
const logger = di.inject(loggerInjectable);
return rendererExtension.statusBarItems.flatMap(
toItemInjectableFor(rendererExtension, getRandomId, logger),
);
},
injectionToken: extensionRegistratorInjectionToken,
});
export default statusBarItemRegistratorInjectable;
const toItemInjectableFor = (extension: LensRendererExtension, getRandomId: () => string, logger: Logger) => {
return (registration: StatusBarRegistration): Injectable<StatusBarItem, StatusBarItem, void>[] => {
const id = `${getRandomId()}-status-bar-item-for-extension-${extension.sanitizedExtensionId}`;
let component: React.ComponentType;
let position: "left" | "right";
if (registration?.item) {
const { item } = registration;
position = "right";
component =
() => (
<>
{
typeof item === "function"
? item()
: item
}
</>
);
} else if (registration?.components) {
const { position: pos = "right", Item } = registration.components;
if (pos !== "left" && pos !== "right") {
throw new TypeError("StatusBarRegistration.components.position must be either 'right' or 'left'");
}
position = pos;
component = Item;
} else {
logger.warn("StatusBarRegistration must have valid item or components field");
return [];
}
return [getInjectable({
id,
instantiate: () => ({
component,
position,
visible: computed(() => true),
}),
injectionToken: statusBarItemInjectionToken,
})];
};
};
@@ -1,21 +0,0 @@
/**
* 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 rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
const statusBarItemsInjectable = getInjectable({
id: "status-bar-items",
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() =>
extensions.get().flatMap((ext) => ext.statusBarItems),
);
},
});
export default statusBarItemsInjectable;
@@ -0,0 +1,61 @@
/**
* 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 type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type { StatusBarItemProps } from "./status-bar-registration";
import type { StatusBarItem } from "./status-bar-item-injection-token";
import { statusBarItemInjectionToken } from "./status-bar-item-injection-token";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
export interface StatusBarItems {
right: React.ComponentType<StatusBarItemProps>[];
left: React.ComponentType<StatusBarItemProps>[];
}
interface Dependencies {
registrations: IComputedValue<StatusBarItem[]>;
}
function getStatusBarItems({ registrations }: Dependencies): IComputedValue<StatusBarItems> {
return computed(() => {
const res: StatusBarItems = {
left: [],
right: [],
};
for (const registration of registrations.get()) {
const { position = "right", component, visible } = registration;
if (!visible.get()) {
continue;
}
res[position].push(component);
}
// This is done so that the first ones registered are closest to the corner
res.right.reverse();
return res;
});
}
const statusBarItemsInjectable = getInjectable({
id: "status-bar-items",
instantiate: (di) => {
const computedInjectMany = di.inject(
computedInjectManyInjectable,
);
return getStatusBarItems({
registrations: computedInjectMany(statusBarItemInjectionToken),
});
},
});
export default statusBarItemsInjectable;
@@ -5,18 +5,16 @@
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { StatusBar } from "./status-bar";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import type { IObservableArray } from "mobx";
import { computed, observable } from "mobx";
import type { DiContainer } from "@ogre-tools/injectable";
import type { StatusBarItems } from "./status-bar-items.injectable";
import statusBarItemsInjectable from "./status-bar-items.injectable";
import type { StatusBarRegistration } from "./status-bar-registration";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import type { ApplicationBuilder } from "../test-utils/get-application-builder";
import { getApplicationBuilder } from "../test-utils/get-application-builder";
import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
class SomeTestExtension extends LensRendererExtension {
constructor(statusBarItems: IObservableArray<any>) {
@@ -35,21 +33,29 @@ class SomeTestExtension extends LensRendererExtension {
}
describe("<StatusBar />", () => {
let render: DiRender;
let di: DiContainer;
let statusBarItems: IObservableArray<any>;
let applicationBuilder: ApplicationBuilder;
beforeEach(() => {
beforeEach(async () => {
statusBarItems = observable.array([]);
di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
rendererDi.unoverride(getRandomIdInjectable);
rendererDi.permitSideEffects(getRandomIdInjectable);
});
applicationBuilder.extensions.renderer.enable(new SomeTestExtension(statusBarItems));
di = applicationBuilder.dis.rendererDi;
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(rendererExtensionsInjectable, () => computed(() => [new SomeTestExtension(statusBarItems)]));
});
it("renders w/o errors", () => {
const { container } = render(<StatusBar />);
it("renders w/o errors", async () => {
const { container } = await applicationBuilder.render();
expect(container).toBeInstanceOf(HTMLElement);
});
@@ -62,26 +68,27 @@ describe("<StatusBar />", () => {
[],
[{}],
{},
])("renders w/o errors when registrations are not type compliant (%p)", val => {
])("renders w/o errors when registrations are not type compliant (%p)", async val => {
statusBarItems.replace([val]);
expect(() => render(<StatusBar />)).not.toThrow();
await expect(applicationBuilder.render()).resolves.toBeTruthy();
});
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", () => {
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", async () => {
const testId = "testId";
const text = "heee";
di.override(statusBarItemsInjectable, () => computed(() => [
{ item: <span data-testid={testId} >{text}</span> },
] as StatusBarRegistration[]));
di.override(statusBarItemsInjectable, () => computed(() => ({
right: [ () => <span data-testid={testId} >{text}</span> ],
left: [],
}) as StatusBarItems));
const { getByTestId } = render(<StatusBar />);
const { getByTestId } = await applicationBuilder.render();
expect(getByTestId(testId)).toHaveTextContent(text);
});
it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", () => {
it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", async () => {
const testId = "testId";
const text = "heee";
@@ -89,13 +96,13 @@ describe("<StatusBar />", () => {
item: () => <span data-testid={testId} >{text}</span>,
}]);
const { getByTestId } = render(<StatusBar />);
const { getByTestId } = await applicationBuilder.render();
expect(getByTestId(testId)).toHaveTextContent(text);
});
it("sort positioned items properly", () => {
it("sort positioned items properly", async () => {
statusBarItems.replace([
{
components: {
@@ -122,7 +129,7 @@ describe("<StatusBar />", () => {
},
]);
const { getAllByTestId } = render(<StatusBar />);
const { getAllByTestId } = await applicationBuilder.render();
const elems = getAllByTestId("sortedElem");
const positions = elems.map(elem => elem.textContent);
@@ -8,14 +8,14 @@ import styles from "./status-bar.module.scss";
import React from "react";
import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { RegisteredStatusBarItems } from "./registered-status-bar-items.injectable";
import registeredStatusBarItemsInjectable from "./registered-status-bar-items.injectable";
import type { StatusBarItems } from "./status-bar-items.injectable";
import statusBarItemsInjectable from "./status-bar-items.injectable";
import type { IComputedValue } from "mobx";
export interface StatusBarProps {}
interface Dependencies {
items: IComputedValue<RegisteredStatusBarItems>;
items: IComputedValue<StatusBarItems>;
}
const NonInjectedStatusBar = observer(({ items }: Dependencies & StatusBarProps) => {
@@ -23,14 +23,14 @@ const NonInjectedStatusBar = observer(({ items }: Dependencies & StatusBarProps)
return (
<div className={styles.StatusBar}>
<div className={styles.leftSide}>
<div className={styles.leftSide} data-testid="status-bar-left">
{left.map((Item, index) => (
<div className={styles.item} key={index}>
<Item />
</div>
))}
</div>
<div className={styles.rightSide}>
<div className={styles.rightSide} data-testid="status-bar-right">
{right.map((Item, index) => (
<div className={styles.item} key={index}>
<Item />
@@ -44,7 +44,7 @@ const NonInjectedStatusBar = observer(({ items }: Dependencies & StatusBarProps)
export const StatusBar = withInjectables<Dependencies, StatusBarProps>(NonInjectedStatusBar, {
getProps: (di, props) => ({
items: di.inject(registeredStatusBarItemsInjectable),
items: di.inject(statusBarItemsInjectable),
...props,
}),
});
@@ -17,8 +17,10 @@ export class TestExtension extends LensRendererExtension {}
export type FakeExtensionData = SetRequired<Partial<LensRendererExtension>, "id" | "name">;
export const getRendererExtensionFakeFor = (builder: ApplicationBuilder) => (
function getRendererExtensionFake({ id, name, ...rest }: FakeExtensionData) {
export type GetRendererExtensionFake = (fakeExtensionData: FakeExtensionData) => TestExtension;
export const getRendererExtensionFakeFor = (builder: ApplicationBuilder): GetRendererExtensionFake => (
function getRendererExtensionFake({ id, name, ...rest }) {
const instance = new TestExtension({
id,
absolutePath: "irrelevant",