mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix opening of release details (#5850)
* Make sure release details are updates when opening details Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Relax filtering of resources to prevent crashing when release has installed resources in another namespace Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add Open Closed Principle compliant way to introduce global overrides without modification in getDiForUnitTesting Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Rework helm release details to fix multiple bugs Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant optional chaining Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Simplify code Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
e7e8d1688c
commit
bedc440d42
@ -11123,6 +11123,49 @@ exports[`installing helm chart from new tab given tab for installing chart was n
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Animate slide-right Drawer ReleaseDetails dark right enter"
|
||||
data-testid="helm-release-details-for-default/some-release"
|
||||
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
|
||||
>
|
||||
<div
|
||||
class="drawer-wrapper flex column"
|
||||
>
|
||||
<div
|
||||
class="drawer-title flex align-center"
|
||||
>
|
||||
<div
|
||||
class="drawer-title-text flex gaps align-center"
|
||||
>
|
||||
|
||||
</div>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
data-testid="close-helm-release-detail"
|
||||
tabindex="0"
|
||||
tooltip="Close"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="close"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="drawer-content flex column box grow"
|
||||
>
|
||||
<div
|
||||
class="Spinner singleColor center"
|
||||
data-testid="helm-release-detail-content-spinner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ResizingAnchor horizontal leading"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,604 @@
|
||||
/**
|
||||
* 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 navigateToHelmReleasesInjectable from "../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import type { CallForHelmReleases } from "../../renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import callForHelmReleasesInjectable from "../../renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import namespaceStoreInjectable from "../../renderer/components/+namespaces/store.injectable";
|
||||
import type { NamespaceStore } from "../../renderer/components/+namespaces/store";
|
||||
import type { CallForHelmReleaseConfiguration } from "../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import callForHelmReleaseConfigurationInjectable from "../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "../../renderer/components/+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import callForHelmReleaseUpdateInjectable from "../../renderer/components/+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import { useFakeTime } from "../../common/test-utils/use-fake-time";
|
||||
import type { CallForHelmRelease, DetailedHelmRelease } from "../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release.injectable";
|
||||
import callForHelmReleaseInjectable from "../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release.injectable";
|
||||
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
|
||||
import showCheckedErrorInjectable from "../../renderer/components/notifications/show-checked-error.injectable";
|
||||
import getRandomUpgradeChartTabIdInjectable from "../../renderer/components/dock/upgrade-chart/get-random-upgrade-chart-tab-id.injectable";
|
||||
|
||||
// TODO: Make tooltips free of side effects by making it deterministic
|
||||
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
|
||||
withTooltip: (target: any) => target,
|
||||
}));
|
||||
|
||||
describe("showing details for helm release", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let callForHelmReleasesMock: AsyncFnMock<CallForHelmReleases>;
|
||||
let callForHelmReleaseMock: AsyncFnMock<CallForHelmRelease>;
|
||||
let callForHelmReleaseConfigurationMock: AsyncFnMock<CallForHelmReleaseConfiguration>;
|
||||
let callForHelmReleaseUpdateMock: AsyncFnMock<CallForHelmReleaseUpdate>;
|
||||
let showSuccessNotificationMock: jest.Mock;
|
||||
let showCheckedErrorNotificationMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
useFakeTime("2015-10-21T07:28:00Z");
|
||||
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
builder.setEnvironmentToClusterFrame();
|
||||
|
||||
callForHelmReleasesMock = asyncFn();
|
||||
callForHelmReleaseMock = asyncFn();
|
||||
callForHelmReleaseConfigurationMock = asyncFn();
|
||||
callForHelmReleaseUpdateMock = asyncFn();
|
||||
|
||||
showSuccessNotificationMock = jest.fn();
|
||||
showCheckedErrorNotificationMock = jest.fn();
|
||||
|
||||
builder.beforeApplicationStart(({ rendererDi }) => {
|
||||
rendererDi.override(
|
||||
getRandomUpgradeChartTabIdInjectable,
|
||||
() => () => "some-tab-id",
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
showSuccessNotificationInjectable,
|
||||
() => showSuccessNotificationMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
showCheckedErrorInjectable,
|
||||
() => showCheckedErrorNotificationMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmReleasesInjectable,
|
||||
() => callForHelmReleasesMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmReleaseInjectable,
|
||||
() => callForHelmReleaseMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmReleaseConfigurationInjectable,
|
||||
() => callForHelmReleaseConfigurationMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmReleaseUpdateInjectable,
|
||||
() => callForHelmReleaseUpdateMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
namespaceStoreInjectable,
|
||||
() =>
|
||||
({
|
||||
contextNamespaces: ["some-namespace", "some-other-namespace"],
|
||||
items: [],
|
||||
selectNamespaces: () => {},
|
||||
} as unknown as NamespaceStore),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given application is started", () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
rendered = await builder.render();
|
||||
});
|
||||
|
||||
describe("when navigating to helm releases", () => {
|
||||
beforeEach(() => {
|
||||
const rendererDi = builder.dis.rendererDi;
|
||||
|
||||
const navigateToHelmReleases = rendererDi.inject(
|
||||
navigateToHelmReleasesInjectable,
|
||||
);
|
||||
|
||||
navigateToHelmReleases();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls for releases for each selected namespace", () => {
|
||||
expect(callForHelmReleasesMock.mock.calls).toEqual([
|
||||
["some-namespace"],
|
||||
["some-other-namespace"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows spinner", () => {
|
||||
expect(
|
||||
rendered.getByTestId("helm-releases-spinner"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("when releases resolve but there is none, renders", async () => {
|
||||
await callForHelmReleasesMock.resolve([]);
|
||||
await callForHelmReleasesMock.resolve([]);
|
||||
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when releases resolve", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmReleasesMock.resolveSpecific(
|
||||
([namespace]) => namespace === "some-namespace",
|
||||
[
|
||||
{
|
||||
appVersion: "some-app-version",
|
||||
name: "some-name",
|
||||
namespace: "some-namespace",
|
||||
chart: "some-chart",
|
||||
status: "some-status",
|
||||
updated: "some-updated",
|
||||
revision: "some-revision",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
await callForHelmReleasesMock.resolveSpecific(
|
||||
([namespace]) => namespace === "some-other-namespace",
|
||||
[
|
||||
{
|
||||
appVersion: "some-other-app-version",
|
||||
name: "some-other-name",
|
||||
namespace: "some-other-namespace",
|
||||
chart: "some-other-chart",
|
||||
status: "some-other-status",
|
||||
updated: "some-other-updated",
|
||||
revision: "some-other-revision",
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not show spinner anymore", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("helm-releases-spinner"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when selecting release to see details", () => {
|
||||
beforeEach(() => {
|
||||
const row = rendered.getByTestId(
|
||||
"helm-release-row-for-some-namespace/some-name",
|
||||
);
|
||||
|
||||
fireEvent.click(row);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens the details", () => {
|
||||
expect(
|
||||
rendered.getByTestId("helm-release-details-for-some-namespace/some-name"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls for release", () => {
|
||||
expect(callForHelmReleaseMock).toHaveBeenCalledWith(
|
||||
"some-name",
|
||||
"some-namespace",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows spinner", () => {
|
||||
expect(
|
||||
rendered.getByTestId("helm-release-detail-content-spinner"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when opening details for second release", () => {
|
||||
beforeEach(() => {
|
||||
callForHelmReleaseMock.mockClear();
|
||||
|
||||
const row = rendered.getByTestId(
|
||||
"helm-release-row-for-some-other-namespace/some-other-name",
|
||||
);
|
||||
|
||||
fireEvent.click(row);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls for another release", () => {
|
||||
expect(callForHelmReleaseMock).toHaveBeenCalledWith(
|
||||
"some-other-name",
|
||||
"some-other-namespace",
|
||||
);
|
||||
});
|
||||
|
||||
it("closes details for first release", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("helm-release-details-for-some-namespace/some-name"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens details for second release", () => {
|
||||
expect(
|
||||
rendered.getByTestId("helm-release-details-for-some-other-namespace/some-other-name"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when details is closed", () => {
|
||||
beforeEach(() => {
|
||||
const closeButton = rendered.getByTestId(
|
||||
"close-helm-release-detail",
|
||||
);
|
||||
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("closes the details", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("helm-release-details-for-some-namespace/some-name"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when opening details for same release", () => {
|
||||
beforeEach(() => {
|
||||
callForHelmReleaseMock.mockClear();
|
||||
|
||||
const row = rendered.getByTestId(
|
||||
"helm-release-row-for-some-namespace/some-name",
|
||||
);
|
||||
|
||||
fireEvent.click(row);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not reload", () => {
|
||||
expect(callForHelmReleaseMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("when release resolve with no data, renders", async () => {
|
||||
await callForHelmReleaseMock.resolve(undefined);
|
||||
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when details resolve", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmReleaseMock.resolve(detailedReleaseFake);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls for release configuration", () => {
|
||||
expect(callForHelmReleaseConfigurationMock).toHaveBeenCalledWith(
|
||||
"some-name",
|
||||
"some-namespace",
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when configuration resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmReleaseConfigurationMock.resolve(
|
||||
"some-configuration",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not have tab for upgrading chart yet", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("dock-tab-for-some-tab-id"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when selecting to upgrade chart", () => {
|
||||
beforeEach(() => {
|
||||
const upgradeButton = rendered.getByTestId(
|
||||
"helm-release-upgrade-button",
|
||||
);
|
||||
|
||||
fireEvent.click(upgradeButton);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens tab for upgrading chart", () => {
|
||||
expect(
|
||||
rendered.getByTestId("dock-tab-for-some-tab-id"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes the details", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("helm-release-details-for-some-namespace/some-name"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when changing the configuration", () => {
|
||||
beforeEach(() => {
|
||||
const configuration = rendered.getByTestId(
|
||||
"monaco-editor-for-helm-release-configuration",
|
||||
);
|
||||
|
||||
fireEvent.change(configuration, {
|
||||
target: { value: "some-new-configuration" },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("has the configuration", () => {
|
||||
const input = rendered.getByTestId(
|
||||
"monaco-editor-for-helm-release-configuration",
|
||||
);
|
||||
|
||||
expect(input).toHaveValue("some-new-configuration");
|
||||
});
|
||||
|
||||
it("does not save changes yet", () => {
|
||||
expect(callForHelmReleaseUpdateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when toggling to see only user defined values", () => {
|
||||
beforeEach(() => {
|
||||
callForHelmReleaseConfigurationMock.mockClear();
|
||||
|
||||
const toggle = rendered.getByTestId(
|
||||
"user-supplied-values-only-checkbox",
|
||||
);
|
||||
|
||||
fireEvent.click(toggle);
|
||||
});
|
||||
|
||||
it("calls for only user defined configuration", () => {
|
||||
expect(callForHelmReleaseConfigurationMock).toHaveBeenCalledWith(
|
||||
"some-name",
|
||||
"some-namespace",
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when configuration resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmReleaseConfigurationMock.resolve(
|
||||
"some-other-configuration",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("overrides the user inputted configuration with new configuration", () => {
|
||||
const input = rendered.getByTestId(
|
||||
"monaco-editor-for-helm-release-configuration",
|
||||
);
|
||||
|
||||
expect(input).toHaveValue("some-other-configuration");
|
||||
});
|
||||
|
||||
it("when toggling again, calls for all configuration", () => {
|
||||
callForHelmReleaseConfigurationMock.mockClear();
|
||||
|
||||
const toggle = rendered.getByTestId(
|
||||
"user-supplied-values-only-checkbox",
|
||||
);
|
||||
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(callForHelmReleaseConfigurationMock).toHaveBeenCalledWith(
|
||||
"some-name",
|
||||
"some-namespace",
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when saving", () => {
|
||||
beforeEach(() => {
|
||||
const saveButton = rendered.getByTestId(
|
||||
"helm-release-configuration-save-button",
|
||||
);
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls for update", () => {
|
||||
expect(callForHelmReleaseUpdateMock).toHaveBeenCalledWith(
|
||||
"some-name",
|
||||
"some-namespace",
|
||||
|
||||
{
|
||||
chart: "some-chart",
|
||||
repo: "",
|
||||
values: "some-new-configuration",
|
||||
version: "",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("shows spinner", () => {
|
||||
const saveButton = rendered.getByTestId(
|
||||
"helm-release-configuration-save-button",
|
||||
);
|
||||
|
||||
expect(saveButton).toHaveClass("waiting");
|
||||
});
|
||||
|
||||
describe("when update resolves with success", () => {
|
||||
beforeEach(async () => {
|
||||
callForHelmReleasesMock.mockClear();
|
||||
callForHelmReleaseConfigurationMock.mockClear();
|
||||
|
||||
await callForHelmReleaseUpdateMock.resolve({
|
||||
updateWasSuccessful: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not show spinner anymore", () => {
|
||||
const saveButton = rendered.getByTestId(
|
||||
"helm-release-configuration-save-button",
|
||||
);
|
||||
|
||||
expect(saveButton).not.toHaveClass("waiting");
|
||||
});
|
||||
|
||||
it("reloads the configuration", () => {
|
||||
expect(callForHelmReleaseConfigurationMock).toHaveBeenCalledWith(
|
||||
"some-name",
|
||||
"some-namespace",
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("shows success notification", () => {
|
||||
expect(showSuccessNotificationMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not show error notification", () => {
|
||||
expect(showCheckedErrorNotificationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when update resolves with failure", () => {
|
||||
beforeEach(async () => {
|
||||
callForHelmReleasesMock.mockClear();
|
||||
callForHelmReleaseConfigurationMock.mockClear();
|
||||
|
||||
await callForHelmReleaseUpdateMock.resolve({
|
||||
updateWasSuccessful: false,
|
||||
error: "some-error",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not show spinner anymore", () => {
|
||||
const saveButton = rendered.getByTestId(
|
||||
"helm-release-configuration-save-button",
|
||||
);
|
||||
|
||||
expect(saveButton).not.toHaveClass("waiting");
|
||||
});
|
||||
|
||||
it("does not reload the configuration", () => {
|
||||
expect(callForHelmReleaseConfigurationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not show success notification", () => {
|
||||
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows error notification", () => {
|
||||
expect(showCheckedErrorNotificationMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const detailedReleaseFake: DetailedHelmRelease = {
|
||||
release: {
|
||||
appVersion: "some-app-version",
|
||||
chart: "some-chart",
|
||||
status: "some-status",
|
||||
updated: "some-updated",
|
||||
revision: "some-revision",
|
||||
name: "some-name",
|
||||
namespace: "some-namespace",
|
||||
},
|
||||
|
||||
details: {
|
||||
name: "some-name",
|
||||
namespace: "some-namespace",
|
||||
version: "some-version",
|
||||
config: "some-config",
|
||||
manifest: "some-manifest",
|
||||
|
||||
info: {
|
||||
deleted: "some-deleted",
|
||||
description: "some-description",
|
||||
first_deployed: "some-first-deployed",
|
||||
last_deployed: "some-last-deployed",
|
||||
notes: "some-notes",
|
||||
status: "some-status",
|
||||
},
|
||||
|
||||
resources: [
|
||||
{
|
||||
kind: "some-kind",
|
||||
apiVersion: "some-api-version",
|
||||
metadata: {
|
||||
uid: "some-uid",
|
||||
name: "some-resource",
|
||||
namespace: "some-namespace",
|
||||
creationTimestamp: "2015-10-22T07:28:00Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -3,48 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import { formatDuration } from "../../utils";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import type { JsonApiData } from "../json-api";
|
||||
import { buildURLPositional } from "../../utils/buildUrl";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export interface HelmReleaseDetails {
|
||||
resources: KubeJsonApiData[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
config: string; // release values
|
||||
manifest: string;
|
||||
info: {
|
||||
deleted: string;
|
||||
description: string;
|
||||
first_deployed: string;
|
||||
last_deployed: string;
|
||||
notes: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HelmReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface HelmReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
import type { HelmReleaseDetails } from "../../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||
|
||||
export interface HelmReleaseUpdateDetails {
|
||||
log: string;
|
||||
@ -69,47 +32,7 @@ interface EndpointQuery {
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||
|
||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||
const releases = await apiBase.get<HelmReleaseDto[]>(endpoint({ namespace }));
|
||||
|
||||
return releases.map(toHelmRelease);
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string): Promise<HelmReleaseDetails> {
|
||||
const path = endpoint({ name, namespace });
|
||||
|
||||
return apiBase.get(path);
|
||||
}
|
||||
|
||||
export async function createRelease(payload: HelmReleaseCreatePayload): Promise<HelmReleaseUpdateDetails> {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
|
||||
return apiBase.post(endpoint(), {
|
||||
data: {
|
||||
chart,
|
||||
values,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateRelease(name: string, namespace: string, payload: HelmReleaseUpdatePayload): Promise<HelmReleaseUpdateDetails> {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
|
||||
return apiBase.put(endpoint({ name, namespace }), {
|
||||
data: {
|
||||
chart,
|
||||
values,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
}
|
||||
export const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||
|
||||
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
|
||||
const path = endpoint({ name, namespace });
|
||||
@ -139,7 +62,7 @@ export async function rollbackRelease(name: string, namespace: string, revision:
|
||||
return apiBase.put(path, { data });
|
||||
}
|
||||
|
||||
interface HelmReleaseDto {
|
||||
export interface HelmReleaseDto {
|
||||
appVersion: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
@ -158,70 +81,3 @@ export interface HelmRelease extends HelmReleaseDto, ItemObject {
|
||||
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
|
||||
getRepo: () => Promise<string>;
|
||||
}
|
||||
|
||||
const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({
|
||||
...release,
|
||||
|
||||
getId() {
|
||||
return this.namespace + this.name;
|
||||
},
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
},
|
||||
|
||||
getNs() {
|
||||
return this.namespace;
|
||||
},
|
||||
|
||||
getChart(withVersion = false) {
|
||||
let chart = this.chart;
|
||||
|
||||
if (!withVersion && this.getVersion() != "") {
|
||||
const search = new RegExp(`-${this.getVersion()}`);
|
||||
|
||||
chart = chart.replace(search, "");
|
||||
}
|
||||
|
||||
return chart;
|
||||
},
|
||||
|
||||
getRevision() {
|
||||
return parseInt(this.revision, 10);
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
return capitalize(this.status);
|
||||
},
|
||||
|
||||
getVersion() {
|
||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||
|
||||
return versions?.[0] ?? "";
|
||||
},
|
||||
|
||||
getUpdated(humanize = true, compact = true) {
|
||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||
const updatedDate = new Date(updated).getTime();
|
||||
const diff = Date.now() - updatedDate;
|
||||
|
||||
if (humanize) {
|
||||
return formatDuration(diff, compact);
|
||||
}
|
||||
|
||||
return diff;
|
||||
},
|
||||
|
||||
// Helm does not store from what repository the release is installed,
|
||||
// so we have to try to guess it by searching charts
|
||||
async getRepo() {
|
||||
const chartName = this.getChart();
|
||||
const version = this.getVersion();
|
||||
const versions = await helmChartStore.getVersions(chartName);
|
||||
const chartVersion = versions.find(
|
||||
(chartVersion) => chartVersion.version === version,
|
||||
);
|
||||
|
||||
return chartVersion ? chartVersion.repo : "";
|
||||
},
|
||||
});
|
||||
|
||||
13
src/common/test-utils/get-global-override.ts
Normal file
13
src/common/test-utils/get-global-override.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
|
||||
export const getGlobalOverride = <T extends Injectable<any, any, any>>(
|
||||
injectable: T,
|
||||
overridingInstantiate: T["instantiate"],
|
||||
) => ({
|
||||
injectable,
|
||||
overridingInstantiate,
|
||||
});
|
||||
@ -124,6 +124,16 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
|
||||
di.preventSideEffects();
|
||||
|
||||
if (doGeneralOverrides) {
|
||||
const globalOverrideFilePaths = getGlobalOverridePaths();
|
||||
|
||||
const globalOverrides = globalOverrideFilePaths.map(
|
||||
(filePath) => require(filePath).default,
|
||||
);
|
||||
|
||||
globalOverrides.forEach(globalOverride => {
|
||||
di.override(globalOverride.injectable, globalOverride.overridingInstantiate);
|
||||
});
|
||||
|
||||
di.override(electronInjectable, () => ({}));
|
||||
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
|
||||
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
|
||||
@ -211,6 +221,14 @@ const getInjectableFilePaths = memoize(() => [
|
||||
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||
]);
|
||||
|
||||
const getGlobalOverridePaths = memoize(() =>
|
||||
glob.sync(
|
||||
"../{common,extensions,main}/**/*.global-override-for-injectable.{ts,tsx}",
|
||||
|
||||
{ cwd: __dirname },
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Reorganize code in Runnables to get rid of requirement for override
|
||||
const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
|
||||
[
|
||||
|
||||
@ -191,7 +191,6 @@ async function getResources(name: string, namespace: string, kubeconfigPath: str
|
||||
];
|
||||
const kubectlArgs = [
|
||||
"get",
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
"-f", "-",
|
||||
"--output", "json",
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import callForHelmReleasesInjectable from "./call-for-helm-releases.injectable";
|
||||
import { getGlobalOverride } from "../../../../common/test-utils/get-global-override";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleasesInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm releases without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 { HelmReleaseDto } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
import { apiBase } from "../../../../common/k8s-api";
|
||||
import { endpoint } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
export type CallForHelmReleases = (
|
||||
namespace?: string
|
||||
) => Promise<HelmReleaseDto[]>;
|
||||
|
||||
const callForHelmReleasesInjectable = getInjectable({
|
||||
id: "call-for-helm-releases",
|
||||
|
||||
instantiate: (): CallForHelmReleases => async (namespace) =>
|
||||
await apiBase.get<HelmReleaseDto[]>(endpoint({ namespace })),
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleasesInjectable;
|
||||
@ -2,9 +2,20 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import yaml from "js-yaml";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { createRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { endpoint } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { apiBase } from "../../../../common/k8s-api";
|
||||
|
||||
interface HelmReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export type CallForCreateHelmRelease = (
|
||||
payload: HelmReleaseCreatePayload
|
||||
@ -12,7 +23,21 @@ export type CallForCreateHelmRelease = (
|
||||
|
||||
const callForCreateHelmReleaseInjectable = getInjectable({
|
||||
id: "call-for-create-helm-release",
|
||||
instantiate: (): CallForCreateHelmRelease => createRelease,
|
||||
|
||||
instantiate: (): CallForCreateHelmRelease => (payload) => {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
|
||||
return apiBase.post(endpoint(), {
|
||||
data: {
|
||||
chart,
|
||||
values,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@ -4,19 +4,18 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import type {
|
||||
HelmReleaseCreatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
import type { CallForCreateHelmRelease } from "./call-for-create-helm-release.injectable";
|
||||
import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
|
||||
|
||||
const createReleaseInjectable = getInjectable({
|
||||
id: "create-release",
|
||||
|
||||
instantiate: (di) => {
|
||||
instantiate: (di): CallForCreateHelmRelease => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
|
||||
|
||||
return async (payload: HelmReleaseCreatePayload) => {
|
||||
return async (payload) => {
|
||||
const release = await callForCreateRelease(payload);
|
||||
|
||||
releases.invalidate();
|
||||
|
||||
@ -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 { computed } from "mobx";
|
||||
import { clusterFrameChildComponentInjectionToken } from "../../../frames/cluster-frame/cluster-frame-child-component-injection-token";
|
||||
import { ReleaseDetails } from "./release-details";
|
||||
import targetHelmReleaseInjectable from "./target-helm-release.injectable";
|
||||
|
||||
const releaseDetailsClusterFrameChildComponentInjectable = getInjectable({
|
||||
id: "release-details-cluster-frame-child-component",
|
||||
|
||||
instantiate: (di) => {
|
||||
const targetRelease = di.inject(targetHelmReleaseInjectable);
|
||||
|
||||
return {
|
||||
id: "release-details",
|
||||
Component: ReleaseDetails,
|
||||
shouldRender: computed(() => !!targetRelease.get()),
|
||||
};
|
||||
},
|
||||
injectionToken: clusterFrameChildComponentInjectionToken,
|
||||
});
|
||||
|
||||
export default releaseDetailsClusterFrameChildComponentInjectable;
|
||||
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./release-details.scss";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { Drawer, DrawerItem, DrawerTitle } from "../../drawer";
|
||||
import { cssNames, stopPropagation } from "../../../utils";
|
||||
import { observer } from "mobx-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { ConfigurationInput, MinimalResourceGroup, OnlyUserSuppliedValuesAreShownToggle, ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
|
||||
import releaseDetailsModelInjectable from "./release-details-model/release-details-model.injectable";
|
||||
import { Button } from "../../button";
|
||||
import { kebabCase } from "lodash/fp";
|
||||
import { Badge } from "../../badge";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||
import { ReactiveDuration } from "../../duration/reactive-duration";
|
||||
import { HelmReleaseMenu } from "../release-menu";
|
||||
import { Checkbox } from "../../checkbox";
|
||||
import { MonacoEditor } from "../../monaco-editor";
|
||||
import { Spinner } from "../../spinner";
|
||||
import type { TargetHelmRelease } from "./target-helm-release.injectable";
|
||||
|
||||
interface ReleaseDetailsContentProps {
|
||||
targetRelease: TargetHelmRelease;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
model: ReleaseDetailsModel;
|
||||
}
|
||||
|
||||
const NonInjectedReleaseDetailsContent = observer(({ model }: Dependencies & ReleaseDetailsContentProps) => {
|
||||
const isLoading = model.isLoading.get();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={cssNames("ReleaseDetails", model.activeTheme)}
|
||||
usePortal={true}
|
||||
open={true}
|
||||
title={isLoading ? "" : model.release.getName()}
|
||||
onClose={model.close}
|
||||
testIdForClose="close-helm-release-detail"
|
||||
toolbar={
|
||||
!isLoading && (
|
||||
<HelmReleaseMenu
|
||||
release={model.release}
|
||||
toolbar
|
||||
hideDetails={model.close}
|
||||
/>
|
||||
)
|
||||
}
|
||||
data-testid={`helm-release-details-for-${model.id}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner center data-testid="helm-release-detail-content-spinner" />
|
||||
) : (
|
||||
<div>
|
||||
<DrawerItem name="Chart" className="chart">
|
||||
<div className="flex gaps align-center">
|
||||
<span>{model.release.chart}</span>
|
||||
|
||||
<Button
|
||||
primary
|
||||
label="Upgrade"
|
||||
className="box right upgrade"
|
||||
onClick={model.startUpgradeProcess}
|
||||
data-testid="helm-release-upgrade-button"
|
||||
/>
|
||||
</div>
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Updated">
|
||||
{model.release.getUpdated()}
|
||||
{` ago (${model.release.updated})`}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Namespace">{model.release.getNs()}</DrawerItem>
|
||||
|
||||
<DrawerItem name="Version" onClick={stopPropagation}>
|
||||
<div className="version flex gaps align-center">
|
||||
<span>{model.release.getVersion()}</span>
|
||||
</div>
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem
|
||||
name="Status"
|
||||
className="status"
|
||||
labelsOnly>
|
||||
<Badge
|
||||
label={model.release.getStatus()}
|
||||
className={kebabCase(model.release.getStatus())}
|
||||
/>
|
||||
</DrawerItem>
|
||||
|
||||
<ReleaseValues
|
||||
configuration={model.configuration}
|
||||
onlyUserSuppliedValuesAreShown={
|
||||
model.onlyUserSuppliedValuesAreShown
|
||||
}
|
||||
/>
|
||||
|
||||
<DrawerTitle>Notes</DrawerTitle>
|
||||
|
||||
{model.notes && <div className="notes">{model.notes}</div>}
|
||||
|
||||
<DrawerTitle>Resources</DrawerTitle>
|
||||
|
||||
{model.groupedResources.length > 0 && (
|
||||
<div className="resources">
|
||||
{model.groupedResources.map((group) => (
|
||||
<ResourceGroup key={group.kind} group={group} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReleaseDetailsContent = withInjectables<Dependencies, ReleaseDetailsContentProps>(NonInjectedReleaseDetailsContent, {
|
||||
getProps: (di, props) => ({
|
||||
model: di.inject(releaseDetailsModelInjectable, props.targetRelease),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
|
||||
const ResourceGroup = ({
|
||||
group: { kind, isNamespaced, resources },
|
||||
}: {
|
||||
group: MinimalResourceGroup;
|
||||
}) => (
|
||||
<>
|
||||
<SubTitle title={kind} />
|
||||
|
||||
<Table scrollable={false}>
|
||||
<TableHead sticky={false}>
|
||||
<TableCell className="name">Name</TableCell>
|
||||
|
||||
{isNamespaced && <TableCell className="namespace">Namespace</TableCell>}
|
||||
|
||||
<TableCell className="age">Age</TableCell>
|
||||
</TableHead>
|
||||
|
||||
{resources.map(
|
||||
({ creationTimestamp, detailsUrl, name, namespace, uid }) => (
|
||||
<TableRow key={uid}>
|
||||
<TableCell className="name">
|
||||
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
||||
</TableCell>
|
||||
|
||||
{isNamespaced && (
|
||||
<TableCell className="namespace">{namespace}</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell className="age">
|
||||
<ReactiveDuration timestamp={creationTimestamp} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
interface ReleaseValuesProps {
|
||||
configuration: ConfigurationInput;
|
||||
onlyUserSuppliedValuesAreShown: OnlyUserSuppliedValuesAreShownToggle;
|
||||
}
|
||||
|
||||
const ReleaseValues = observer(({ configuration, onlyUserSuppliedValuesAreShown }: ReleaseValuesProps) => {
|
||||
const configurationIsLoading = configuration.isLoading.get();
|
||||
|
||||
return (
|
||||
<div className="values">
|
||||
<DrawerTitle>Values</DrawerTitle>
|
||||
|
||||
<div className="flex column gaps">
|
||||
<Checkbox
|
||||
label="User-supplied values only"
|
||||
value={onlyUserSuppliedValuesAreShown.value.get()}
|
||||
onChange={onlyUserSuppliedValuesAreShown.toggle}
|
||||
disabled={configurationIsLoading}
|
||||
data-testid="user-supplied-values-only-checkbox"
|
||||
/>
|
||||
|
||||
<MonacoEditor
|
||||
id="helm-release-configuration"
|
||||
style={{ minHeight: 300 }}
|
||||
value={configuration.nonSavedValue.get()}
|
||||
onChange={configuration.onChange}
|
||||
/>
|
||||
|
||||
<Button
|
||||
primary
|
||||
label="Save"
|
||||
waiting={configuration.isSaving.get()}
|
||||
disabled={configurationIsLoading}
|
||||
onClick={configuration.save}
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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 callForHelmReleaseConfigurationInjectable from "./call-for-helm-release-configuration.injectable";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleaseConfigurationInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm release configuration without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { apiBase } from "../../../../../../common/k8s-api";
|
||||
import { endpoint } from "../../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
export type CallForHelmReleaseConfiguration = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
all: boolean
|
||||
) => Promise<string>;
|
||||
|
||||
const callForHelmReleaseConfigurationInjectable = getInjectable({
|
||||
id: "call-for-helm-release-configuration",
|
||||
|
||||
instantiate:
|
||||
(): CallForHelmReleaseConfiguration => async (name, namespace, all: boolean) => {
|
||||
const route = "values";
|
||||
const path = endpoint({ name, namespace, route }, { all });
|
||||
|
||||
return apiBase.get<string>(path);
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleaseConfigurationInjectable;
|
||||
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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 callForHelmReleaseDetailsInjectable from "./call-for-helm-release-details.injectable";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleaseDetailsInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm release details without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiBase } from "../../../../../../../common/k8s-api";
|
||||
import { endpoint } from "../../../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { KubeJsonApiData } from "../../../../../../../common/k8s-api/kube-json-api";
|
||||
|
||||
export interface HelmReleaseDetails {
|
||||
resources: KubeJsonApiData[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
config: string; // release values
|
||||
manifest: string;
|
||||
|
||||
info: {
|
||||
deleted: string;
|
||||
description: string;
|
||||
first_deployed: string;
|
||||
last_deployed: string;
|
||||
notes: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CallForHelmReleaseDetails = (
|
||||
name: string,
|
||||
namespace: string
|
||||
) => Promise<HelmReleaseDetails>;
|
||||
|
||||
const callForHelmReleaseDetailsInjectable = getInjectable({
|
||||
id: "call-for-helm-release-details",
|
||||
|
||||
instantiate: (): CallForHelmReleaseDetails => async (name, namespace) => {
|
||||
const path = endpoint({ name, namespace });
|
||||
|
||||
return apiBase.get<HelmReleaseDetails>(path);
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleaseDetailsInjectable;
|
||||
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 { HelmReleaseDto } from "../../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import callForHelmReleasesInjectable from "../../../call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import type { HelmReleaseDetails } from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||
import callForHelmReleaseDetailsInjectable from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||
|
||||
export interface DetailedHelmRelease {
|
||||
release: HelmReleaseDto;
|
||||
details: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export type CallForHelmRelease = (
|
||||
name: string,
|
||||
namespace: string
|
||||
) => Promise<DetailedHelmRelease | undefined>;
|
||||
|
||||
const callForHelmReleaseInjectable = getInjectable({
|
||||
id: "call-for-helm-release",
|
||||
|
||||
instantiate: (di): CallForHelmRelease => {
|
||||
const callForHelmReleases = di.inject(callForHelmReleasesInjectable);
|
||||
const callForHelmReleaseDetails = di.inject(callForHelmReleaseDetailsInjectable);
|
||||
|
||||
return async (name, namespace) => {
|
||||
const [releases, details] = await Promise.all([
|
||||
callForHelmReleases(namespace),
|
||||
callForHelmReleaseDetails(name, namespace),
|
||||
]);
|
||||
|
||||
const release = releases.find(
|
||||
(rel) => rel.name === name && rel.namespace === namespace,
|
||||
);
|
||||
|
||||
if (!release) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { release, details };
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default callForHelmReleaseInjectable;
|
||||
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import apiManagerInjectable from "../../../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import getDetailsUrlInjectable from "../../../kube-detail-params/get-details-url.injectable";
|
||||
|
||||
export type GetResourceDetailsUrl = (
|
||||
kind: string,
|
||||
apiVersion: string,
|
||||
namespace: string | undefined,
|
||||
name: string
|
||||
) => string;
|
||||
|
||||
const getResourceDetailsUrlInjectable = getInjectable({
|
||||
id: "get-resource-details-url",
|
||||
|
||||
instantiate: (di): GetResourceDetailsUrl => {
|
||||
const apiManager = di.inject(apiManagerInjectable);
|
||||
const getDetailsUrl = di.inject(getDetailsUrlInjectable);
|
||||
|
||||
const getKubeApi = (kind: string, apiVersion: string) =>
|
||||
apiManager.getApi(
|
||||
(api) => api.kind === kind && api.apiVersionWithGroup == apiVersion,
|
||||
);
|
||||
|
||||
return (kind, apiVersion, namespace, name) => {
|
||||
const kubeApi = getKubeApi(kind, apiVersion);
|
||||
|
||||
if (!kubeApi) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const resourceUrl = kubeApi.getUrl({
|
||||
name,
|
||||
namespace,
|
||||
});
|
||||
|
||||
return getDetailsUrl(resourceUrl);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default getResourceDetailsUrlInjectable;
|
||||
@ -0,0 +1,308 @@
|
||||
/**
|
||||
* 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";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import { runInAction, action, observable, computed } from "mobx";
|
||||
import type { TargetHelmRelease } from "../target-helm-release.injectable";
|
||||
import type { CallForHelmRelease, DetailedHelmRelease } from "./call-for-helm-release/call-for-helm-release.injectable";
|
||||
import callForHelmReleaseInjectable from "./call-for-helm-release/call-for-helm-release.injectable";
|
||||
import type { ThemeStore } from "../../../../themes/store";
|
||||
import themeStoreInjectable from "../../../../themes/store.injectable";
|
||||
import type { CallForHelmReleaseConfiguration } from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import callForHelmReleaseConfigurationInjectable from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import { toHelmRelease } from "../../releases.injectable";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { groupBy, map } from "lodash/fp";
|
||||
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
|
||||
import type { GetResourceDetailsUrl } from "./get-resource-details-url.injectable";
|
||||
import getResourceDetailsUrlInjectable from "./get-resource-details-url.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "../../update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import updateReleaseInjectable from "../../update-release/update-release.injectable";
|
||||
import type { ShowCheckedErrorNotification } from "../../../notifications/show-checked-error.injectable";
|
||||
import showCheckedErrorNotificationInjectable from "../../../notifications/show-checked-error.injectable";
|
||||
import type { ShowNotification } from "../../../notifications";
|
||||
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
||||
import React from "react";
|
||||
import createUpgradeChartTabInjectable from "../../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
|
||||
import type { HelmRelease } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { NavigateToHelmReleases } from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import assert from "assert";
|
||||
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
|
||||
|
||||
const releaseDetailsModelInjectable = getInjectable({
|
||||
id: "release-details-model",
|
||||
|
||||
instantiate: (di, targetRelease: TargetHelmRelease) => {
|
||||
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
|
||||
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
|
||||
const themeStore = di.inject(themeStoreInjectable);
|
||||
const getResourceDetailsUrl = di.inject(getResourceDetailsUrlInjectable);
|
||||
const updateRelease = di.inject(updateReleaseInjectable);
|
||||
const showCheckedErrorNotification = di.inject(showCheckedErrorNotificationInjectable);
|
||||
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
|
||||
const createUpgradeChartTab = di.inject(createUpgradeChartTabInjectable);
|
||||
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
|
||||
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
|
||||
|
||||
const model = new ReleaseDetailsModel({
|
||||
callForHelmRelease,
|
||||
targetRelease,
|
||||
themeStore,
|
||||
callForHelmReleaseConfiguration,
|
||||
getResourceDetailsUrl,
|
||||
updateRelease,
|
||||
showCheckedErrorNotification,
|
||||
showSuccessNotification,
|
||||
createUpgradeChartTab,
|
||||
navigateToHelmReleases,
|
||||
});
|
||||
|
||||
const load = withOrphanPromise(model.load);
|
||||
|
||||
// TODO: Reorganize Drawer to allow setting of header-bar in children to make "getPlaceholder" from injectable usable.
|
||||
load();
|
||||
|
||||
return model;
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, release: TargetHelmRelease) =>
|
||||
`${release.namespace}/${release.name}`,
|
||||
}),
|
||||
});
|
||||
|
||||
export default releaseDetailsModelInjectable;
|
||||
|
||||
export interface OnlyUserSuppliedValuesAreShownToggle {
|
||||
value: IObservableValue<boolean>;
|
||||
toggle: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ConfigurationInput {
|
||||
nonSavedValue: IObservableValue<string>;
|
||||
isLoading: IObservableValue<boolean>;
|
||||
isSaving: IObservableValue<boolean>;
|
||||
onChange: (value: string) => void;
|
||||
save: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
callForHelmRelease: CallForHelmRelease;
|
||||
targetRelease: TargetHelmRelease;
|
||||
themeStore: ThemeStore;
|
||||
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
|
||||
getResourceDetailsUrl: GetResourceDetailsUrl;
|
||||
updateRelease: CallForHelmReleaseUpdate;
|
||||
showCheckedErrorNotification: ShowCheckedErrorNotification;
|
||||
showSuccessNotification: ShowNotification;
|
||||
createUpgradeChartTab: (release: HelmRelease) => string;
|
||||
navigateToHelmReleases: NavigateToHelmReleases;
|
||||
}
|
||||
|
||||
export class ReleaseDetailsModel {
|
||||
id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
|
||||
|
||||
constructor(private dependencies: Dependencies) {}
|
||||
|
||||
private detailedRelease = observable.box<DetailedHelmRelease | undefined>();
|
||||
|
||||
readonly isLoading = observable.box(false);
|
||||
|
||||
readonly configuration: ConfigurationInput = {
|
||||
nonSavedValue: observable.box(""),
|
||||
isLoading: observable.box(false),
|
||||
isSaving: observable.box(false),
|
||||
|
||||
onChange: action((value: string) => {
|
||||
this.configuration.nonSavedValue.set(value);
|
||||
}),
|
||||
|
||||
save: async () => {
|
||||
runInAction(() => {
|
||||
this.configuration.isSaving.set(true);
|
||||
});
|
||||
|
||||
const name = this.release.getName();
|
||||
const namespace = this.release.getNs();
|
||||
|
||||
const data = {
|
||||
chart: this.release.getChart(),
|
||||
repo: await this.release.getRepo(),
|
||||
version: this.release.getVersion(),
|
||||
values: this.configuration.nonSavedValue.get(),
|
||||
};
|
||||
|
||||
const result = await this.dependencies.updateRelease(name, namespace, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.configuration.isSaving.set(false);
|
||||
});
|
||||
|
||||
if (!result.updateWasSuccessful) {
|
||||
this.dependencies.showCheckedErrorNotification(
|
||||
result.error,
|
||||
"Unknown error occured while updating release",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.dependencies.showSuccessNotification(
|
||||
<p>
|
||||
Release
|
||||
{" "}
|
||||
<b>{name}</b>
|
||||
{" successfully updated!"}
|
||||
</p>,
|
||||
);
|
||||
|
||||
await this.loadConfiguration();
|
||||
},
|
||||
};
|
||||
|
||||
readonly onlyUserSuppliedValuesAreShown: OnlyUserSuppliedValuesAreShownToggle = {
|
||||
value: observable.box(false),
|
||||
|
||||
toggle: action(async () => {
|
||||
const value = this.onlyUserSuppliedValuesAreShown.value;
|
||||
|
||||
value.set(!value.get());
|
||||
|
||||
await this.loadConfiguration();
|
||||
}),
|
||||
};
|
||||
|
||||
load = async () => {
|
||||
runInAction(() => {
|
||||
this.isLoading.set(true);
|
||||
});
|
||||
|
||||
const { name, namespace } = this.dependencies.targetRelease;
|
||||
|
||||
const detailedRelease = await this.dependencies.callForHelmRelease(
|
||||
name,
|
||||
namespace,
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.detailedRelease.set(detailedRelease);
|
||||
});
|
||||
|
||||
await this.loadConfiguration();
|
||||
|
||||
runInAction(() => {
|
||||
this.isLoading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
private loadConfiguration = async () => {
|
||||
runInAction(() => {
|
||||
this.configuration.isLoading.set(true);
|
||||
});
|
||||
|
||||
const { name, namespace } = this.release;
|
||||
|
||||
const configuration =
|
||||
await this.dependencies.callForHelmReleaseConfiguration(
|
||||
name,
|
||||
namespace,
|
||||
!this.onlyUserSuppliedValuesAreShown.value.get(),
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.configuration.isLoading.set(false);
|
||||
this.configuration.nonSavedValue.set(configuration);
|
||||
});
|
||||
};
|
||||
|
||||
@computed get release() {
|
||||
const detailedRelease = this.detailedRelease.get();
|
||||
|
||||
assert(detailedRelease, "Tried to access release before load");
|
||||
|
||||
return toHelmRelease(detailedRelease.release);
|
||||
}
|
||||
|
||||
@computed private get details() {
|
||||
const detailedRelease = this.detailedRelease.get();
|
||||
|
||||
assert(detailedRelease, "Tried to access details before load");
|
||||
|
||||
return detailedRelease.details;
|
||||
}
|
||||
|
||||
@computed get notes() {
|
||||
return this.details.info.notes;
|
||||
}
|
||||
|
||||
@computed get groupedResources(): MinimalResourceGroup[] {
|
||||
return pipeline(
|
||||
this.details.resources,
|
||||
groupBy((resource) => resource.kind),
|
||||
(grouped) => Object.entries(grouped),
|
||||
|
||||
map(([kind, resources]) => ({
|
||||
kind,
|
||||
|
||||
resources: resources.map(
|
||||
toMinimalResourceFor(this.dependencies.getResourceDetailsUrl, kind),
|
||||
),
|
||||
|
||||
isNamespaced: resources.some(
|
||||
(resource) => !!resource.metadata.namespace,
|
||||
),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@computed get activeTheme() {
|
||||
return this.dependencies.themeStore.activeTheme.type;
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.dependencies.navigateToHelmReleases();
|
||||
};
|
||||
|
||||
startUpgradeProcess = () => {
|
||||
this.dependencies.createUpgradeChartTab(this.release);
|
||||
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
export interface MinimalResourceGroup {
|
||||
kind: string;
|
||||
isNamespaced: boolean;
|
||||
resources: MinimalResource[];
|
||||
}
|
||||
|
||||
export interface MinimalResource {
|
||||
uid: string | undefined;
|
||||
name: string;
|
||||
namespace: string | undefined;
|
||||
detailsUrl: string | undefined;
|
||||
creationTimestamp: string | undefined;
|
||||
}
|
||||
|
||||
const toMinimalResourceFor =
|
||||
(getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) =>
|
||||
(resource: KubeJsonApiData): MinimalResource => {
|
||||
const { creationTimestamp, name, namespace, uid } = resource.metadata;
|
||||
|
||||
return {
|
||||
uid,
|
||||
name,
|
||||
namespace,
|
||||
creationTimestamp,
|
||||
|
||||
detailsUrl: getResourceDetailsUrl(
|
||||
kind,
|
||||
resource.apiVersion,
|
||||
namespace,
|
||||
name,
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import releaseInjectable from "./release.injectable";
|
||||
import { waitUntilDefined } from "../../../utils";
|
||||
|
||||
const releaseDetailsInjectable = getInjectable({
|
||||
id: "release-details",
|
||||
|
||||
instantiate: (di) => {
|
||||
const releaseComputed = di.inject(releaseInjectable);
|
||||
|
||||
return asyncComputed(async () => {
|
||||
const release = await waitUntilDefined(releaseComputed);
|
||||
|
||||
return getRelease(release.name, release.namespace);
|
||||
});},
|
||||
});
|
||||
|
||||
export default releaseDetailsInjectable;
|
||||
@ -5,296 +5,35 @@
|
||||
|
||||
import "./release-details.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, makeObservable, observable } from "mobx";
|
||||
import { Link } from "react-router-dom";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import type { HelmRelease, HelmReleaseDetails, HelmReleaseUpdateDetails, HelmReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { HelmReleaseMenu } from "../release-menu";
|
||||
import { Drawer, DrawerItem, DrawerTitle } from "../../drawer";
|
||||
import { Badge } from "../../badge";
|
||||
import { cssNames, stopPropagation } from "../../../utils";
|
||||
import { Observer, observer } from "mobx-react";
|
||||
import { Spinner } from "../../spinner";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||
import { Button } from "../../button";
|
||||
import { Notifications } from "../../notifications";
|
||||
import type { ThemeStore } from "../../../themes/store";
|
||||
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Checkbox } from "../../checkbox";
|
||||
import { MonacoEditor } from "../../monaco-editor";
|
||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createUpgradeChartTabInjectable from "../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
|
||||
import updateReleaseInjectable from "../update-release/update-release.injectable";
|
||||
import releaseInjectable from "./release.injectable";
|
||||
import releaseDetailsInjectable from "./release-details.injectable";
|
||||
import releaseValuesInjectable from "./release-values.injectable";
|
||||
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
|
||||
import { KubeObjectAge } from "../../kube-object/age";
|
||||
import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
|
||||
import { entries } from "../../../../common/utils/objects";
|
||||
import themeStoreInjectable from "../../../themes/store.injectable";
|
||||
import type { GetDetailsUrl } from "../../kube-detail-params/get-details-url.injectable";
|
||||
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import getDetailsUrlInjectable from "../../kube-detail-params/get-details-url.injectable";
|
||||
import React from "react";
|
||||
|
||||
export interface ReleaseDetailsProps {
|
||||
hideDetails(): void;
|
||||
}
|
||||
import { observer } from "mobx-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { ReleaseDetailsContent } from "./release-details-content";
|
||||
import type { TargetHelmRelease } from "./target-helm-release.injectable";
|
||||
import targetHelmReleaseInjectable from "./target-helm-release.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
release: IComputedValue<HelmRelease | null | undefined>;
|
||||
releaseDetails: IAsyncComputed<HelmReleaseDetails>;
|
||||
releaseValues: IAsyncComputed<string>;
|
||||
updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>;
|
||||
createUpgradeChartTab: (release: HelmRelease) => void;
|
||||
userSuppliedValuesAreShown: { toggle: () => void; value: boolean };
|
||||
themeStore: ThemeStore;
|
||||
apiManager: ApiManager;
|
||||
getDetailsUrl: GetDetailsUrl;
|
||||
targetRelease: IComputedValue<
|
||||
TargetHelmRelease | undefined
|
||||
>;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependencies> {
|
||||
@observable saving = false;
|
||||
const NonInjectedReleaseDetails = observer(
|
||||
({ targetRelease }: Dependencies) => {
|
||||
const release = targetRelease.get();
|
||||
|
||||
private nonSavedValues = "";
|
||||
return release ? <ReleaseDetailsContent targetRelease={release} /> : null;
|
||||
},
|
||||
);
|
||||
|
||||
constructor(props: ReleaseDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get details() {
|
||||
return this.props.releaseDetails.value.get();
|
||||
}
|
||||
|
||||
updateValues = async (release: HelmRelease) => {
|
||||
const name = release.getName();
|
||||
const namespace = release.getNs();
|
||||
const data = {
|
||||
chart: release.getChart(),
|
||||
repo: await release.getRepo(),
|
||||
version: release.getVersion(),
|
||||
values: this.nonSavedValues,
|
||||
};
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.props.updateRelease(name, namespace, data);
|
||||
Notifications.ok(
|
||||
<p>
|
||||
Release
|
||||
<b>{name}</b>
|
||||
{" successfully updated!"}
|
||||
</p>,
|
||||
);
|
||||
|
||||
this.props.releaseValues.invalidate();
|
||||
} catch (err) {
|
||||
Notifications.checkedError(err, "Unknown error occured while updating release");
|
||||
}
|
||||
this.saving = false;
|
||||
};
|
||||
|
||||
upgradeVersion = (release: HelmRelease) => {
|
||||
const { hideDetails, createUpgradeChartTab } = this.props;
|
||||
|
||||
createUpgradeChartTab(release);
|
||||
hideDetails();
|
||||
};
|
||||
|
||||
renderValues(release: HelmRelease) {
|
||||
return (
|
||||
<Observer>
|
||||
{() => {
|
||||
const { saving } = this;
|
||||
|
||||
const releaseValuesArePending =
|
||||
this.props.releaseValues.pending.get();
|
||||
|
||||
this.nonSavedValues = this.props.releaseValues.value.get();
|
||||
|
||||
return (
|
||||
<div className="values">
|
||||
<DrawerTitle>Values</DrawerTitle>
|
||||
<div className="flex column gaps">
|
||||
<Checkbox
|
||||
label="User-supplied values only"
|
||||
value={this.props.userSuppliedValuesAreShown.value}
|
||||
onChange={this.props.userSuppliedValuesAreShown.toggle}
|
||||
disabled={releaseValuesArePending}
|
||||
/>
|
||||
<MonacoEditor
|
||||
style={{ minHeight: 300 }}
|
||||
value={this.nonSavedValues}
|
||||
onChange={(text) => (this.nonSavedValues = text)}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
label="Save"
|
||||
waiting={saving}
|
||||
disabled={releaseValuesArePending}
|
||||
onClick={() => this.updateValues(release)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Observer>
|
||||
);
|
||||
}
|
||||
|
||||
renderNotes() {
|
||||
if (!this.details.info?.notes) return null;
|
||||
const { notes } = this.details.info;
|
||||
|
||||
return (
|
||||
<div className="notes">
|
||||
{notes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderResources(resources: KubeJsonApiData[]) {
|
||||
const { apiManager, getDetailsUrl } = this.props;
|
||||
|
||||
return (
|
||||
<div className="resources">
|
||||
{
|
||||
entries(groupBy(resources, item => item.kind))
|
||||
.map(([kind, items]) => (
|
||||
<React.Fragment key={kind}>
|
||||
<SubTitle title={kind} />
|
||||
<Table scrollable={false}>
|
||||
<TableHead sticky={false}>
|
||||
<TableCell className="name">Name</TableCell>
|
||||
{items[0].metadata.namespace && <TableCell className="namespace">Namespace</TableCell>}
|
||||
<TableCell className="age">Age</TableCell>
|
||||
</TableHead>
|
||||
{items.map(item => {
|
||||
const { name, namespace, uid } = item.metadata;
|
||||
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion);
|
||||
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
|
||||
|
||||
return (
|
||||
<TableRow key={uid}>
|
||||
<TableCell className="name">
|
||||
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
||||
</TableCell>
|
||||
{namespace && (
|
||||
<TableCell className="namespace">
|
||||
{namespace}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="age">
|
||||
<KubeObjectAge key="age" object={item} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent(release: HelmRelease) {
|
||||
if (!this.details) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
const { resources } = this.details;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DrawerItem name="Chart" className="chart">
|
||||
<div className="flex gaps align-center">
|
||||
<span>{release.getChart()}</span>
|
||||
<Button
|
||||
primary
|
||||
label="Upgrade"
|
||||
className="box right upgrade"
|
||||
onClick={() => this.upgradeVersion(release)}
|
||||
/>
|
||||
</div>
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Updated">
|
||||
{release.getUpdated()}
|
||||
{` ago (${release.updated})`}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Namespace">
|
||||
{release.getNs()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Version" onClick={stopPropagation}>
|
||||
<div className="version flex gaps align-center">
|
||||
<span>
|
||||
{release.getVersion()}
|
||||
</span>
|
||||
</div>
|
||||
</DrawerItem>
|
||||
<DrawerItem
|
||||
name="Status"
|
||||
className="status"
|
||||
labelsOnly
|
||||
>
|
||||
<Badge
|
||||
label={release.getStatus()}
|
||||
className={kebabCase(release.getStatus())}
|
||||
/>
|
||||
</DrawerItem>
|
||||
{this.renderValues(release)}
|
||||
<DrawerTitle>Notes</DrawerTitle>
|
||||
{this.renderNotes()}
|
||||
<DrawerTitle>Resources</DrawerTitle>
|
||||
{resources && this.renderResources(resources)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hideDetails, themeStore } = this.props;
|
||||
const release = this.props.release.get();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
|
||||
usePortal={true}
|
||||
open={Boolean(release)}
|
||||
title={release ? `Release: ${release.getName()}` : ""}
|
||||
onClose={hideDetails}
|
||||
toolbar={release && (
|
||||
<HelmReleaseMenu
|
||||
release={release}
|
||||
toolbar
|
||||
hideDetails={hideDetails}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{release && this.renderContent(release)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ReleaseDetails = withInjectables<Dependencies, ReleaseDetailsProps>(NonInjectedReleaseDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
release: di.inject(releaseInjectable),
|
||||
releaseDetails: di.inject(releaseDetailsInjectable),
|
||||
releaseValues: di.inject(releaseValuesInjectable),
|
||||
userSuppliedValuesAreShown: di.inject(userSuppliedValuesAreShownInjectable),
|
||||
updateRelease: di.inject(updateReleaseInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
getDetailsUrl: di.inject(getDetailsUrlInjectable),
|
||||
}),
|
||||
});
|
||||
export const ReleaseDetails = withInjectables<Dependencies>(
|
||||
NonInjectedReleaseDetails,
|
||||
{
|
||||
getProps: (di) => ({
|
||||
targetRelease: di.inject(targetHelmReleaseInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import releaseInjectable from "./release.injectable";
|
||||
import { Notifications } from "../../notifications";
|
||||
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
|
||||
|
||||
const releaseValuesInjectable = getInjectable({
|
||||
id: "release-values",
|
||||
|
||||
instantiate: (di) =>
|
||||
asyncComputed(async () => {
|
||||
const release = di.inject(releaseInjectable).get();
|
||||
|
||||
// TODO: Figure out way to get rid of defensive code
|
||||
if (!release) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const userSuppliedValuesAreShown = di.inject(userSuppliedValuesAreShownInjectable).value;
|
||||
|
||||
try {
|
||||
return await getReleaseValues(release.getName(), release.getNs(), !userSuppliedValuesAreShown) ?? "";
|
||||
} catch (error) {
|
||||
Notifications.error(`Failed to load values for ${release.getName()}: ${error}`);
|
||||
|
||||
return "";
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
export default releaseValuesInjectable;
|
||||
@ -3,29 +3,24 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { matches } from "lodash/fp";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
import { computed } from "mobx";
|
||||
import helmReleasesRouteParametersInjectable from "../helm-releases-route-parameters.injectable";
|
||||
|
||||
const releaseInjectable = getInjectable({
|
||||
id: "release",
|
||||
export interface TargetHelmRelease { name: string; namespace: string }
|
||||
|
||||
const targetHelmReleaseInjectable = getInjectable({
|
||||
id: "target-helm-release",
|
||||
|
||||
instantiate: (di) => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const routeParameters = di.inject(helmReleasesRouteParametersInjectable);
|
||||
|
||||
return computed(() => {
|
||||
return computed((): TargetHelmRelease | undefined => {
|
||||
const name = routeParameters.name.get();
|
||||
const namespace = routeParameters.namespace.get();
|
||||
|
||||
if (!name || !namespace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return releases.value.get().find(matches({ name, namespace }));
|
||||
return name && namespace ? { name, namespace } : undefined;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default releaseInjectable;
|
||||
export default targetHelmReleaseInjectable;
|
||||
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { observable } from "mobx";
|
||||
|
||||
const userSuppliedValuesAreShownInjectable = getInjectable({
|
||||
id: "user-supplied-values-are-shown",
|
||||
|
||||
instantiate: () => {
|
||||
const state = observable.box(false);
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return state.get();
|
||||
},
|
||||
|
||||
toggle: () => {
|
||||
state.set(!state.get());
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default userSuppliedValuesAreShownInjectable;
|
||||
|
||||
@ -5,9 +5,13 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
||||
import { listReleases } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
||||
import releaseSecretsInjectable from "./release-secrets.injectable";
|
||||
import callForHelmReleasesInjectable from "./call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import type { HelmRelease, HelmReleaseDto } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { formatDuration } from "../../../common/utils";
|
||||
import { helmChartStore } from "../+helm-charts/helm-chart.store";
|
||||
import { capitalize } from "lodash/fp";
|
||||
|
||||
const releasesInjectable = getInjectable({
|
||||
id: "releases",
|
||||
@ -16,6 +20,7 @@ const releasesInjectable = getInjectable({
|
||||
const clusterContext = di.inject(clusterFrameContextInjectable);
|
||||
const namespaceStore = di.inject(namespaceStoreInjectable);
|
||||
const releaseSecrets = di.inject(releaseSecretsInjectable);
|
||||
const callForHelmReleases = di.inject(callForHelmReleasesInjectable);
|
||||
|
||||
return asyncComputed(async () => {
|
||||
const contextNamespaces = namespaceStore.contextNamespaces || [];
|
||||
@ -29,15 +34,83 @@ const releasesInjectable = getInjectable({
|
||||
contextNamespaces.includes(namespace),
|
||||
);
|
||||
|
||||
const releaseArrays = await (isLoadingAll ? listReleases() : Promise.all(
|
||||
const releaseArrays = await (isLoadingAll ? callForHelmReleases() : Promise.all(
|
||||
contextNamespaces.map((namespace) =>
|
||||
listReleases(namespace),
|
||||
callForHelmReleases(namespace),
|
||||
),
|
||||
));
|
||||
|
||||
return releaseArrays.flat();
|
||||
return releaseArrays.flat().map(toHelmRelease);
|
||||
}, []);
|
||||
},
|
||||
});
|
||||
|
||||
export const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({
|
||||
...release,
|
||||
|
||||
getId() {
|
||||
return `${this.namespace}/${this.name}`;
|
||||
},
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
},
|
||||
|
||||
getNs() {
|
||||
return this.namespace;
|
||||
},
|
||||
|
||||
getChart(withVersion = false) {
|
||||
let chart = this.chart;
|
||||
|
||||
if (!withVersion && this.getVersion() != "") {
|
||||
const search = new RegExp(`-${this.getVersion()}`);
|
||||
|
||||
chart = chart.replace(search, "");
|
||||
}
|
||||
|
||||
return chart;
|
||||
},
|
||||
|
||||
getRevision() {
|
||||
return parseInt(this.revision, 10);
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
return capitalize(this.status);
|
||||
},
|
||||
|
||||
getVersion() {
|
||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||
|
||||
return versions?.[0] ?? "";
|
||||
},
|
||||
|
||||
getUpdated(humanize = true, compact = true) {
|
||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||
const updatedDate = new Date(updated).getTime();
|
||||
const diff = Date.now() - updatedDate;
|
||||
|
||||
if (humanize) {
|
||||
return formatDuration(diff, compact);
|
||||
}
|
||||
|
||||
return diff;
|
||||
},
|
||||
|
||||
// Helm does not store from what repository the release is installed,
|
||||
// so we have to try to guess it by searching charts
|
||||
async getRepo() {
|
||||
const chartName = this.getChart();
|
||||
const version = this.getVersion();
|
||||
const versions = await helmChartStore.getVersions(chartName);
|
||||
const chartVersion = versions.find(
|
||||
(chartVersion) => chartVersion.version === version,
|
||||
);
|
||||
|
||||
return chartVersion ? chartVersion.repo : "";
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default releasesInjectable;
|
||||
|
||||
@ -16,7 +16,6 @@ import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import { kebabCase } from "lodash/fp";
|
||||
import { HelmReleaseMenu } from "./release-menu";
|
||||
import { ReleaseRollbackDialog } from "./dialog/dialog";
|
||||
import { ReleaseDetails } from "./release-details/release-details";
|
||||
import removableReleasesInjectable from "./removable-releases.injectable";
|
||||
import type { RemovableHelmRelease } from "./removable-releases";
|
||||
import type { IComputedValue } from "mobx";
|
||||
@ -145,6 +144,7 @@ class NonInjectedHelmReleases extends Component<Dependencies> {
|
||||
isConfigurable
|
||||
tableId="helm_releases"
|
||||
className="HelmReleases"
|
||||
customizeTableRowProps={(item) => ({ testId: `helm-release-row-for-${item.getId()}` })}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: release => release.getName(),
|
||||
[columnId.namespace]: release => release.getNs(),
|
||||
@ -204,10 +204,7 @@ class NonInjectedHelmReleases extends Component<Dependencies> {
|
||||
message: this.renderRemoveDialogMessage(selectedItems),
|
||||
})}
|
||||
onDetails={this.onDetails}
|
||||
/>
|
||||
|
||||
<ReleaseDetails
|
||||
hideDetails={this.hideDetails}
|
||||
spinnerTestId="helm-releases-spinner"
|
||||
/>
|
||||
|
||||
<ReleaseRollbackDialog/>
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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 callForHelmReleaseUpdateInjectable from "./call-for-helm-release-update.injectable";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleaseUpdateInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm release update without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 { apiBase } from "../../../../../common/k8s-api";
|
||||
import { endpoint } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
interface HelmReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export type CallForHelmReleaseUpdate = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
payload: HelmReleaseUpdatePayload
|
||||
) => Promise<{ updateWasSuccessful: true } | { updateWasSuccessful: false; error: unknown }>;
|
||||
|
||||
const callForHelmReleaseUpdateInjectable = getInjectable({
|
||||
id: "call-for-helm-release-update",
|
||||
|
||||
instantiate:
|
||||
(): CallForHelmReleaseUpdate => async (name, namespace, payload) => {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
|
||||
try {
|
||||
await apiBase.put(endpoint({ name, namespace }), {
|
||||
data: {
|
||||
chart,
|
||||
values,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return { updateWasSuccessful: false, error: e };
|
||||
}
|
||||
|
||||
return { updateWasSuccessful: true };
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleaseUpdateInjectable;
|
||||
@ -3,26 +3,23 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import type {
|
||||
HelmReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import {
|
||||
updateRelease,
|
||||
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "./call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import callForHelmReleaseUpdateInjectable from "./call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
|
||||
const updateReleaseInjectable = getInjectable({
|
||||
id: "update-release",
|
||||
|
||||
instantiate: (di) => {
|
||||
instantiate: (di): CallForHelmReleaseUpdate => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const callForHelmReleaseUpdate = di.inject(callForHelmReleaseUpdateInjectable);
|
||||
|
||||
return async (
|
||||
name: string,
|
||||
namespace: string,
|
||||
payload: HelmReleaseUpdatePayload,
|
||||
name,
|
||||
namespace,
|
||||
payload,
|
||||
) => {
|
||||
const result = await updateRelease(name, namespace, payload);
|
||||
const result = await callForHelmReleaseUpdate(name, namespace, payload);
|
||||
|
||||
releases.invalidate();
|
||||
|
||||
|
||||
@ -10,13 +10,15 @@ import type { DockStore, DockTabCreateSpecific, TabId } from "../dock/store";
|
||||
import { TabKind } from "../dock/store";
|
||||
import type { UpgradeChartTabStore } from "./store";
|
||||
import { runInAction } from "mobx";
|
||||
import getRandomUpgradeChartTabIdInjectable from "./get-random-upgrade-chart-tab-id.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
upgradeChartStore: UpgradeChartTabStore;
|
||||
dockStore: DockStore;
|
||||
getRandomId: () => string;
|
||||
}
|
||||
|
||||
const createUpgradeChartTab = ({ upgradeChartStore, dockStore }: Dependencies) => (release: HelmRelease, tabParams: DockTabCreateSpecific = {}): TabId => {
|
||||
const createUpgradeChartTab = ({ upgradeChartStore, dockStore, getRandomId }: Dependencies) => (release: HelmRelease, tabParams: DockTabCreateSpecific = {}): TabId => {
|
||||
const tabId = upgradeChartStore.getTabIdByRelease(release.getName());
|
||||
|
||||
if (tabId) {
|
||||
@ -29,6 +31,7 @@ const createUpgradeChartTab = ({ upgradeChartStore, dockStore }: Dependencies) =
|
||||
return runInAction(() => {
|
||||
const tab = dockStore.createTab(
|
||||
{
|
||||
id: getRandomId(),
|
||||
title: `Helm Upgrade: ${release.getName()}`,
|
||||
...tabParams,
|
||||
kind: TabKind.UPGRADE_CHART,
|
||||
@ -51,6 +54,7 @@ const createUpgradeChartTabInjectable = getInjectable({
|
||||
instantiate: (di) => createUpgradeChartTab({
|
||||
upgradeChartStore: di.inject(upgradeChartTabStoreInjectable),
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
getRandomId: di.inject(getRandomUpgradeChartTabIdInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
|
||||
|
||||
const getRandomUpgradeChartTabIdInjectable = getInjectable({
|
||||
id: "get-random-upgrade-chart-tab-id",
|
||||
instantiate: (di) => di.inject(getRandomIdInjectable),
|
||||
});
|
||||
|
||||
export default getRandomUpgradeChartTabIdInjectable;
|
||||
@ -16,7 +16,7 @@ import { Spinner } from "../../spinner";
|
||||
import { Badge } from "../../badge";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { helmChartStore, type ChartVersion } from "../../+helm-charts/helm-chart.store";
|
||||
import type { HelmRelease, HelmReleaseUpdateDetails, HelmReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||
@ -24,6 +24,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import upgradeChartTabStoreInjectable from "./store.injectable";
|
||||
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
|
||||
import releasesInjectable from "../../+helm-releases/releases.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "../../+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import { first } from "lodash/fp";
|
||||
|
||||
export interface UpgradeChartProps {
|
||||
className?: string;
|
||||
@ -33,7 +35,7 @@ export interface UpgradeChartProps {
|
||||
interface Dependencies {
|
||||
releases: IAsyncComputed<HelmRelease[]>;
|
||||
upgradeChartTabStore: UpgradeChartTabStore;
|
||||
updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>;
|
||||
updateRelease: CallForHelmReleaseUpdate;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -96,7 +98,7 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
|
||||
const versions = await helmChartStore.getVersions(release.getChart());
|
||||
|
||||
this.versions.replace(versions);
|
||||
this.version = this.versions[0];
|
||||
this.version = first(this.versions);
|
||||
}
|
||||
|
||||
onChange = action((value: string) => {
|
||||
|
||||
@ -39,6 +39,8 @@ export interface DrawerProps {
|
||||
onClose?: () => void;
|
||||
toolbar?: React.ReactNode;
|
||||
children?: SingleOrMany<React.ReactNode>;
|
||||
"data-testid"?: string;
|
||||
testIdForClose?: string;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@ -179,7 +181,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies & typ
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, contentClass, animation, open, position, title, children, toolbar, size, usePortal } = this.props;
|
||||
const { className, contentClass, animation, open, position, title, children, toolbar, size, usePortal, "data-testid": testId, testIdForClose } = this.props;
|
||||
const { isCopied, width } = this.state;
|
||||
const copyTooltip = isCopied ? "Copied!" : "Copy";
|
||||
const copyIcon = isCopied ? "done" : "content_copy";
|
||||
@ -193,6 +195,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies & typ
|
||||
className={cssNames("Drawer", className, position)}
|
||||
style={{ "--size": drawerSize } as React.CSSProperties}
|
||||
ref={e => this.contentElem = e}
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className="drawer-wrapper flex column">
|
||||
<div className="drawer-title flex align-center">
|
||||
@ -211,6 +214,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies & typ
|
||||
material="close"
|
||||
tooltip="Close"
|
||||
onClick={this.close}
|
||||
data-testid={testIdForClose}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -60,6 +60,8 @@ export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStor
|
||||
// other
|
||||
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
|
||||
|
||||
spinnerTestId?: string;
|
||||
|
||||
/**
|
||||
* Message to display when a store failed to load
|
||||
*
|
||||
@ -221,7 +223,7 @@ class NonInjectedItemListLayoutContent<
|
||||
}
|
||||
|
||||
if (!this.props.getIsReady()) {
|
||||
return <Spinner center />;
|
||||
return <Spinner center data-testid={this.props.spinnerTestId} />;
|
||||
}
|
||||
|
||||
if (this.props.getFilters().length > 0) {
|
||||
|
||||
@ -121,6 +121,8 @@ export type ItemListLayoutProps<Item extends ItemObject, PreLoadStores extends b
|
||||
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
|
||||
renderFooter?: (parent: NonInjectedItemListLayout<Item, PreLoadStores>) => React.ReactNode;
|
||||
|
||||
spinnerTestId?: string;
|
||||
|
||||
/**
|
||||
* Message to display when a store failed to load
|
||||
*
|
||||
@ -321,6 +323,7 @@ class NonInjectedItemListLayout<I extends ItemObject, PreLoadStores extends bool
|
||||
onDetails={this.props.onDetails}
|
||||
customizeRemoveDialog={this.props.customizeRemoveDialog}
|
||||
failedToLoadMessage={this.props.failedToLoadMessage}
|
||||
spinnerTestId={this.props.spinnerTestId}
|
||||
/>
|
||||
|
||||
{this.props.renderFooter?.(this)}
|
||||
|
||||
@ -96,6 +96,16 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
|
||||
di.preventSideEffects();
|
||||
|
||||
if (doGeneralOverrides) {
|
||||
const globalOverrideFilePaths = getGlobalOverridePaths();
|
||||
|
||||
const globalOverrides = globalOverrideFilePaths.map(
|
||||
(filePath) => require(filePath).default,
|
||||
);
|
||||
|
||||
globalOverrides.forEach(globalOverride => {
|
||||
di.override(globalOverride.injectable, globalOverride.overridingInstantiate);
|
||||
});
|
||||
|
||||
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
|
||||
di.override(platformInjectable, () => "darwin");
|
||||
di.override(startTopbarStateSyncInjectable, () => ({
|
||||
@ -228,6 +238,14 @@ const getInjectableFilePaths = memoize(() => [
|
||||
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||
]);
|
||||
|
||||
const getGlobalOverridePaths = memoize(() =>
|
||||
glob.sync(
|
||||
"../{common,extensions,renderer}/**/*.global-override-for-injectable.{ts,tsx}",
|
||||
|
||||
{ cwd: __dirname },
|
||||
),
|
||||
);
|
||||
|
||||
const overrideFunctionalInjectables = (di: DiContainer, injectables: Injectable<any, any, any>[]) => {
|
||||
injectables.forEach(injectable => {
|
||||
di.override(injectable, () => () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user