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>
|
||||||
</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>
|
</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.
|
* 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 { apiBase } from "../index";
|
||||||
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
|
|
||||||
import type { ItemObject } from "../../item.store";
|
import type { ItemObject } from "../../item.store";
|
||||||
import type { JsonApiData } from "../json-api";
|
import type { JsonApiData } from "../json-api";
|
||||||
import { buildURLPositional } from "../../utils/buildUrl";
|
import { buildURLPositional } from "../../utils/buildUrl";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HelmReleaseUpdateDetails {
|
export interface HelmReleaseUpdateDetails {
|
||||||
log: string;
|
log: string;
|
||||||
@ -69,47 +32,7 @@ interface EndpointQuery {
|
|||||||
all?: boolean;
|
all?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
export 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 async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
|
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
|
||||||
const path = endpoint({ name, namespace });
|
const path = endpoint({ name, namespace });
|
||||||
@ -139,7 +62,7 @@ export async function rollbackRelease(name: string, namespace: string, revision:
|
|||||||
return apiBase.put(path, { data });
|
return apiBase.put(path, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HelmReleaseDto {
|
export interface HelmReleaseDto {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
@ -158,70 +81,3 @@ export interface HelmRelease extends HelmReleaseDto, ItemObject {
|
|||||||
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
|
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
|
||||||
getRepo: () => Promise<string>;
|
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();
|
di.preventSideEffects();
|
||||||
|
|
||||||
if (doGeneralOverrides) {
|
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(electronInjectable, () => ({}));
|
||||||
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
|
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
|
||||||
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
|
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
|
||||||
@ -211,6 +221,14 @@ const getInjectableFilePaths = memoize(() => [
|
|||||||
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
...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
|
// TODO: Reorganize code in Runnables to get rid of requirement for override
|
||||||
const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
|
const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
|
||||||
[
|
[
|
||||||
|
|||||||
@ -191,7 +191,6 @@ async function getResources(name: string, namespace: string, kubeconfigPath: str
|
|||||||
];
|
];
|
||||||
const kubectlArgs = [
|
const kubectlArgs = [
|
||||||
"get",
|
"get",
|
||||||
"--namespace", namespace,
|
|
||||||
"--kubeconfig", kubeconfigPath,
|
"--kubeconfig", kubeconfigPath,
|
||||||
"-f", "-",
|
"-f", "-",
|
||||||
"--output", "json",
|
"--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.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
import yaml from "js-yaml";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
import { createRelease } 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 = (
|
export type CallForCreateHelmRelease = (
|
||||||
payload: HelmReleaseCreatePayload
|
payload: HelmReleaseCreatePayload
|
||||||
@ -12,7 +23,21 @@ export type CallForCreateHelmRelease = (
|
|||||||
|
|
||||||
const callForCreateHelmReleaseInjectable = getInjectable({
|
const callForCreateHelmReleaseInjectable = getInjectable({
|
||||||
id: "call-for-create-helm-release",
|
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,
|
causesSideEffects: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -4,19 +4,18 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
import type {
|
|
||||||
HelmReleaseCreatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
|
||||||
import releasesInjectable from "../releases.injectable";
|
import releasesInjectable from "../releases.injectable";
|
||||||
|
import type { CallForCreateHelmRelease } from "./call-for-create-helm-release.injectable";
|
||||||
import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
|
import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
|
||||||
|
|
||||||
const createReleaseInjectable = getInjectable({
|
const createReleaseInjectable = getInjectable({
|
||||||
id: "create-release",
|
id: "create-release",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di): CallForCreateHelmRelease => {
|
||||||
const releases = di.inject(releasesInjectable);
|
const releases = di.inject(releasesInjectable);
|
||||||
const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
|
const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
|
||||||
|
|
||||||
return async (payload: HelmReleaseCreatePayload) => {
|
return async (payload) => {
|
||||||
const release = await callForCreateRelease(payload);
|
const release = await callForCreateRelease(payload);
|
||||||
|
|
||||||
releases.invalidate();
|
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 "./release-details.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React 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";
|
|
||||||
|
|
||||||
export interface ReleaseDetailsProps {
|
import { observer } from "mobx-react";
|
||||||
hideDetails(): void;
|
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 {
|
interface Dependencies {
|
||||||
release: IComputedValue<HelmRelease | null | undefined>;
|
targetRelease: IComputedValue<
|
||||||
releaseDetails: IAsyncComputed<HelmReleaseDetails>;
|
TargetHelmRelease | undefined
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
const NonInjectedReleaseDetails = observer(
|
||||||
class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependencies> {
|
({ targetRelease }: Dependencies) => {
|
||||||
@observable saving = false;
|
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();
|
export const ReleaseDetails = withInjectables<Dependencies>(
|
||||||
} catch (err) {
|
NonInjectedReleaseDetails,
|
||||||
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))
|
getProps: (di) => ({
|
||||||
.map(([kind, items]) => (
|
targetRelease: di.inject(targetHelmReleaseInjectable),
|
||||||
<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),
|
|
||||||
}),
|
}),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -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.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { matches } from "lodash/fp";
|
|
||||||
import releasesInjectable from "../releases.injectable";
|
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import helmReleasesRouteParametersInjectable from "../helm-releases-route-parameters.injectable";
|
import helmReleasesRouteParametersInjectable from "../helm-releases-route-parameters.injectable";
|
||||||
|
|
||||||
const releaseInjectable = getInjectable({
|
export interface TargetHelmRelease { name: string; namespace: string }
|
||||||
id: "release",
|
|
||||||
|
const targetHelmReleaseInjectable = getInjectable({
|
||||||
|
id: "target-helm-release",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const releases = di.inject(releasesInjectable);
|
|
||||||
const routeParameters = di.inject(helmReleasesRouteParametersInjectable);
|
const routeParameters = di.inject(helmReleasesRouteParametersInjectable);
|
||||||
|
|
||||||
return computed(() => {
|
return computed((): TargetHelmRelease | undefined => {
|
||||||
const name = routeParameters.name.get();
|
const name = routeParameters.name.get();
|
||||||
const namespace = routeParameters.namespace.get();
|
const namespace = routeParameters.namespace.get();
|
||||||
|
|
||||||
if (!name || !namespace) {
|
return name && namespace ? { name, namespace } : undefined;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return releases.value.get().find(matches({ name, namespace }));
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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 { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
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 clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
||||||
import releaseSecretsInjectable from "./release-secrets.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({
|
const releasesInjectable = getInjectable({
|
||||||
id: "releases",
|
id: "releases",
|
||||||
@ -16,6 +20,7 @@ const releasesInjectable = getInjectable({
|
|||||||
const clusterContext = di.inject(clusterFrameContextInjectable);
|
const clusterContext = di.inject(clusterFrameContextInjectable);
|
||||||
const namespaceStore = di.inject(namespaceStoreInjectable);
|
const namespaceStore = di.inject(namespaceStoreInjectable);
|
||||||
const releaseSecrets = di.inject(releaseSecretsInjectable);
|
const releaseSecrets = di.inject(releaseSecretsInjectable);
|
||||||
|
const callForHelmReleases = di.inject(callForHelmReleasesInjectable);
|
||||||
|
|
||||||
return asyncComputed(async () => {
|
return asyncComputed(async () => {
|
||||||
const contextNamespaces = namespaceStore.contextNamespaces || [];
|
const contextNamespaces = namespaceStore.contextNamespaces || [];
|
||||||
@ -29,15 +34,83 @@ const releasesInjectable = getInjectable({
|
|||||||
contextNamespaces.includes(namespace),
|
contextNamespaces.includes(namespace),
|
||||||
);
|
);
|
||||||
|
|
||||||
const releaseArrays = await (isLoadingAll ? listReleases() : Promise.all(
|
const releaseArrays = await (isLoadingAll ? callForHelmReleases() : Promise.all(
|
||||||
contextNamespaces.map((namespace) =>
|
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;
|
export default releasesInjectable;
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
|||||||
import { kebabCase } from "lodash/fp";
|
import { kebabCase } from "lodash/fp";
|
||||||
import { HelmReleaseMenu } from "./release-menu";
|
import { HelmReleaseMenu } from "./release-menu";
|
||||||
import { ReleaseRollbackDialog } from "./dialog/dialog";
|
import { ReleaseRollbackDialog } from "./dialog/dialog";
|
||||||
import { ReleaseDetails } from "./release-details/release-details";
|
|
||||||
import removableReleasesInjectable from "./removable-releases.injectable";
|
import removableReleasesInjectable from "./removable-releases.injectable";
|
||||||
import type { RemovableHelmRelease } from "./removable-releases";
|
import type { RemovableHelmRelease } from "./removable-releases";
|
||||||
import type { IComputedValue } from "mobx";
|
import type { IComputedValue } from "mobx";
|
||||||
@ -145,6 +144,7 @@ class NonInjectedHelmReleases extends Component<Dependencies> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="helm_releases"
|
tableId="helm_releases"
|
||||||
className="HelmReleases"
|
className="HelmReleases"
|
||||||
|
customizeTableRowProps={(item) => ({ testId: `helm-release-row-for-${item.getId()}` })}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: release => release.getName(),
|
[columnId.name]: release => release.getName(),
|
||||||
[columnId.namespace]: release => release.getNs(),
|
[columnId.namespace]: release => release.getNs(),
|
||||||
@ -204,10 +204,7 @@ class NonInjectedHelmReleases extends Component<Dependencies> {
|
|||||||
message: this.renderRemoveDialogMessage(selectedItems),
|
message: this.renderRemoveDialogMessage(selectedItems),
|
||||||
})}
|
})}
|
||||||
onDetails={this.onDetails}
|
onDetails={this.onDetails}
|
||||||
/>
|
spinnerTestId="helm-releases-spinner"
|
||||||
|
|
||||||
<ReleaseDetails
|
|
||||||
hideDetails={this.hideDetails}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ReleaseRollbackDialog/>
|
<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.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
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 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({
|
const updateReleaseInjectable = getInjectable({
|
||||||
id: "update-release",
|
id: "update-release",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di): CallForHelmReleaseUpdate => {
|
||||||
const releases = di.inject(releasesInjectable);
|
const releases = di.inject(releasesInjectable);
|
||||||
|
const callForHelmReleaseUpdate = di.inject(callForHelmReleaseUpdateInjectable);
|
||||||
|
|
||||||
return async (
|
return async (
|
||||||
name: string,
|
name,
|
||||||
namespace: string,
|
namespace,
|
||||||
payload: HelmReleaseUpdatePayload,
|
payload,
|
||||||
) => {
|
) => {
|
||||||
const result = await updateRelease(name, namespace, payload);
|
const result = await callForHelmReleaseUpdate(name, namespace, payload);
|
||||||
|
|
||||||
releases.invalidate();
|
releases.invalidate();
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,15 @@ import type { DockStore, DockTabCreateSpecific, TabId } from "../dock/store";
|
|||||||
import { TabKind } from "../dock/store";
|
import { TabKind } from "../dock/store";
|
||||||
import type { UpgradeChartTabStore } from "./store";
|
import type { UpgradeChartTabStore } from "./store";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
|
import getRandomUpgradeChartTabIdInjectable from "./get-random-upgrade-chart-tab-id.injectable";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
upgradeChartStore: UpgradeChartTabStore;
|
upgradeChartStore: UpgradeChartTabStore;
|
||||||
dockStore: DockStore;
|
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());
|
const tabId = upgradeChartStore.getTabIdByRelease(release.getName());
|
||||||
|
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
@ -29,6 +31,7 @@ const createUpgradeChartTab = ({ upgradeChartStore, dockStore }: Dependencies) =
|
|||||||
return runInAction(() => {
|
return runInAction(() => {
|
||||||
const tab = dockStore.createTab(
|
const tab = dockStore.createTab(
|
||||||
{
|
{
|
||||||
|
id: getRandomId(),
|
||||||
title: `Helm Upgrade: ${release.getName()}`,
|
title: `Helm Upgrade: ${release.getName()}`,
|
||||||
...tabParams,
|
...tabParams,
|
||||||
kind: TabKind.UPGRADE_CHART,
|
kind: TabKind.UPGRADE_CHART,
|
||||||
@ -51,6 +54,7 @@ const createUpgradeChartTabInjectable = getInjectable({
|
|||||||
instantiate: (di) => createUpgradeChartTab({
|
instantiate: (di) => createUpgradeChartTab({
|
||||||
upgradeChartStore: di.inject(upgradeChartTabStoreInjectable),
|
upgradeChartStore: di.inject(upgradeChartTabStoreInjectable),
|
||||||
dockStore: di.inject(dockStoreInjectable),
|
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 { Badge } from "../../badge";
|
||||||
import { EditorPanel } from "../editor-panel";
|
import { EditorPanel } from "../editor-panel";
|
||||||
import { helmChartStore, type ChartVersion } from "../../+helm-charts/helm-chart.store";
|
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 type { SelectOption } from "../../select";
|
||||||
import { Select } from "../../select";
|
import { Select } from "../../select";
|
||||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
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 upgradeChartTabStoreInjectable from "./store.injectable";
|
||||||
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
|
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
|
||||||
import releasesInjectable from "../../+helm-releases/releases.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 {
|
export interface UpgradeChartProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -33,7 +35,7 @@ export interface UpgradeChartProps {
|
|||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
releases: IAsyncComputed<HelmRelease[]>;
|
releases: IAsyncComputed<HelmRelease[]>;
|
||||||
upgradeChartTabStore: UpgradeChartTabStore;
|
upgradeChartTabStore: UpgradeChartTabStore;
|
||||||
updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>;
|
updateRelease: CallForHelmReleaseUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -96,7 +98,7 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
|
|||||||
const versions = await helmChartStore.getVersions(release.getChart());
|
const versions = await helmChartStore.getVersions(release.getChart());
|
||||||
|
|
||||||
this.versions.replace(versions);
|
this.versions.replace(versions);
|
||||||
this.version = this.versions[0];
|
this.version = first(this.versions);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = action((value: string) => {
|
onChange = action((value: string) => {
|
||||||
|
|||||||
@ -39,6 +39,8 @@ export interface DrawerProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
toolbar?: React.ReactNode;
|
toolbar?: React.ReactNode;
|
||||||
children?: SingleOrMany<React.ReactNode>;
|
children?: SingleOrMany<React.ReactNode>;
|
||||||
|
"data-testid"?: string;
|
||||||
|
testIdForClose?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
@ -179,7 +181,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies & typ
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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 { isCopied, width } = this.state;
|
||||||
const copyTooltip = isCopied ? "Copied!" : "Copy";
|
const copyTooltip = isCopied ? "Copied!" : "Copy";
|
||||||
const copyIcon = isCopied ? "done" : "content_copy";
|
const copyIcon = isCopied ? "done" : "content_copy";
|
||||||
@ -193,6 +195,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies & typ
|
|||||||
className={cssNames("Drawer", className, position)}
|
className={cssNames("Drawer", className, position)}
|
||||||
style={{ "--size": drawerSize } as React.CSSProperties}
|
style={{ "--size": drawerSize } as React.CSSProperties}
|
||||||
ref={e => this.contentElem = e}
|
ref={e => this.contentElem = e}
|
||||||
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
<div className="drawer-wrapper flex column">
|
<div className="drawer-wrapper flex column">
|
||||||
<div className="drawer-title flex align-center">
|
<div className="drawer-title flex align-center">
|
||||||
@ -211,6 +214,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies & typ
|
|||||||
material="close"
|
material="close"
|
||||||
tooltip="Close"
|
tooltip="Close"
|
||||||
onClick={this.close}
|
onClick={this.close}
|
||||||
|
data-testid={testIdForClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -60,6 +60,8 @@ export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStor
|
|||||||
// other
|
// other
|
||||||
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
|
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
|
||||||
|
|
||||||
|
spinnerTestId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message to display when a store failed to load
|
* Message to display when a store failed to load
|
||||||
*
|
*
|
||||||
@ -221,7 +223,7 @@ class NonInjectedItemListLayoutContent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.props.getIsReady()) {
|
if (!this.props.getIsReady()) {
|
||||||
return <Spinner center />;
|
return <Spinner center data-testid={this.props.spinnerTestId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.getFilters().length > 0) {
|
if (this.props.getFilters().length > 0) {
|
||||||
|
|||||||
@ -121,6 +121,8 @@ export type ItemListLayoutProps<Item extends ItemObject, PreLoadStores extends b
|
|||||||
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
|
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
|
||||||
renderFooter?: (parent: NonInjectedItemListLayout<Item, PreLoadStores>) => React.ReactNode;
|
renderFooter?: (parent: NonInjectedItemListLayout<Item, PreLoadStores>) => React.ReactNode;
|
||||||
|
|
||||||
|
spinnerTestId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message to display when a store failed to load
|
* 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}
|
onDetails={this.props.onDetails}
|
||||||
customizeRemoveDialog={this.props.customizeRemoveDialog}
|
customizeRemoveDialog={this.props.customizeRemoveDialog}
|
||||||
failedToLoadMessage={this.props.failedToLoadMessage}
|
failedToLoadMessage={this.props.failedToLoadMessage}
|
||||||
|
spinnerTestId={this.props.spinnerTestId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{this.props.renderFooter?.(this)}
|
{this.props.renderFooter?.(this)}
|
||||||
|
|||||||
@ -96,6 +96,16 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
|
|||||||
di.preventSideEffects();
|
di.preventSideEffects();
|
||||||
|
|
||||||
if (doGeneralOverrides) {
|
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(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
|
||||||
di.override(platformInjectable, () => "darwin");
|
di.override(platformInjectable, () => "darwin");
|
||||||
di.override(startTopbarStateSyncInjectable, () => ({
|
di.override(startTopbarStateSyncInjectable, () => ({
|
||||||
@ -228,6 +238,14 @@ const getInjectableFilePaths = memoize(() => [
|
|||||||
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
...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>[]) => {
|
const overrideFunctionalInjectables = (di: DiContainer, injectables: Injectable<any, any, any>[]) => {
|
||||||
injectables.forEach(injectable => {
|
injectables.forEach(injectable => {
|
||||||
di.override(injectable, () => () => {
|
di.override(injectable, () => () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user