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

Add implementation for asking boolean over processes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-05-19 11:32:38 +03:00
parent 1b46ccd18b
commit e2de1449d5
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
11 changed files with 800 additions and 12 deletions

View File

@ -300,7 +300,12 @@ describe("installing update using tray", () => {
});
it("asks user to install update immediately", () => {
expect(askBooleanMock).toHaveBeenCalledWith("Do you want to install update some-version?");
expect(askBooleanMock).toHaveBeenCalledWith({
id: "install-update",
title: "Update Available",
question:
"Version some-version of Lens IDE is available and ready to be installed. Would you like to update now?",
});
});
describe("when user answers to install the update", () => {

View File

@ -0,0 +1,21 @@
/**
* 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 { Channel } from "../channel/channel-injection-token";
import { channelInjectionToken } from "../channel/channel-injection-token";
type AskBooleanAnswerChannel = Channel<{ id: string; value: boolean }>;
const askBooleanAnswerChannelInjectable = getInjectable({
id: "ask-boolean-answer-channel",
instantiate: (): AskBooleanAnswerChannel => ({
id: "ask-boolean-answer",
}),
injectionToken: channelInjectionToken,
});
export default askBooleanAnswerChannelInjectable;

View File

@ -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 type { Channel } from "../channel/channel-injection-token";
import { channelInjectionToken } from "../channel/channel-injection-token";
export interface AskBooleanQuestionParameters { id: string; title: string; question: string }
export type AskBooleanQuestionChannel = Channel<AskBooleanQuestionParameters>;
const askBooleanQuestionChannelInjectable = getInjectable({
id: "ask-boolean-question-channel",
instantiate: (): AskBooleanQuestionChannel => ({
id: "ask-boolean-question",
}),
injectionToken: channelInjectionToken,
});
export default askBooleanQuestionChannelInjectable;

View File

@ -0,0 +1,336 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ask-boolean given started when asking multiple questions renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-question-id"
>
<b>
some-title
</b>
<p>
Some question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-question-id-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-question-id-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-question-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-other-question-id"
>
<b>
some-other-title
</b>
<p>
Some other question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-other-question-id-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-other-question-id-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-other-question-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`ask-boolean given started when asking multiple questions when answering to first question renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-other-question-id"
>
<b>
some-other-title
</b>
<p>
Some other question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-other-question-id-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-other-question-id-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-other-question-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`ask-boolean given started when asking question renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-question-id"
>
<b>
some-title
</b>
<p>
Some question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-question-id-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-question-id-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-question-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`ask-boolean given started when asking question when user answers "no" renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`ask-boolean given started when asking question when user answers "yes" renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`ask-boolean given started when asking question when user closes notification without answering the question renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
const askBooleanPromiseInjectable = getInjectable({
id: "ask-boolean-promise",
instantiate: (di, questionId: string) => {
void questionId;
let resolve: (value: boolean) => void;
let _promise: Promise<boolean>;
return ({
get promise() {
return _promise;
},
clear: () => {
_promise = new Promise(_resolve => {
resolve = _resolve;
});
},
resolve: (value: boolean) => {
resolve(value); },
});
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, questionId: string) => questionId,
}),
});
export default askBooleanPromiseInjectable;

View File

@ -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 { channelListenerInjectionToken } from "../../common/channel/channel-listener-injection-token";
import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable";
const askBooleanReturnValueListenerInjectable = getInjectable({
id: "ask-boolean-return-value-listener",
instantiate: (di) => ({
channel: di.inject(askBooleanAnswerChannelInjectable),
handler: ({ id, value }) => {
const returnValuePromise = di.inject(askBooleanPromiseInjectable, id);
returnValuePromise.resolve(value);
},
}),
injectionToken: channelListenerInjectionToken,
});
export default askBooleanReturnValueListenerInjectable;

View File

@ -3,12 +3,37 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { sendToAgnosticChannelInjectionToken } from "../../common/channel/send-to-agnostic-channel-injection-token";
import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable";
export type AskBoolean = (question: string) => Promise<boolean>;
export type AskBoolean = ({
id,
title,
question,
}: {
id: string;
title: string;
question: string;
}) => Promise<boolean>;
const askBooleanInjectable = getInjectable({
id: "ask-boolean",
instantiate: (di): AskBoolean => async (question: string) => false,
instantiate: (di): AskBoolean => {
const sendToAgnosticChannel = di.inject(sendToAgnosticChannelInjectionToken);
const askBooleanChannel = di.inject(askBooleanQuestionChannelInjectable);
return async ({ id, title, question }) => {
const returnValuePromise = di.inject(askBooleanPromiseInjectable, id);
returnValuePromise.clear();
await sendToAgnosticChannel(askBooleanChannel, { id, title, question });
return await returnValuePromise.promise;
};
},
});
export default askBooleanInjectable;

View File

@ -0,0 +1,201 @@
/**
* 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 type { AskBoolean } from "./ask-boolean.injectable";
import askBooleanInjectable from "./ask-boolean.injectable";
import { getPromiseStatus } from "../../common/test-utils/get-promise-status";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
describe("ask-boolean", () => {
let applicationBuilder: ApplicationBuilder;
let askBoolean: AskBoolean;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
askBoolean = applicationBuilder.dis.mainDi.inject(askBooleanInjectable);
});
describe("given started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await applicationBuilder.render();
});
describe("when asking question", () => {
let actualPromise: Promise<boolean>;
beforeEach(() => {
actualPromise = askBoolean({
id: "some-question-id",
title: "some-title",
question: "Some question",
});
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows notification", () => {
const notification = rendered.getByTestId("ask-boolean-some-question-id");
expect(notification).not.toBeUndefined();
});
describe('when user answers "yes"', () => {
beforeEach(() => {
const button = rendered.getByTestId("ask-boolean-some-question-id-button-yes");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-question-id");
expect(notification).toBeNull();
});
it("resolves", async () => {
const actual = await actualPromise;
expect(actual).toBe(true);
});
});
describe('when user answers "no"', () => {
beforeEach(() => {
const button = rendered.getByTestId("ask-boolean-some-question-id-button-no");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-question-id");
expect(notification).toBeNull();
});
it("resolves", async () => {
const actual = await actualPromise;
expect(actual).toBe(false);
});
});
describe("when user closes notification without answering the question", () => {
beforeEach(() => {
const button = rendered.getByTestId("close-notification-for-ask-boolean-for-some-question-id");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-question-id");
expect(notification).toBeNull();
});
it("resolves", async () => {
const actual = await actualPromise;
expect(actual).toBe(false);
});
});
});
describe("when asking multiple questions", () => {
let firstQuestionPromise: Promise<boolean>;
let secondQuestionPromise: Promise<boolean>;
beforeEach(() => {
firstQuestionPromise = askBoolean({
id: "some-question-id",
title: "some-title",
question: "Some question",
});
secondQuestionPromise = askBoolean({
id: "some-other-question-id",
title: "some-other-title",
question: "Some other question",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows notification for first question", () => {
const notification = rendered.getByTestId("ask-boolean-some-question-id");
expect(notification).not.toBeUndefined();
});
it("shows notification for second question", () => {
const notification = rendered.getByTestId("ask-boolean-some-other-question-id");
expect(notification).not.toBeUndefined();
});
describe("when answering to first question", () => {
beforeEach(() => {
const button = rendered.getByTestId("ask-boolean-some-question-id-button-no");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification for first question anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-question-id");
expect(notification).toBeNull();
});
it("still shows notification for second question", () => {
const notification = rendered.getByTestId("ask-boolean-some-other-question-id");
expect(notification).not.toBeUndefined();
});
it("resolves first question", async () => {
const actual = await firstQuestionPromise;
expect(actual).toBe(false);
});
it("does not resolve second question yet", async () => {
const promiseStatus = await getPromiseStatus(secondQuestionPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
});
});
});
});

View File

@ -76,7 +76,11 @@ const checkForUpdatesTrayItemInjectable = getInjectable({
return;
}
const userWantsToInstallUpdate = await askBoolean(`Do you want to install update ${version}?`);
const userWantsToInstallUpdate = await askBoolean({
id: "install-update",
title: "Update available",
question: `Version ${version} of Lens IDE is available and ready to be installed. Would you like to update now?`,
});
if (userWantsToInstallUpdate) {
quitAndInstallUpdate();

View File

@ -0,0 +1,106 @@
/**
* 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 { channelListenerInjectionToken } from "../../common/channel/channel-listener-injection-token";
import type { AskBooleanQuestionParameters } from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
import showInfoNotificationInjectable from "../components/notifications/show-info-notification.injectable";
import { Button } from "../components/button";
import React from "react";
import { sendToAgnosticChannelInjectionToken } from "../../common/channel/send-to-agnostic-channel-injection-token";
import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable";
const askBooleanQuestionChannelListenerInjectable = getInjectable({
id: "ask-boolean-question-channel-listener",
instantiate: (di) => {
const questionChannel = di.inject(askBooleanQuestionChannelInjectable);
const showInfoNotification = di.inject(showInfoNotificationInjectable);
const sendToAgnosticChannel = di.inject(sendToAgnosticChannelInjectionToken);
const answerChannel = di.inject(askBooleanAnswerChannelInjectable);
const notificationsStore = di.inject(notificationsStoreInjectable);
const sendAnswerFor = (id: string) => (value: boolean) => {
sendToAgnosticChannel(answerChannel, { id, value });
};
const closeNotification = (notificationId: string) => {
notificationsStore.remove(notificationId);
};
const sendAnswerAndCloseNotificationFor = (sendAnswer: (value: boolean) => void, notificationId: string) => (value: boolean) => () => {
sendAnswer(value);
closeNotification(notificationId);
};
return {
channel: questionChannel,
handler: ({ id: questionId, title, question }: AskBooleanQuestionParameters) => {
const notificationId = `ask-boolean-for-${questionId}`;
const sendAnswer = sendAnswerFor(questionId);
const sendAnswerAndCloseNotification = sendAnswerAndCloseNotificationFor(sendAnswer, notificationId);
showInfoNotification(
<AskBoolean
id={questionId}
title={title}
message={question}
onNo={sendAnswerAndCloseNotification(false)}
onYes={sendAnswerAndCloseNotification(true)}
/>,
{
id: notificationId,
timeout: 0,
onClose: () => sendAnswer(false),
},
);
},
};
},
injectionToken: channelListenerInjectionToken,
});
export default askBooleanQuestionChannelListenerInjectable;
const AskBoolean = ({
id,
title,
message,
onNo,
onYes,
}: {
id: string;
title: string;
message: string;
onNo: () => void;
onYes: () => void;
}) => (
<div className="flex column gaps" data-testid={`ask-boolean-${id}`}>
<b>{title}</b>
<p>{message}</p>
<div className="flex gaps row align-left box grow">
<Button
light
label="Yes"
onClick={onYes}
data-testid={`ask-boolean-${id}-button-yes`}
/>
<Button
active
outlined
label="No"
data-testid={`ask-boolean-${id}-button-no`}
onClick={onNo}
/>
</div>
</div>
);

View File

@ -3,19 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { notificationsStore, NotificationStatus } from "./notifications.store";
import type { NotificationMessage, Notification } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import notificationsStoreInjectable from "./notifications-store.injectable";
const showInfoNotificationInjectable = getInjectable({
id: "show-info-notification",
instantiate: () => (message: string) =>
instantiate: (di) => {
const notificationsStore = di.inject(notificationsStoreInjectable);
return (message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) =>
notificationsStore.add({
status: NotificationStatus.INFO,
timeout: 5000,
message,
}),
causesSideEffects: true,
...customOpts,
});
},
});
export default showInfoNotificationInjectable;