Cherry-pick from 4.2.3 (#2628)

* Fix: logs data disapearing causing crashes (#2566)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Refactor helm-chart.api and improve kube validation and error handling (#2265)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix: HPA's not sortable by age (#2565)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Conditionally render status icon for kube meta (#2298)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix custom resource loading spinner appears above extensions' cluster menus (#2344)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Lens should point to the release docs (#2268)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Refactor the Extensions settings page (#2221)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* try and get jest to not core dump

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-04-27 08:11:50 +03:00
1 parent 7fde8125eb
commit 1f854d0a0f
42 files changed
+1777 -1067

No files matched your search

@@ -1,16 +1,18 @@
import "@testing-library/jest-dom/extend-expect";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, waitFor } from "@testing-library/react";
import fse from "fs-extra";
import React from "react";
import { UserStore } from "../../../../common/user-store";
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
import { ExtensionLoader } from "../../../../extensions/extension-loader";
import { ThemeStore } from "../../../theme.store";
import { ConfirmDialog } from "../../confirm-dialog";
import { Notifications } from "../../notifications";
import { ExtensionInstallationStateStore } from "../extension-install.store";
import { Extensions } from "../extensions";
import mockFs from "mock-fs";
jest.setTimeout(30000);
jest.mock("fs-extra");
jest.mock("../../notifications");
jest.mock("../../../../common/utils", () => ({
...jest.requireActual("../../../../common/utils"),
@@ -20,37 +22,30 @@ jest.mock("../../../../common/utils", () => ({
extractTar: jest.fn(() => Promise.resolve())
}));
jest.mock("../../notifications", () => ({
ok: jest.fn(),
error: jest.fn(),
info: jest.fn()
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
}));
jest.mock("electron", () => {
return {
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
};
});
describe("Extensions", () => {
beforeEach(async () => {
mockFs({
"tmp": {}
});
ExtensionInstallationStateStore.reset();
UserStore.resetInstance();
ThemeStore.resetInstance();
await UserStore.createInstance().load();
await ThemeStore.createInstance().init();
ExtensionLoader.resetInstance();
ExtensionDiscovery.resetInstance();
Extensions.installStates.clear();
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
ExtensionLoader.resetInstance();
ExtensionLoader.createInstance().addExtension({
id: "extensionId",
manifest: {
@@ -64,49 +59,38 @@ describe("Extensions", () => {
});
});
it("disables uninstall and disable buttons while uninstalling", async () => {
ExtensionDiscovery.getInstance().isLoaded = true;
render(<><Extensions /><ConfirmDialog/></>);
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
fireEvent.click(screen.getByText("Uninstall"));
// Approve confirm dialog
fireEvent.click(screen.getByText("Yes"));
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
expect(screen.getByText("Disable").closest("button")).toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
afterEach(() => {
mockFs.restore();
});
it("displays error notification on uninstall error", () => {
it("disables uninstall and disable buttons while uninstalling", async () => {
ExtensionDiscovery.getInstance().isLoaded = true;
(ExtensionDiscovery.getInstance().uninstallExtension as any).mockImplementationOnce(() =>
Promise.reject()
);
render(<><Extensions /><ConfirmDialog/></>);
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
const res = render(<><Extensions /><ConfirmDialog /></>);
fireEvent.click(screen.getByText("Uninstall"));
expect(res.getByText("Disable").closest("button")).not.toBeDisabled();
expect(res.getByText("Uninstall").closest("button")).not.toBeDisabled();
fireEvent.click(res.getByText("Uninstall"));
// Approve confirm dialog
fireEvent.click(screen.getByText("Yes"));
fireEvent.click(res.getByText("Yes"));
waitFor(() => {
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
expect(Notifications.error).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
expect(res.getByText("Disable").closest("button")).toBeDisabled();
expect(res.getByText("Uninstall").closest("button")).toBeDisabled();
}, {
timeout: 30000,
});
});
it("disables install button while installing", () => {
render(<Extensions />);
it("disables install button while installing", async () => {
const res = render(<Extensions />);
fireEvent.change(screen.getByPlaceholderText("Path or URL to an extension package", {
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve() as any);
fireEvent.change(res.getByPlaceholderText("Path or URL to an extension package", {
exact: false
}), {
target: {
@@ -114,13 +98,8 @@ describe("Extensions", () => {
}
});
fireEvent.click(screen.getByText("Install"));
waitFor(() => {
expect(screen.getByText("Install").closest("button")).toBeDisabled();
expect(fse.move).toHaveBeenCalledWith("");
expect(Notifications.error).not.toHaveBeenCalled();
});
fireEvent.click(res.getByText("Install"));
expect(res.getByText("Install").closest("button")).toBeDisabled();
});
it("displays spinner while extensions are loading", () => {
@@ -128,8 +107,11 @@ describe("Extensions", () => {
const { container } = render(<Extensions />);
expect(container.querySelector(".Spinner")).toBeInTheDocument();
});
it("does not display the spinner while extensions are not loading", async () => {
ExtensionDiscovery.getInstance().isLoaded = true;
const { container } = render(<Extensions />);
waitFor(() =>
expect(container.querySelector(".Spinner")).not.toBeInTheDocument()
@@ -0,0 +1,218 @@
import { action, computed, observable } from "mobx";
import logger from "../../../main/logger";
import { disposer, ExtendableDisposer } from "../../utils";
import * as uuid from "uuid";
import { broadcastMessage } from "../../../common/ipc";
import { ipcRenderer } from "electron";
export enum ExtensionInstallationState {
INSTALLING = "installing",
UNINSTALLING = "uninstalling",
IDLE = "idle",
}
const Prefix = "[ExtensionInstallationStore]";
export class ExtensionInstallationStateStore {
private static InstallingFromMainChannel = "extension-installation-state-store:install";
private static ClearInstallingFromMainChannel = "extension-installation-state-store:clear-install";
private static PreInstallIds = observable.set<string>();
private static UninstallingExtensions = observable.set<string>();
private static InstallingExtensions = observable.set<string>();
static bindIpcListeners() {
ipcRenderer
.on(ExtensionInstallationStateStore.InstallingFromMainChannel, (event, extId) => {
ExtensionInstallationStateStore.setInstalling(extId);
})
.on(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, (event, extId) => {
ExtensionInstallationStateStore.clearInstalling(extId);
});
}
@action static reset() {
logger.warn(`${Prefix}: resetting, may throw errors`);
ExtensionInstallationStateStore.InstallingExtensions.clear();
ExtensionInstallationStateStore.UninstallingExtensions.clear();
ExtensionInstallationStateStore.PreInstallIds.clear();
}
/**
* Strictly transitions an extension from not installing to installing
* @param extId the ID of the extension
* @throws if state is not IDLE
*/
@action static setInstalling(extId: string): void {
logger.debug(`${Prefix}: trying to set ${extId} as installing`);
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
if (curState !== ExtensionInstallationState.IDLE) {
throw new Error(`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`);
}
ExtensionInstallationStateStore.InstallingExtensions.add(extId);
}
/**
* Broadcasts that an extension is being installed by the main process
* @param extId the ID of the extension
*/
static setInstallingFromMain(extId: string): void {
broadcastMessage(ExtensionInstallationStateStore.InstallingFromMainChannel, extId);
}
/**
* Broadcasts that an extension is no longer being installed by the main process
* @param extId the ID of the extension
*/
static clearInstallingFromMain(extId: string): void {
broadcastMessage(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, extId);
}
/**
* Marks the start of a pre-install phase of an extension installation. The
* part of the installation before the tarball has been unpacked and the ID
* determined.
* @returns a disposer which should be called to mark the end of the install phase
*/
@action static startPreInstall(): ExtendableDisposer {
const preInstallStepId = uuid.v4();
logger.debug(`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`);
ExtensionInstallationStateStore.PreInstallIds.add(preInstallStepId);
return disposer(() => {
ExtensionInstallationStateStore.PreInstallIds.delete(preInstallStepId);
logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`);
});
}
/**
* Strictly transitions an extension from not uninstalling to uninstalling
* @param extId the ID of the extension
* @throws if state is not IDLE
*/
@action static setUninstalling(extId: string): void {
logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`);
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
if (curState !== ExtensionInstallationState.IDLE) {
throw new Error(`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`);
}
ExtensionInstallationStateStore.UninstallingExtensions.add(extId);
}
/**
* Strictly clears the INSTALLING state of an extension
* @param extId The ID of the extension
* @throws if state is not INSTALLING
*/
@action static clearInstalling(extId: string): void {
logger.debug(`${Prefix}: trying to clear ${extId} as installing`);
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
switch (curState) {
case ExtensionInstallationState.INSTALLING:
return void ExtensionInstallationStateStore.InstallingExtensions.delete(extId);
default:
throw new Error(`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`);
}
}
/**
* Strictly clears the UNINSTALLING state of an extension
* @param extId The ID of the extension
* @throws if state is not UNINSTALLING
*/
@action static clearUninstalling(extId: string): void {
logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`);
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
switch (curState) {
case ExtensionInstallationState.UNINSTALLING:
return void ExtensionInstallationStateStore.UninstallingExtensions.delete(extId);
default:
throw new Error(`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`);
}
}
/**
* Returns the current state of the extension. IDLE is default value.
* @param extId The ID of the extension
*/
static getInstallationState(extId: string): ExtensionInstallationState {
if (ExtensionInstallationStateStore.InstallingExtensions.has(extId)) {
return ExtensionInstallationState.INSTALLING;
}
if (ExtensionInstallationStateStore.UninstallingExtensions.has(extId)) {
return ExtensionInstallationState.UNINSTALLING;
}
return ExtensionInstallationState.IDLE;
}
/**
* Returns true if the extension is currently INSTALLING
* @param extId The ID of the extension
*/
static isExtensionInstalling(extId: string): boolean {
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.INSTALLING;
}
/**
* Returns true if the extension is currently UNINSTALLING
* @param extId The ID of the extension
*/
static isExtensionUninstalling(extId: string): boolean {
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.UNINSTALLING;
}
/**
* Returns true if the extension is currently IDLE
* @param extId The ID of the extension
*/
static isExtensionIdle(extId: string): boolean {
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.IDLE;
}
/**
* The current number of extensions installing
*/
@computed static get installing(): number {
return ExtensionInstallationStateStore.InstallingExtensions.size;
}
/**
* If there is at least one extension currently installing
*/
@computed static get anyInstalling(): boolean {
return ExtensionInstallationStateStore.installing > 0;
}
/**
* The current number of extensions preinstalling
*/
@computed static get preinstalling(): number {
return ExtensionInstallationStateStore.PreInstallIds.size;
}
/**
* If there is at least one extension currently downloading
*/
@computed static get anyPreinstalling(): boolean {
return ExtensionInstallationStateStore.preinstalling > 0;
}
/**
* If there is at least one installing or preinstalling step taking place
*/
@computed static get anyPreInstallingOrInstalling(): boolean {
return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling;
}
}
File diff suppressed because it is too large. Load diff