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

Fix flakiness and improve tests for DeleteClusterDialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-23 16:19:40 -04:00
parent 7e47c633bf
commit aec299feb4
34 changed files with 2936 additions and 464 deletions

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 { computed } from "mobx";
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
const filteredCategoriesInjectable = getInjectable({
id: "filtered-categories",
instantiate: (di) => {
const registry = di.inject(catalogCategoryRegistryInjectable);
return computed(() => [...registry.filteredItems]);
},
});
export default filteredCategoriesInjectable;

View File

@ -0,0 +1,15 @@
/**
* 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 fsInjectable from "./fs.injectable";
export type DeleteFile = (filePath: string) => Promise<void>;
const deleteFileInjectable = getInjectable({
id: "delete-file",
instantiate: (di): DeleteFile => di.inject(fsInjectable).unlink,
});
export default deleteFileInjectable;

View File

@ -8,9 +8,6 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterVisibilityHandler = "cluster:visibility";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterDeleteHandler = "cluster:delete";
export const clusterSetDeletingHandler = "cluster:deleting:set";
export const clusterClearDeletingHandler = "cluster:deleting:clear";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
export const clusterStates = "cluster:states";

View File

@ -12,7 +12,7 @@ export type RequestFromChannel = <
channel: TChannel,
...request: TChannel["_requestSignature"] extends void
? []
: [TChannel["_requestSignature"]]
: [SetRequired<TChannel, "_requestSignature">["_requestSignature"]]
) => Promise<SetRequired<TChannel, "_responseSignature">["_responseSignature"]>;
export const requestFromChannelInjectionToken =

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 type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token";
export type ClearClusterAsDeletingChannel = RequestChannel<ClusterId, void>;
const clearClusterAsDeletingChannelInjectable = getInjectable({
id: "clear-cluster-as-deleting-channel",
instantiate: (): ClearClusterAsDeletingChannel => ({
id: "clear-cluster-as-deleting",
}),
injectionToken: requestChannelInjectionToken,
});
export default clearClusterAsDeletingChannelInjectable;

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 type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token";
export type DeleteClusterChannel = RequestChannel<ClusterId, void>;
const deleteClusterChannelInjectable = getInjectable({
id: "delete-cluster-channel",
instantiate: (): DeleteClusterChannel => ({
id: "delete-cluster",
}),
injectionToken: requestChannelInjectionToken,
});
export default deleteClusterChannelInjectable;

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 type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token";
export type SetClusterAsDeletingChannel = RequestChannel<ClusterId, void>;
const setClusterAsDeletingChannelInjectable = getInjectable({
id: "set-cluster-as-deleting-channel",
instantiate: (): SetClusterAsDeletingChannel => ({
id: "set-cluster-as-deleting",
}),
injectionToken: requestChannelInjectionToken,
});
export default setClusterAsDeletingChannelInjectable;

View File

@ -0,0 +1,283 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "@testing-library/jest-dom/extend-expect";
import { KubeConfig } from "@kubernetes/client-node";
import type { RenderResult } from "@testing-library/react";
import type { CreateCluster } from "../../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import createContextHandlerInjectable from "../../../main/context-handler/create-context-handler.injectable";
import createKubeconfigManagerInjectable from "../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../../main/kubectl/normalized-arch.injectable";
import openDeleteClusterDialogInjectable, { type OpenDeleteClusterDialog } from "../../../renderer/components/delete-cluster-dialog/open.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
import path from "path";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
const currentClusterServerUrl = "https://localhost";
const nonCurrentClusterServerUrl = "http://localhost";
const multiClusterConfig = `
apiVersion: v1
clusters:
- cluster:
server: ${currentClusterServerUrl}
name: some-current-context-cluster
- cluster:
server: ${nonCurrentClusterServerUrl}
name: some-non-current-context-cluster
contexts:
- context:
cluster: some-current-context-cluster
user: some-user
name: some-current-context
- context:
cluster: some-non-current-context-cluster
user: some-user
name: some-non-current-context
current-context: some-current-context
kind: Config
preferences: {}
users:
- name: some-user
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
const singleClusterServerUrl = "http://localhost";
const singleClusterConfig = `
apiVersion: v1
clusters:
- cluster:
server: ${singleClusterServerUrl}
name: some-cluster
contexts:
- context:
cluster: some-cluster
user: some-user
name: some-context
current-context: some-context
kind: Config
preferences: {}
users:
- name: some-user
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
describe("Deleting a cluster", () => {
let builder: ApplicationBuilder;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let rendered: RenderResult;
let config: KubeConfig;
beforeEach(async () => {
config = new KubeConfig();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(createContextHandlerInjectable, () => () => undefined as never);
mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never);
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
mainDi.override(normalizedPlatformInjectable, () => "darwin");
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(storesAndApisCanBeCreatedInjectable, () => true);
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
// TODO: remove this line when all global uses of appEventBus are removed
windowDi.permitSideEffects(appEventBusInjectable);
});
builder.afterWindowStart(windowDi => {
createCluster = windowDi.inject(createClusterInjectionToken);
const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable);
navigateToCatalog();
});
rendered = await builder.render();
});
describe("when the kubeconfig has multiple clusters", () => {
let currentCluster: Cluster;
let nonCurrentCluster: Cluster;
beforeEach(() => {
config.loadFromString(multiClusterConfig);
currentCluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
nonCurrentCluster = createCluster({
id: "some-non-current-context-cluster",
contextName: "some-non-current-context",
preferences: {
clusterName: "some-non-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
});
describe("when the dialog is opened for the current cluster", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog({
cluster: currentCluster,
config,
});
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows context switcher", () => {
expect(rendered.queryByText("Select new context...")).toBeInTheDocument();
});
it("shows warning", () => {
expect(rendered.queryByTestId("current-context-warning")).toBeInTheDocument();
});
});
describe("when the dialog is opened for not the current cluster", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog({
cluster: nonCurrentCluster,
config,
});
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows warning", () => {
expect(rendered.queryByTestId("kubeconfig-change-warning")).toBeInTheDocument();
});
it("does not show context switcher", () => {
expect(rendered.queryByText("Select new context...")).not.toBeInTheDocument();
});
describe("when context switching checkbox is clicked", () => {
beforeEach(() => {
rendered.getByTestId("delete-cluster-dialog-context-switch").click();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows context switcher", () => {
expect(rendered.queryByText("Select new context...")).toBeInTheDocument();
});
});
});
});
describe("when an internal kubeconfig cluster is used", () => {
let currentCluster: Cluster;
beforeEach(() => {
config.loadFromString(singleClusterConfig);
const directoryForKubeConfigs = builder.applicationWindow.only.di.inject(directoryForKubeConfigsInjectable);
currentCluster = createCluster({
id: "some-cluster",
contextName: "some-context",
preferences: {
clusterName: "some-cluster",
},
kubeConfigPath: path.join(directoryForKubeConfigs, "some-cluster.json"),
}, {
clusterServerUrl: singleClusterServerUrl,
});
});
describe("when the dialog is opened", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog({
cluster: currentCluster,
config,
});
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows warning", () => {
expect(rendered.queryByTestId("internal-kubeconfig-warning")).toBeInTheDocument();
});
});
});
describe("when the kubeconfig has only one cluster", () => {
let currentCluster: Cluster;
beforeEach(() => {
config.loadFromString(singleClusterConfig);
currentCluster = createCluster({
id: "some-cluster",
contextName: "some-context",
preferences: {
clusterName: "some-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: singleClusterServerUrl,
});
});
describe("when the dialog is opened", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog({
cluster: currentCluster,
config,
});
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows warning", () => {
expect(rendered.queryByTestId("no-more-contexts-warning")).toBeInTheDocument();
});
});
});
});

View File

@ -0,0 +1,23 @@
/**
* 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 { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import clustersThatAreBeingDeletedInjectable from "../../../../main/clusters-that-are-being-deleted.injectable";
import clearClusterAsDeletingChannelInjectable from "../common/clear-as-deleting-channel.injectable";
const clearClusterAsDeletingChannelHandlerInjectable = getInjectable({
id: "clear-cluster-as-deleting-channel-handler",
instantiate: (di) => {
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return {
channel: di.inject(clearClusterAsDeletingChannelInjectable),
handler: (clusterId) => clustersThatAreBeingDeleted.delete(clusterId),
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default clearClusterAsDeletingChannelHandlerInjectable;

View File

@ -0,0 +1,56 @@
/**
* 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 appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable";
import clusterFramesInjectable from "../../../../common/cluster-frames.injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import deleteFileInjectable from "../../../../common/fs/delete-file.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import deleteClusterChannelInjectable from "../common/delete-channel.injectable";
const deleteClusterChannelHandlerInjectable = getInjectable({
id: "delete-cluster-channel-handler",
instantiate: (di) => {
const appEventBus = di.inject(appEventBusInjectable);
const clusterStore = di.inject(clusterStoreInjectable);
const clusterFrames = di.inject(clusterFramesInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable);
const deleteFile = di.inject(deleteFileInjectable);
return {
channel: di.inject(deleteClusterChannelInjectable),
handler: async (clusterId) =>{
appEventBus.emit({ name: "cluster", action: "remove" });
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
cluster.disconnect();
clusterFrames.delete(cluster.id);
// Remove from the cluster store as well, this should clear any old settings
clusterStore.clusters.delete(cluster.id);
try {
// remove the local storage file
const localStorageFilePath = joinPaths(directoryForLensLocalStorage, `${cluster.id}.json`);
await deleteFile(localStorageFilePath);
} catch {
// ignore error
}
},
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default deleteClusterChannelHandlerInjectable;

View File

@ -0,0 +1,23 @@
/**
* 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 { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import clustersThatAreBeingDeletedInjectable from "../../../../main/clusters-that-are-being-deleted.injectable";
import setClusterAsDeletingChannelInjectable from "../common/set-as-deleting-channel.injectable";
const setClusterAsDeletingChannelHandlerInjectable = getInjectable({
id: "set-cluster-as-deleting-channel-handler",
instantiate: (di) => {
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return {
channel: di.inject(setClusterAsDeletingChannelInjectable),
handler: (clusterId) => clustersThatAreBeingDeleted.add(clusterId),
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default setClusterAsDeletingChannelHandlerInjectable;

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 { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import clearClusterAsDeletingChannelInjectable from "../common/clear-as-deleting-channel.injectable";
export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise<void>;
const requestClearClusterAsDeletingInjectable = getInjectable({
id: "request-clear-cluster-as-deleting",
instantiate: (di): RequestClearClusterAsDeleting => {
const requestChannel = di.inject(requestFromChannelInjectable);
const clearClusterAsDeletingChannel = di.inject(clearClusterAsDeletingChannelInjectable);
return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId);
},
});
export default requestClearClusterAsDeletingInjectable;

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 { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import deleteClusterChannelInjectable from "../common/delete-channel.injectable";
export type RequestDeleteCluster = (clusterId: ClusterId) => Promise<void>;
const requestDeleteClusterInjectable = getInjectable({
id: "request-delete-cluster",
instantiate: (di): RequestDeleteCluster => {
const requestChannel = di.inject(requestFromChannelInjectable);
const deleteClusterChannel = di.inject(deleteClusterChannelInjectable);
return (clusterId) => requestChannel(deleteClusterChannel, clusterId);
},
});
export default requestDeleteClusterInjectable;

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 { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import setClusterAsDeletingChannelInjectable from "../common/set-as-deleting-channel.injectable";
export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise<void>;
const requestSetClusterAsDeletingInjectable = getInjectable({
id: "request-set-cluster-as-deleting",
instantiate: (di): RequestSetClusterAsDeleting => {
const requestChannel = di.inject(requestFromChannelInjectable);
const setClusterAsDeletingChannel = di.inject(setClusterAsDeletingChannelInjectable);
return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId);
},
});
export default requestSetClusterAsDeletingInjectable;

View File

@ -6916,7 +6916,7 @@ exports[`add custom helm repository in preferences when navigating to preference
/>
</div>
<div
class="Animate opacity-scale Dialog flex center AddHelmRepoDialog modal enter leave"
class="Animate opacity-scale Dialog flex center AddHelmRepoDialog modal enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
@ -7502,7 +7502,7 @@ exports[`add custom helm repository in preferences when navigating to preference
/>
</div>
<div
class="Animate opacity-scale Dialog flex center AddHelmRepoDialog modal enter leave"
class="Animate opacity-scale Dialog flex center AddHelmRepoDialog modal enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div

View File

@ -17,6 +17,7 @@ import isPathInjectable from "../../renderer/components/input/validators/is-path
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
import { useFakeTime } from "../../common/test-utils/use-fake-time";
describe("add custom helm repository in preferences", () => {
let builder: ApplicationBuilder;
@ -33,6 +34,8 @@ describe("add custom helm repository in preferences", () => {
builder = getApplicationBuilder();
useFakeTime("2021-01-01 12:00:00");
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
showSuccessNotificationMock = jest.fn();

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 { observable } from "mobx";
import type { ClusterId } from "../common/cluster-types";
const clustersThatAreBeingDeletedInjectable = getInjectable({
id: "clusters-that-are-being-deleted",
instantiate: () => observable.set<ClusterId>(),
});
export default clustersThatAreBeingDeletedInjectable;

View File

@ -3,12 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import { setupIpcMainHandlers } from "./setup-ipc-main-handlers";
import loggerInjectable from "../../../../common/logger.injectable";
import clusterManagerInjectable from "../../../cluster/manager.injectable";
import applicationMenuItemsInjectable from "../../../menu/application-menu-items.injectable";
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable";
@ -21,14 +19,8 @@ const setupIpcMainHandlersInjectable = getInjectable({
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const directoryForLensLocalStorage = di.inject(
directoryForLensLocalStorageInjectable,
);
const clusterManager = di.inject(clusterManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const clusterStore = di.inject(clusterStoreInjectable);
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
@ -42,8 +34,6 @@ const setupIpcMainHandlersInjectable = getInjectable({
setupIpcMainHandlers({
applicationMenuItems,
getAbsolutePath,
directoryForLensLocalStorage,
clusterManager,
catalogEntityRegistry,
clusterStore,

View File

@ -5,7 +5,7 @@
import type { IpcMainInvokeEvent } from "electron";
import { BrowserWindow, Menu } from "electron";
import { clusterFrameMap } from "../../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../../common/ipc/cluster";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../../../common/ipc/cluster";
import type { ClusterId } from "../../../../common/cluster-types";
import { ClusterStore } from "../../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../../common/app-event-bus/event-bus";
@ -14,9 +14,7 @@ import type { CatalogEntityRegistry } from "../../../catalog";
import { pushCatalogToRenderer } from "../../../catalog-pusher";
import type { ClusterManager } from "../../../cluster/manager";
import { ResourceApplier } from "../../../resource-applier";
import { remove } from "fs-extra";
import type { IComputedValue, ObservableSet } from "mobx";
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
import type { IComputedValue } from "mobx";
import type { MenuItemOpts } from "../../../menu/application-menu-items.injectable";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../../common/ipc/window";
import { handleWindowAction, onLocationChange } from "../../../ipc/window";
@ -26,27 +24,21 @@ import type { Theme } from "../../../theme/operating-system-theme-state.injectab
import type { AskUserForFilePaths } from "../../../ipc/ask-user-for-file-paths.injectable";
interface Dependencies {
directoryForLensLocalStorage: string;
getAbsolutePath: GetAbsolutePath;
applicationMenuItems: IComputedValue<MenuItemOpts[]>;
clusterManager: ClusterManager;
catalogEntityRegistry: CatalogEntityRegistry;
clusterStore: ClusterStore;
operatingSystemTheme: IComputedValue<Theme>;
askUserForFilePaths: AskUserForFilePaths;
clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
}
export const setupIpcMainHandlers = ({
applicationMenuItems,
directoryForLensLocalStorage,
getAbsolutePath,
clusterManager,
catalogEntityRegistry,
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
clustersThatAreBeingDeleted,
}: Dependencies) => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
@ -85,40 +77,6 @@ export const setupIpcMainHandlers = ({
}
});
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "remove" });
const clusterStore = ClusterStore.getInstance();
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
cluster.disconnect();
clusterFrameMap.delete(cluster.id);
// Remove from the cluster store as well, this should clear any old settings
clusterStore.clusters.delete(cluster.id);
try {
// remove the local storage file
const localStorageFilePath = getAbsolutePath(directoryForLensLocalStorage, `${cluster.id}.json`);
await remove(localStorageFilePath);
} catch {
// ignore error
}
});
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
clustersThatAreBeingDeleted.add(clusterId);
});
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
clustersThatAreBeingDeleted.delete(clusterId);
});
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);

View File

@ -3,7 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { HelmChartManager } from "../helm-chart-manager";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
const getHelmChartVersionsInjectable = getInjectable({
@ -11,6 +12,7 @@ const getHelmChartVersionsInjectable = getInjectable({
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo);
return async (repoName: string, chartName: string) => {
const repo = await getActiveHelmRepository(repoName);
@ -19,7 +21,7 @@ const getHelmChartVersionsInjectable = getInjectable({
return undefined;
}
return HelmChartManager.forRepo(repo).chartVersions(chartName);
return getChartManager(repo).chartVersions(chartName);
};
},

View File

@ -9,23 +9,21 @@ import styles from "./catalog-menu.module.scss";
import React from "react";
import type { TreeItemProps } from "@material-ui/lab";
import { TreeItem, TreeView } from "@material-ui/lab";
import { catalogCategoryRegistry } from "../../api/catalog-category-registry";
import { Icon } from "../icon";
import { StylesProvider } from "@material-ui/core";
import { cssNames } from "../../utils";
import type { CatalogCategory } from "../../api/catalog-entity";
import { observer } from "mobx-react";
import { CatalogCategoryLabel } from "./catalog-category-label";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import filteredCategoriesInjectable from "../../../common/catalog/filtered-categories.injectable";
export interface CatalogMenuProps {
activeTab: string | undefined;
onItemClick: (id: string) => void;
}
function getCategories() {
return catalogCategoryRegistry.filteredItems;
}
function getCategoryIcon(category: CatalogCategory) {
const { icon } = category.metadata ?? {};
@ -44,44 +42,56 @@ function Item(props: TreeItemProps) {
);
}
export const CatalogMenu = observer((props: CatalogMenuProps) => {
return (
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
<StylesProvider injectFirst>
<div className="flex flex-col w-full">
<div className={styles.catalog}>Catalog</div>
<TreeView
defaultExpanded={["catalog"]}
defaultCollapseIcon={<Icon material="expand_more"/>}
defaultExpandIcon={<Icon material="chevron_right" />}
selected={props.activeTab || "browse"}
interface Dependencies {
filteredCategories: IComputedValue<CatalogCategory[]>;
}
const NonInjectedCatalogMenu = observer(({
activeTab,
filteredCategories,
onItemClick,
}: CatalogMenuProps & Dependencies) => (
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
<StylesProvider injectFirst>
<div className="flex flex-col w-full">
<div className={styles.catalog}>Catalog</div>
<TreeView
defaultExpanded={["catalog"]}
defaultCollapseIcon={<Icon material="expand_more" />}
defaultExpandIcon={<Icon material="chevron_right" />}
selected={activeTab || "browse"}
>
<Item
nodeId="browse"
label="Browse"
data-testid="*-tab"
onClick={() => onItemClick("*")} />
<Item
nodeId="catalog"
label={<div className={styles.parent}>Categories</div>}
className={cssNames(styles.bordered)}
>
<Item
nodeId="browse"
label="Browse"
data-testid="*-tab"
onClick={() => props.onItemClick("*")}
/>
<Item
nodeId="catalog"
label={<div className={styles.parent}>Categories</div>}
className={cssNames(styles.bordered)}
>
{
getCategories().map(category => (
{
filteredCategories.get()
.map(category => (
<Item
icon={getCategoryIcon(category)}
key={category.getId()}
nodeId={category.getId()}
label={<CatalogCategoryLabel category={category}/>}
label={<CatalogCategoryLabel category={category} />}
data-testid={`${category.getId()}-tab`}
onClick={() => props.onItemClick(category.getId())}
/>
onClick={() => onItemClick(category.getId())} />
))
}
</Item>
</TreeView>
</div>
</StylesProvider>
);
}
</Item>
</TreeView>
</div>
</StylesProvider>
));
export const CatalogMenu = withInjectables<Dependencies, CatalogMenuProps>(NonInjectedCatalogMenu, {
getProps: (di, props) => ({
...props,
filteredCategories: di.inject(filteredCategoriesInjectable),
}),
});

View File

@ -7,6 +7,7 @@ import "./animate.scss";
import React, { useEffect, useState } from "react";
import { cssNames, noop } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { RequestAnimationFrame } from "./request-animation-frame.injectable";
import requestAnimationFrameInjectable from "./request-animation-frame.injectable";
import defaultEnterDurationForAnimatedInjectable from "./default-enter-duration.injectable";
import defaultLeaveDurationForAnimatedInjectable from "./default-leave-duration.injectable";
@ -24,7 +25,7 @@ export interface AnimateProps {
}
interface Dependencies {
requestAnimationFrame: (callback: () => void) => void;
requestAnimationFrame: RequestAnimationFrame;
defaultEnterDuration: number;
defaultLeaveDuration: number;
}
@ -46,21 +47,25 @@ const NonInjectedAnimate = (propsAndDeps: AnimateProps & Dependencies) => {
onLeave: onLeaveHandler = noop<[]>,
} = props;
const [isVisible, setIsVisible] = useState(enter);
const [isVisible, setIsVisible] = useState(false);
const [showClassNameEnter, setShowClassNameEnter] = useState(false);
const [showClassNameLeave, setShowClassNameLeave] = useState(false);
const contentElem = React.Children.only(children) as React.ReactElement<React.HTMLAttributes<any>>;
const onEnter = () => {
setIsVisible(true);
const classNames = cssNames("Animate", name, contentElem.props.className, {
enter: showClassNameEnter,
leave: showClassNameLeave,
});
requestAnimationFrame(() => {
setShowClassNameEnter(true);
onEnterHandler();
});
};
const onLeave = () => {
if (isVisible) {
useEffect(() => {
if (enter) {
setIsVisible(true);
requestAnimationFrame(() => {
setShowClassNameEnter(true);
onEnterHandler();
});
} else if (isVisible) {
setShowClassNameLeave(true);
onLeaveHandler();
@ -71,16 +76,7 @@ const NonInjectedAnimate = (propsAndDeps: AnimateProps & Dependencies) => {
setShowClassNameLeave(false);
}, leaveDuration);
}
};
const toggle = (entering: boolean) => {
if (entering) {
onEnter();
} else {
onLeave();
}
};
useEffect(() => toggle(enter), [enter]);
}, [enter]);
if (!isVisible) {
return null;
@ -92,10 +88,7 @@ const NonInjectedAnimate = (propsAndDeps: AnimateProps & Dependencies) => {
} as React.CSSProperties;
return React.cloneElement(contentElem, {
className: cssNames("Animate", name, contentElem.props.className, {
enter: showClassNameEnter,
leave: showClassNameLeave,
}),
className: classNames,
children: contentElem.props.children,
style: {
...contentElem.props.style,
@ -112,5 +105,3 @@ export const Animate = withInjectables<Dependencies, AnimateProps>(NonInjectedAn
defaultLeaveDuration: di.inject(defaultLeaveDurationForAnimatedInjectable),
}),
});

View File

@ -1,275 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "@testing-library/jest-dom/extend-expect";
import { KubeConfig } from "@kubernetes/client-node";
import { fireEvent } from "@testing-library/react";
import type { RenderResult } from "@testing-library/react";
import * as selectEvent from "react-select-event";
import type { CreateCluster } from "../../../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import createKubeconfigManagerInjectable from "../../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
import type { ApplicationBuilder } from "../../test-utils/get-application-builder";
import { getApplicationBuilder } from "../../test-utils/get-application-builder";
import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable";
import type { OpenDeleteClusterDialog } from "../open.injectable";
import openDeleteClusterDialogInjectable from "../open.injectable";
const currentClusterServerUrl = "https://localhost";
const nonCurrentClusterServerUrl = "http://localhost";
const multiClusterConfig = `
apiVersion: v1
clusters:
- cluster:
server: ${currentClusterServerUrl}
name: some-current-context-cluster
- cluster:
server: ${nonCurrentClusterServerUrl}
name: some-non-current-context-cluster
contexts:
- context:
cluster: some-current-context-cluster
user: some-user
name: some-current-context
- context:
cluster: some-non-current-context-cluster
user: some-user
name: some-non-current-context
current-context: some-current-context
kind: Config
preferences: {}
users:
- name: some-user
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
const singleClusterServerUrl = "http://localhost";
const singleClusterConfig = `
apiVersion: v1
clusters:
- cluster:
server: ${singleClusterServerUrl}
name: some-cluster
contexts:
- context:
cluster: some-cluster
user: some-user
name: some-context
current-context: some-context
kind: Config
preferences: {}
users:
- name: some-user
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
describe("<DeleteClusterDialog />", () => {
let builder: ApplicationBuilder;
beforeEach(() => {
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(createContextHandlerInjectable, () => () => undefined as never);
mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never);
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
mainDi.override(normalizedPlatformInjectable, () => "darwin");
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(storesAndApisCanBeCreatedInjectable, () => true);
});
});
it("shows context switcher when deleting current cluster", async () => {
const config = new KubeConfig();
config.loadFromString(multiClusterConfig);
const rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const createCluster = windowDi.inject(createClusterInjectionToken);
const cluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
const openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
openDeleteClusterDialog({ cluster, config });
const { getByText } = rendered;
const menu = getByText("Select new context...");
expect(menu).toBeInTheDocument();
selectEvent.openMenu(menu);
expect(getByText("some-current-context")).toBeInTheDocument();
expect(getByText("some-non-current-context")).toBeInTheDocument();
});
describe("Kubeconfig with different clusters", () => {
let rendered: RenderResult;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let config: KubeConfig;
beforeEach(async () => {
config = new KubeConfig();
config.loadFromString(multiClusterConfig);
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
createCluster = windowDi.inject(createClusterInjectionToken);
});
it("renders w/o errors", () => {
expect(rendered.container).toBeInstanceOf(HTMLElement);
});
it("shows warning when deleting non-current-context cluster", () => {
const cluster = createCluster({
id: "some-non-current-context-cluster",
contextName: "some-non-current-context",
preferences: {
clusterName: "minikube",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: nonCurrentClusterServerUrl,
});
openDeleteClusterDialog({ cluster, config });
const message = "The contents of kubeconfig file will be changed!";
expect(rendered.getByText(message)).toBeInstanceOf(HTMLElement);
});
it("shows warning when deleting current-context cluster", () => {
const cluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
openDeleteClusterDialog({ cluster, config });
expect(rendered.getByTestId("current-context-warning")).toBeInstanceOf(HTMLElement);
});
it("shows context switcher after checkbox click", () => {
const cluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
openDeleteClusterDialog({ cluster, config });
const { getByText, getByTestId } = rendered;
const link = getByTestId("context-switch");
expect(link).toBeInstanceOf(HTMLElement);
fireEvent.click(link);
const menu = getByText("Select new context...");
expect(menu).toBeInTheDocument();
selectEvent.openMenu(menu);
expect(getByText("some-current-context")).toBeInTheDocument();
expect(getByText("some-non-current-context")).toBeInTheDocument();
});
it("given cluster in internal kubeconfig, when deleting cluster outside of current context, shows warning for internal kubeconfig cluster", () => {
const cluster = createCluster({
id: "some-non-current-context-cluster",
contextName: "some-non-current-context",
preferences: {
clusterName: "some-non-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: nonCurrentClusterServerUrl,
});
const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true);
openDeleteClusterDialog({ cluster, config });
expect(rendered.getByTestId("internal-kubeconfig-warning")).toBeInstanceOf(HTMLElement);
spy.mockRestore();
});
});
describe("Kubeconfig with single cluster", () => {
let rendered: RenderResult;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let config: KubeConfig;
beforeEach(async () => {
config = new KubeConfig();
config.loadFromString(singleClusterConfig);
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
createCluster = windowDi.inject(createClusterInjectionToken);
});
it("shows warning if no other contexts left", () => {
const cluster = createCluster({
id: "some-cluster",
contextName: "some-context",
preferences: {
clusterName: "some-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: singleClusterServerUrl,
});
openDeleteClusterDialog({ cluster, config });
expect(rendered.getByTestId("no-more-contexts-warning")).toBeInstanceOf(HTMLElement);
});
});
});

View File

@ -17,8 +17,6 @@ const deleteClusterDialogClusterFrameChildComponentInjectable = getInjectable({
}),
injectionToken: clusterFrameChildComponentInjectionToken,
causesSideEffects: true,
});
export default deleteClusterDialogClusterFrameChildComponentInjectable;

View File

@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { dumpYaml } from "@kubernetes/client-node";
import fs from "fs";
import * as lockFile from "proper-lockfile";
export async function saveKubeconfig(config: KubeConfig, path: string) {
try {
const release = await lockFile.lock(path);
const contents = dumpYaml(JSON.parse(config.exportConfig()));
await fs.promises.writeFile(path, contents);
await release();
} catch (e) {
throw new Error(`Failed to acquire lock file.\n${e}`);
}
}

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import saveKubeconfigInjectable from "./save-kubeconfig.injectable";
export default getGlobalOverride(saveKubeconfigInjectable, () => async () => {
throw new Error("tried to save a mondified kubeconfig without override");
});

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 type { KubeConfig } from "@kubernetes/client-node";
import { dumpYaml } from "@kubernetes/client-node";
import * as lockFile from "proper-lockfile";
import { getInjectable } from "@ogre-tools/injectable";
import writeFileInjectable from "../../../common/fs/write-file.injectable";
export type SaveKubeconfig = (config: KubeConfig, path: string) => Promise<void>;
const saveKubeconfigInjectable = getInjectable({
id: "save-kubeconfig",
instantiate: (di): SaveKubeconfig => {
const writeFile = di.inject(writeFileInjectable);
return async (config, filePath) => {
const release = await lockFile.lock(filePath);
try {
const contents = dumpYaml(JSON.parse(config.exportConfig()));
await writeFile(filePath, contents);
} finally {
await release();
}
};
},
causesSideEffects: true,
});
export default saveKubeconfigInjectable;

View File

@ -10,22 +10,34 @@ import { observer } from "mobx-react";
import React from "react";
import { Button } from "../button";
import { saveKubeconfig } from "./save-config";
import { Notifications } from "../notifications";
import type { ShowNotification } from "../notifications";
import { Dialog } from "../dialog";
import { Icon } from "../icon";
import { Select } from "../select";
import { Checkbox } from "../checkbox";
import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc";
import type { HotbarStore } from "../../../common/hotbars/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { DeleteClusterDialogState } from "./state.injectable";
import deleteClusterDialogStateInjectable from "./state.injectable";
import type { RequestSetClusterAsDeleting } from "../../../features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable";
import requestSetClusterAsDeletingInjectable from "../../../features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable";
import type { RequestClearClusterAsDeleting } from "../../../features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable";
import requestClearClusterAsDeletingInjectable from "../../../features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable";
import type { RequestDeleteCluster } from "../../../features/cluster/delete-dialog/renderer/request-delete.injectable";
import requestDeleteClusterInjectable from "../../../features/cluster/delete-dialog/renderer/request-delete.injectable";
import type { SaveKubeconfig } from "./save-kubeconfig.injectable";
import saveKubeconfigInjectable from "./save-kubeconfig.injectable";
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
interface Dependencies {
state: IObservableValue<DeleteClusterDialogState | undefined>;
hotbarStore: HotbarStore;
requestSetClusterAsDeleting: RequestSetClusterAsDeleting;
requestDeleteCluster: RequestDeleteCluster;
requestClearClusterAsDeleting: RequestClearClusterAsDeleting;
showErrorNotification: ShowNotification;
saveKubeconfig: SaveKubeconfig;
}
@observer
@ -61,18 +73,18 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
async onDelete(state: DeleteClusterDialogState) {
const { cluster, config } = state;
await requestSetClusterAsDeleting(cluster.id);
await this.props.requestSetClusterAsDeleting(cluster.id);
this.removeContext(state);
this.changeCurrentContext(state);
try {
await saveKubeconfig(config, cluster.kubeConfigPath);
await this.props.saveKubeconfig(config, cluster.kubeConfigPath);
this.props.hotbarStore.removeAllHotbarItems(cluster.id);
await requestDeleteCluster(cluster.id);
await this.props.requestDeleteCluster(cluster.id);
} catch(error) {
Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`);
this.props.showErrorNotification(`Cannot remove cluster, failed to process config file. ${error}`);
} finally {
await requestClearClusterAsDeleting(cluster.id);
await this.props.requestClearClusterAsDeleting(cluster.id);
}
this.onClose();
@ -155,6 +167,14 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
}
getWarningMessage({ cluster, config }: DeleteClusterDialogState) {
if (cluster.isInLocalKubeconfig()) {
return (
<p data-testid="internal-kubeconfig-warning">
Are you sure you want to delete it? It can be re-added through the copy/paste mechanism.
</p>
);
}
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
if (!contexts.length) {
@ -173,14 +193,6 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
);
}
if (cluster.isInLocalKubeconfig()) {
return (
<p data-testid="internal-kubeconfig-warning">
Are you sure you want to delete it? It can be re-added through the copy/paste mechanism.
</p>
);
}
return (
<p data-testid="kubeconfig-change-warning">The contents of kubeconfig file will be changed!</p>
);
@ -209,7 +221,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
<hr className={styles.hr} />
<div className="mt-4">
<Checkbox
data-testid="context-switch"
data-testid="delete-cluster-dialog-context-switch"
label={(
<>
<span className="font-semibold">Select current-context</span>
@ -255,6 +267,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
close={this.close}
onClose={this.onClose}
onOpen={state && (() => this.onOpen(state))}
data-testid={state ? "delete-cluster-dialog" : undefined}
>
{state && this.renderContents(state)}
</Dialog>
@ -266,5 +279,10 @@ export const DeleteClusterDialog = withInjectables<Dependencies>(NonInjectedDele
getProps: (di) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
state: di.inject(deleteClusterDialogStateInjectable),
requestSetClusterAsDeleting: di.inject(requestSetClusterAsDeletingInjectable),
requestClearClusterAsDeleting: di.inject(requestClearClusterAsDeletingInjectable),
requestDeleteCluster: di.inject(requestDeleteClusterInjectable),
saveKubeconfig: di.inject(saveKubeconfigInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable),
}),
});

View File

@ -146,13 +146,11 @@ class NonInjectedDialog extends React.PureComponent<DialogProps & Dependencies &
};
render() {
const { modal, animated, pinned, "data-testid": testId } = this.props;
let { className } = this.props;
const { modal, animated, pinned, "data-testid": testId, className } = this.props;
className = cssNames("Dialog flex center", className, { modal, pinned });
let dialog = (
<div
className={className}
className={cssNames("Dialog flex center", className, { modal, pinned })}
onClick={stopPropagation}
ref={this.ref}
data-testid={testId}

View File

@ -68,6 +68,7 @@ import type { FakeExtensionOptions } from "./get-extension-fake";
import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake";
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
type Callback = (di: DiContainer) => void | Promise<void>;
@ -114,6 +115,7 @@ export interface ApplicationBuilder {
allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder;
beforeApplicationStart: (callback: Callback) => ApplicationBuilder;
beforeWindowStart: (callback: Callback) => ApplicationBuilder;
afterWindowStart: (callback: Callback) => ApplicationBuilder;
startHidden: () => Promise<void>;
render: () => Promise<RenderResult>;
@ -168,6 +170,11 @@ export const getApplicationBuilder = () => {
const beforeApplicationStartCallbacks: Callback[] = [];
const beforeWindowStartCallbacks: Callback[] = [];
const afterWindowStartCallbacks: Callback[] = [];
const fsState = new Map();
overrideFsWithFakes(mainDi, fsState);
let environment = environments.application;
@ -202,6 +209,7 @@ export const getApplicationBuilder = () => {
const windowDi = getRendererDi({ doGeneralOverrides: true });
overrideChannelsForWindow(windowDi, windowId);
overrideFsWithFakes(windowDi, fsState);
runInAction(() => {
windowDi.register(rendererExtensionsStateInjectable);
@ -235,6 +243,10 @@ export const getApplicationBuilder = () => {
await startFrame();
for (const callback of afterWindowStartCallbacks) {
await callback(windowDi);
}
const history = windowDi.inject(historyInjectable);
const render = renderFor(windowDi);
@ -646,6 +658,18 @@ export const getApplicationBuilder = () => {
return builder;
},
afterWindowStart(callback) {
const alreadyRenderedWindows = builder.applicationWindow.getAll();
alreadyRenderedWindows.forEach((window) => {
callback(window.di);
});
afterWindowStartCallbacks.push(callback);
return builder;
},
startHidden: async () => {
mainDi.inject(lensProxyPortInjectable).set(42);

View File

@ -53,7 +53,6 @@ import cronJobTriggerDialogClusterFrameChildComponentInjectable from "./componen
import deploymentScaleDialogClusterFrameChildComponentInjectable from "./components/+workloads-deployments/scale/deployment-scale-dialog-cluster-frame-child-component.injectable";
import replicasetScaleDialogClusterFrameChildComponentInjectable from "./components/+workloads-replicasets/scale-dialog/replicaset-scale-dialog-cluster-frame-child-component.injectable";
import statefulsetScaleDialogClusterFrameChildComponentInjectable from "./components/+workloads-statefulsets/scale/statefulset-scale-dialog-cluster-frame-child-component.injectable";
import deleteClusterDialogClusterFrameChildComponentInjectable from "./components/delete-cluster-dialog/delete-cluster-dialog-cluster-frame-child-component.injectable";
import kubeObjectDetailsClusterFrameChildComponentInjectable from "./components/kube-object-details/kube-object-details-cluster-frame-child-component.injectable";
import kubeconfigDialogClusterFrameChildComponentInjectable from "./components/kubeconfig-dialog/kubeconfig-dialog-cluster-frame-child-component.injectable";
import portForwardDialogClusterFrameChildComponentInjectable from "./port-forward/port-forward-dialog-cluster-frame-child-component.injectable";
@ -149,7 +148,6 @@ export const getDiForUnitTesting = (
deploymentScaleDialogClusterFrameChildComponentInjectable,
replicasetScaleDialogClusterFrameChildComponentInjectable,
statefulsetScaleDialogClusterFrameChildComponentInjectable,
deleteClusterDialogClusterFrameChildComponentInjectable,
kubeObjectDetailsClusterFrameChildComponentInjectable,
kubeconfigDialogClusterFrameChildComponentInjectable,
portForwardDialogClusterFrameChildComponentInjectable,

View File

@ -4,7 +4,7 @@
*/
import type { OpenDialogOptions } from "electron";
import { clusterActivateHandler, clusterClearDeletingHandler, clusterDeleteHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetDeletingHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster";
import { clusterActivateHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster";
import type { ClusterId, ClusterState } from "../../common/cluster-types";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel, type WindowAction } from "../../common/ipc/window";
import { openFilePickingDialogChannel } from "../../common/ipc/dialog";
@ -60,18 +60,6 @@ export function requestClusterDisconnection(clusterId: ClusterId, force?: boolea
return requestMain(clusterDisconnectHandler, clusterId, force);
}
export function requestSetClusterAsDeleting(clusterId: ClusterId): Promise<void> {
return requestMain(clusterSetDeletingHandler, clusterId);
}
export function requestClearClusterAsDeleting(clusterId: ClusterId): Promise<void> {
return requestMain(clusterClearDeletingHandler, clusterId);
}
export function requestDeleteCluster(clusterId: ClusterId): Promise<void> {
return requestMain(clusterDeleteHandler, clusterId);
}
export function requestInitialClusterStates(): Promise<{ id: string; state: ClusterState }[]> {
return requestMain(clusterStates);
}

View File

@ -7,10 +7,9 @@ import readFileInjectable from "../common/fs/read-file.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import pathExistsInjectable from "../common/fs/path-exists.injectable";
import deleteFileInjectable from "../common/fs/delete-file.injectable";
export const overrideFsWithFakes = (di: DiContainer) => {
const state = new Map();
export const overrideFsWithFakes = (di: DiContainer, state = new Map()) => {
const readFile = readFileFor(state);
di.override(readFileInjectable, () => readFile);
@ -25,6 +24,9 @@ export const overrideFsWithFakes = (di: DiContainer) => {
di.override(pathExistsInjectable, () => (
(filePath: string) => Promise.resolve(state.has(filePath))
));
di.override(deleteFileInjectable, () => async (filePath: string) => {
state.delete(filePath);
});
};
const readFileFor = (state: Map<string, string>) => (filePath: string) => {