Change update warning level over time (#5445)

* Give an UpdateButton injectables

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* AppUpdateWarning class and injectable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Rename folder to app-update-warning

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add appPublishDateInjectable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Inject ipcRenderer into app-update-warning

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add setter for downloadedUpdateDate

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add tests for AppUpdateWarning

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* appPublishDate initial test

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Parsing app release date from the package version

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Introduce appUpdateWarningLevelInjectable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix withInjectables props

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Determine warning from days passed after update available

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Save update available date in session storage

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Move quitAndInstall() to separate function

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Register quit-and-install ipc channel

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Make update function injectable for button

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Update tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Set proper timeframes

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Setting global UTC timezone for tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Refresh tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Testing warning level changes over time

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove unused app-publish-date files

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Make linter happy

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Pass quitAndInstallUpdateInjectable to UpdateButton

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove updateAppInjectable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Initial tests for installing update using topbar button

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Refactor AppUpdateWarning class using injectables

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Permit side effects in injectable
periodicalCheckForUpdateWarningInjectable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Temporarily set noop for UpdateButton update()

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove previous implementation files

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Unoverriding topbar injectables

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Render TopBar inside ApplicationBuilder

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Move updateWarningLevelInjectable

to /common subfolders from /main

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Make UpdateButton warning prop observable

by using computed() structure

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Updating snapshots

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add sync box files allowing to restart & install

update from renderer process

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add Update button dropdown tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix jest.advanceTimersByTime usage

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Check for severe warning level

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Linter fixes

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove usage of legacy global causing trouble in unit tests

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Make update warning level completely reactive and responsibility of a renderer

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Remove dead code

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Remove tests that are covered by behaviour

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Fix restarting and installing update when pressing update button

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Update snapshots

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Remove dead code

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>

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

* Update more snapshots

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

* Remove duplication from behavioural unit test

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

* Reducing too large MenuItems height

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Reduce too large UpdateButton paddings

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add jest globalSetup into package.json

to fix timezone test

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Do not triggering computed value every time it is being used

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Alex AndreevandJanne Savolainen authored and GitHub committed 2022-06-28 16:27:19 +03:00
1 parent 1e8ba07cc3
commit 3084b7b8d9
47 files changed
+5534 -145

No files matched your search

@@ -8,8 +8,7 @@ import React, { useEffect, useRef } from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import { Icon } from "../../icon";
import { watchHistoryState } from "../../../remote-helpers/history-updater";
import { cssNames, noop } from "../../../utils";
import { cssNames } from "../../../utils";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TopBarRegistration } from "./top-bar-registration";
@@ -28,6 +27,7 @@ import goForwardInjectable from "./go-forward.injectable";
import closeWindowInjectable from "./close-window.injectable";
import maximizeWindowInjectable from "./maximize-window.injectable";
import toggleMaximizeWindowInjectable from "./toggle-maximize-window.injectable";
import watchHistoryStateInjectable from "../../../remote-helpers/watch-history-state.injectable";
interface Dependencies {
navigateToCatalog: NavigateToCatalog;
@@ -43,6 +43,7 @@ interface Dependencies {
minimizeWindow: () => void;
toggleMaximizeWindow: () => void;
closeWindow: () => void;
watchHistoryState: () => () => void;
}
const NonInjectedTopBar = observer(({
@@ -59,6 +60,7 @@ const NonInjectedTopBar = observer(({
closeWindow,
minimizeWindow,
toggleMaximizeWindow,
watchHistoryState,
}: Dependencies) => {
const elem = useRef<HTMLDivElement | null>(null);
@@ -115,7 +117,7 @@ const NonInjectedTopBar = observer(({
onClick={goForward}
disabled={!nextEnabled.get()}
/>
<UpdateButton update={noop} />
<UpdateButton />
</div>
<div className={styles.items}>
{renderRegisteredItems(items.get())}
@@ -196,5 +198,6 @@ export const TopBar = withInjectables<Dependencies>(NonInjectedTopBar, {
closeWindow: di.inject(closeWindowInjectable),
minimizeWindow: di.inject(maximizeWindowInjectable),
toggleMaximizeWindow: di.inject(toggleMaximizeWindowInjectable),
watchHistoryState: di.inject(watchHistoryStateInjectable),
}),
});
+1 -1
View File
@@ -55,7 +55,7 @@
display: flex;
align-items: center;
color: var(--color);
padding: 6px var(--padding);
padding: calc(var(--padding) / 2) var(--padding);
margin-bottom: -3px;
white-space: nowrap;
text-decoration: none;
@@ -62,6 +62,7 @@ import type { LensMainExtension } from "../../../extensions/lens-main-extension"
import type { LensExtension } from "../../../extensions/lens-extension";
import extensionInjectable from "../../../extensions/extension-loader/extension/extension.injectable";
import { TopBar } from "../layout/top-bar/top-bar";
type Callback = (dis: DiContainers) => void | Promise<void>;
@@ -125,6 +126,7 @@ interface DiContainers {
interface Environment {
renderSidebar: () => React.ReactNode;
renderTopBar: () => React.ReactNode;
renderStatusBar: () => React.ReactNode;
beforeRender: () => void;
onAllowKubeResource: () => void;
@@ -166,6 +168,8 @@ export const getApplicationBuilder = () => {
application: {
renderSidebar: () => null,
renderTopBar: () => <TopBar />,
renderStatusBar: () => <StatusBar />,
beforeRender: () => {
@@ -184,6 +188,7 @@ export const getApplicationBuilder = () => {
clusterFrame: {
renderSidebar: () => <Sidebar />,
renderStatusBar: () => null,
renderTopBar: () => null,
beforeRender: () => {},
onAllowKubeResource: () => {},
} as Environment,
@@ -482,6 +487,7 @@ export const getApplicationBuilder = () => {
rendered = render(
<Router history={history}>
{environment.renderSidebar()}
{environment.renderTopBar()}
{environment.renderStatusBar()}
<Observer>
@@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UpdateButton/> should render if warning level prop passed 1`] = `
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
`;
@@ -1,87 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { act } from "@testing-library/react";
import React from "react";
import { UpdateButton } from "../update-button";
import "@testing-library/jest-dom/extend-expect";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
const update = jest.fn();
describe("<UpdateButton/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
update.mockClear();
});
it("should not render if no warning level prop passed", () => {
const { queryByTestId } = render(<UpdateButton update={update} />);
expect(queryByTestId("update-button")).not.toBeInTheDocument();
});
it("should render if warning level prop passed", () => {
const { getByTestId } = render(<UpdateButton update={update} warningLevel="light" />);
expect(getByTestId("update-button")).toMatchSnapshot();
});
it("should open menu when clicked", async () => {
const { getByTestId } = render(<UpdateButton update={update} warningLevel="light" />);
const button = getByTestId("update-button");
act(() => button.click());
expect(getByTestId("update-lens-menu-item")).toBeInTheDocument();
});
it("should call update function when menu item clicked", () => {
const { getByTestId } = render(<UpdateButton update={update} warningLevel="light" />);
const button = getByTestId("update-button");
act(() => button.click());
const menuItem = getByTestId("update-lens-menu-item");
menuItem.click();
expect(update).toHaveBeenCalled();
});
it("should have light warning level", () => {
const { getByTestId } = render(<UpdateButton update={update} warningLevel="light" />);
const button = getByTestId("update-button");
expect(button.dataset.warningLevel).toBe("light");
});
it("should have medium warning level", () => {
const { getByTestId } = render(<UpdateButton update={update} warningLevel="medium" />);
const button = getByTestId("update-button");
expect(button.dataset.warningLevel).toBe("medium");
});
it("should have high warning level", () => {
const { getByTestId } = render(<UpdateButton update={update} warningLevel="high" />);
const button = getByTestId("update-button");
expect(button.dataset.warningLevel).toBe("high");
});
});
@@ -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 restartAndInstallUpdateChannel from "../../../common/application-update/restart-and-install-update-channel/restart-and-install-update-channel.injectable";
import messageToChannelInjectable from "../../utils/channel/message-to-channel.injectable";
const restartAndInstallUpdateInjectable = getInjectable({
id: "restart-and-install-update",
instantiate: (di) => {
const messageToChannel = di.inject(messageToChannelInjectable);
const channel = di.inject(restartAndInstallUpdateChannel);
return () => {
messageToChannel(channel);
};
},
});
export default restartAndInstallUpdateInjectable;
@@ -11,7 +11,7 @@
color: var(--accent-color);
display: flex;
align-items: center;
padding: 4px 8px;
padding: calc(var(--padding) / 4) var(--padding);
gap: 6px;
cursor: default;
position: relative;
@@ -11,22 +11,30 @@ import { Menu, MenuItem } from "../menu";
import { cssNames } from "../../utils";
import type { IconProps } from "../icon";
import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import restartAndInstallUpdateInjectable from "./restart-and-install-update.injectable";
import updateWarningLevelInjectable from "./update-warning-level.injectable";
interface UpdateButtonProps extends HTMLAttributes<HTMLButtonElement> {
warningLevel?: "light" | "medium" | "high";
interface UpdateButtonProps extends HTMLAttributes<HTMLButtonElement> {}
interface Dependencies {
warningLevel: IComputedValue<"light" | "medium" | "high" | "">;
update: () => void;
}
export function UpdateButton({ warningLevel, update, id }: UpdateButtonProps) {
export const NonInjectedUpdateButton = observer(({ warningLevel, update, id }: UpdateButtonProps & Dependencies) => {
const buttonId = id ?? "update-lens-button";
const menuIconProps: IconProps = { material: "update", small: true };
const [opened, setOpened] = useState(false);
const level = warningLevel.get();
const toggle = () => {
setOpened(!opened);
};
if (!warningLevel) {
if (!level) {
return null;
}
@@ -34,11 +42,11 @@ export function UpdateButton({ warningLevel, update, id }: UpdateButtonProps) {
<>
<button
data-testid="update-button"
data-warning-level={warningLevel}
data-warning-level={level}
id={buttonId}
className={cssNames(styles.updateButton, {
[styles.warningHigh]: warningLevel === "high",
[styles.warningMedium]: warningLevel === "medium",
[styles.warningHigh]: level === "high",
[styles.warningMedium]: level === "medium",
})}
>
Update
@@ -61,4 +69,15 @@ export function UpdateButton({ warningLevel, update, id }: UpdateButtonProps) {
</Menu>
</>
);
}
});
export const UpdateButton = withInjectables<Dependencies, UpdateButtonProps>(
NonInjectedUpdateButton,
{
getProps: (di, props) => ({
...props,
warningLevel: di.inject(updateWarningLevelInjectable),
update: di.inject(restartAndInstallUpdateInjectable),
}),
},
);
@@ -0,0 +1,45 @@
/**
* 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 { now as reactiveDateNow } from "mobx-utils";
import updateDownloadedDateTimeInjectable from "../../../common/application-update/update-downloaded-date-time/update-downloaded-date-time.injectable";
const updateWarningLevelInjectable = getInjectable({
id: "update-warning-level",
instantiate: (di) => {
const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable);
return computed(() => {
const downloadedAt = updateDownloadedDateTime.value.get();
if (!downloadedAt) {
return "";
}
const ONE_DAY = 1000 * 60 * 60 * 24;
const downloadedAtTimestamp = new Date(downloadedAt).getTime();
const currentDateTimeTimestamp = reactiveDateNow(ONE_DAY);
const elapsedTime = currentDateTimeTimestamp - downloadedAtTimestamp;
const elapsedDays = elapsedTime / ONE_DAY;
if (elapsedDays < 20) {
return "light";
}
if (elapsedDays >= 20 && elapsedDays < 25) {
return "medium";
}
return "high";
});
},
});
export default updateWarningLevelInjectable;
@@ -20,7 +20,6 @@ import { KubeObjectDetails } from "../../components/kube-object-details";
import { KubeConfigDialog } from "../../components/kubeconfig-dialog";
import { Sidebar } from "../../components/layout/sidebar";
import { Dock } from "../../components/dock";
import { watchHistoryState } from "../../remote-helpers/history-updater";
import { PortForwardDialog } from "../../port-forward";
import { DeleteClusterDialog } from "../../components/delete-cluster-dialog";
import type { NamespaceStore } from "../../components/+namespaces/store";
@@ -32,6 +31,7 @@ import currentRouteComponentInjectable from "../../routes/current-route-componen
import startUrlInjectable from "./start-url.injectable";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import currentPathInjectable from "../../routes/current-path.injectable";
import watchHistoryStateInjectable from "../../remote-helpers/watch-history-state.injectable";
interface Dependencies {
namespaceStore: NamespaceStore;
@@ -39,6 +39,7 @@ interface Dependencies {
startUrl: IComputedValue<string>;
subscribeStores: SubscribeStores;
currentPath: IComputedValue<string>;
watchHistoryState: () => () => void;
}
export const NonInjectedClusterFrame = observer(({
@@ -47,6 +48,7 @@ export const NonInjectedClusterFrame = observer(({
startUrl,
subscribeStores,
currentPath,
watchHistoryState,
}: Dependencies) => {
useEffect(() => disposer(
subscribeStores([
@@ -103,6 +105,7 @@ export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFram
startUrl: di.inject(startUrlInjectable),
currentRouteComponent: di.inject(currentRouteComponentInjectable),
currentPath: di.inject(currentPathInjectable),
watchHistoryState: di.inject(watchHistoryStateInjectable),
}),
});
+16
View File
@@ -52,6 +52,13 @@ import callForPublicHelmRepositoriesInjectable from "./components/+preferences/k
import platformInjectable from "../common/vars/platform.injectable";
import startTopbarStateSyncInjectable from "./components/layout/top-bar/start-state-sync.injectable";
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import watchHistoryStateInjectable from "./remote-helpers/watch-history-state.injectable";
import openAppContextMenuInjectable from "./components/layout/top-bar/open-app-context-menu.injectable";
import goBackInjectable from "./components/layout/top-bar/go-back.injectable";
import goForwardInjectable from "./components/layout/top-bar/go-forward.injectable";
import closeWindowInjectable from "./components/layout/top-bar/close-window.injectable";
import maximizeWindowInjectable from "./components/layout/top-bar/maximize-window.injectable";
import toggleMaximizeWindowInjectable from "./components/layout/top-bar/toggle-maximize-window.injectable";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@@ -96,6 +103,15 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
di.override(lensResourcesDirInjectable, () => "/irrelevant");
di.override(watchHistoryStateInjectable, () => () => () => {});
di.override(openAppContextMenuInjectable, () => () => {});
di.override(goBackInjectable, () => () => {});
di.override(goForwardInjectable, () => () => {});
di.override(closeWindowInjectable, () => () => {});
di.override(maximizeWindowInjectable, () => () => {});
di.override(toggleMaximizeWindowInjectable, () => () => {});
di.override(ipcRendererInjectable, () => ({
invoke: () => {},
on: () => {},
@@ -1,12 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { reaction } from "mobx";
import { emitWindowLocationChanged } from "../ipc";
import { navigation } from "../navigation";
export function watchHistoryState() {
return reaction(() => navigation.location, emitWindowLocationChanged);
}
@@ -0,0 +1,26 @@
/**
* 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 { emitWindowLocationChanged } from "../ipc";
import { reaction } from "mobx";
import observableHistoryInjectable from "../navigation/observable-history.injectable";
const watchHistoryStateInjectable = getInjectable({
id: "watch-history-state",
instantiate: (di) => {
const observableHistory = di.inject(observableHistoryInjectable);
return () => reaction(
() => observableHistory.location,
emitWindowLocationChanged,
);
},
causesSideEffects: true,
});
export default watchHistoryStateInjectable;