mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Make opening of release details work properly when release has resources without namespace (#6088)
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
d4bb416f99
commit
ec78080d99
@ -16,8 +16,7 @@ const navigateToHelmReleasesInjectable = getInjectable({
|
|||||||
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
|
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
|
||||||
const route = di.inject(helmReleasesRouteInjectable);
|
const route = di.inject(helmReleasesRouteInjectable);
|
||||||
|
|
||||||
return (parameters) =>
|
return (parameters) => navigateToRoute(route, { parameters });
|
||||||
navigateToRoute(route, { parameters });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,20 +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 { ExecFileOptions } from "child_process";
|
||||||
import { execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
export type ExecFile = (filePath: string, args: string[]) => Promise<string>;
|
export type ExecFile = (filePath: string, args: string[], options: ExecFileOptions) => Promise<string>;
|
||||||
|
|
||||||
const execFileInjectable = getInjectable({
|
const execFileInjectable = getInjectable({
|
||||||
id: "exec-file",
|
id: "exec-file",
|
||||||
|
|
||||||
instantiate: (): ExecFile => async (filePath, args) => {
|
instantiate: (): ExecFile => {
|
||||||
const asyncExecFile = promisify(execFile);
|
const asyncExecFile = promisify(execFile);
|
||||||
|
|
||||||
const result = await asyncExecFile(filePath, args);
|
return async (filePath, args, options) => {
|
||||||
|
const result = await asyncExecFile(filePath, args, options);
|
||||||
|
|
||||||
return result.stdout;
|
return result.stdout;
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
causesSideEffects: true,
|
causesSideEffects: true,
|
||||||
|
|||||||
@ -167,6 +167,7 @@ describe("add custom helm repository in preferences", () => {
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "add", "some-custom-repository", "http://some.url"],
|
["repo", "add", "some-custom-repository", "http://some.url"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -365,6 +366,7 @@ describe("add custom helm repository in preferences", () => {
|
|||||||
"--cert-file",
|
"--cert-file",
|
||||||
"some-cert-file",
|
"some-cert-file",
|
||||||
],
|
],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -119,6 +119,7 @@ describe("add helm repository from list in preferences", () => {
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "add", "Some to be added repository", "some-other-url"],
|
["repo", "add", "Some to be added repository", "some-other-url"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -227,6 +228,7 @@ describe("add helm repository from list in preferences", () => {
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "remove", "Some already active repository"],
|
["repo", "remove", "Some already active repository"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -11496,7 +11496,21 @@ exports[`installing helm chart from new tab given tab for installing chart was n
|
|||||||
<div
|
<div
|
||||||
class="drawer-title-text flex gaps align-center"
|
class="drawer-title-text flex gaps align-center"
|
||||||
>
|
>
|
||||||
|
some-release
|
||||||
|
<i
|
||||||
|
class="Icon material interactive focusable"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="content_copy"
|
||||||
|
>
|
||||||
|
content_copy
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
Copy
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i
|
<i
|
||||||
class="Icon material interactive focusable"
|
class="Icon material interactive focusable"
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/h
|
|||||||
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
|
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
|
||||||
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import callForHelmReleasesInjectable from "../../../renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.injectable";
|
||||||
|
import callForHelmReleaseDetailsInjectable 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";
|
||||||
|
|
||||||
describe("installing helm chart from new tab", () => {
|
describe("installing helm chart from new tab", () => {
|
||||||
let builder: ApplicationBuilder;
|
let builder: ApplicationBuilder;
|
||||||
@ -50,6 +52,9 @@ describe("installing helm chart from new tab", () => {
|
|||||||
callForCreateHelmReleaseMock = asyncFn();
|
callForCreateHelmReleaseMock = asyncFn();
|
||||||
|
|
||||||
builder.beforeWindowStart((windowDi) => {
|
builder.beforeWindowStart((windowDi) => {
|
||||||
|
windowDi.override(callForHelmReleasesInjectable, () => async () => []);
|
||||||
|
windowDi.override(callForHelmReleaseDetailsInjectable, () => () => new Promise(() => {}));
|
||||||
|
|
||||||
windowDi.override(
|
windowDi.override(
|
||||||
directoryForLensLocalStorageInjectable,
|
directoryForLensLocalStorageInjectable,
|
||||||
() => "/some-directory-for-lens-local-storage",
|
() => "/some-directory-for-lens-local-storage",
|
||||||
|
|||||||
@ -69,6 +69,7 @@ describe("listing active helm repositories in preferences", () => {
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["env"],
|
["env"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ describe("listing active helm repositories in preferences", () => {
|
|||||||
expect(execFileMock).not.toHaveBeenCalledWith(
|
expect(execFileMock).not.toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "update"],
|
["repo", "update"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,6 +209,7 @@ describe("listing active helm repositories in preferences", () => {
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "update"],
|
["repo", "update"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -265,6 +268,7 @@ describe("listing active helm repositories in preferences", () => {
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
|
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -400,6 +404,7 @@ describe("listing active helm repositories in preferences", () => {
|
|||||||
expect(execFileMock).not.toHaveBeenCalledWith(
|
expect(execFileMock).not.toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
|
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -86,6 +86,7 @@ describe("remove helm repository from list of active repositories in preferences
|
|||||||
expect(execFileMock).toHaveBeenCalledWith(
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
"some-helm-binary-path",
|
"some-helm-binary-path",
|
||||||
["repo", "remove", "some-active-repository"],
|
["repo", "remove", "some-active-repository"],
|
||||||
|
{ "maxBuffer": 34359738368 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -288,15 +288,42 @@ describe("showing details for helm release", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when release resolve with no data, renders", async () => {
|
|
||||||
await callForHelmReleaseMock.resolve(undefined);
|
|
||||||
|
|
||||||
expect(rendered.baseElement).toMatchSnapshot();
|
describe("when call for release resolves with error", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await callForHelmReleaseMock.resolve({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "some-error",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", async () => {
|
||||||
|
expect(rendered.baseElement).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show spinner anymore", () => {
|
||||||
|
expect(
|
||||||
|
rendered.queryByTestId("helm-release-detail-content-spinner"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows error message about missing release", () => {
|
||||||
|
expect(
|
||||||
|
rendered.getByTestId("helm-release-detail-error"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call for release configuration", () => {
|
||||||
|
expect(callForHelmReleaseConfigurationMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when details resolve", () => {
|
describe("when call for release resolve with release", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await callForHelmReleaseMock.resolve(detailedReleaseFake);
|
await callForHelmReleaseMock.resolve({
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: detailedReleaseFake,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@ -83,12 +83,10 @@ import getHelmChartValuesInjectable from "./helm/helm-service/get-helm-chart-val
|
|||||||
import listHelmChartsInjectable from "./helm/helm-service/list-helm-charts.injectable";
|
import listHelmChartsInjectable from "./helm/helm-service/list-helm-charts.injectable";
|
||||||
import deleteHelmReleaseInjectable from "./helm/helm-service/delete-helm-release.injectable";
|
import deleteHelmReleaseInjectable from "./helm/helm-service/delete-helm-release.injectable";
|
||||||
import getHelmReleaseHistoryInjectable from "./helm/helm-service/get-helm-release-history.injectable";
|
import getHelmReleaseHistoryInjectable from "./helm/helm-service/get-helm-release-history.injectable";
|
||||||
import getHelmReleaseInjectable from "./helm/helm-service/get-helm-release.injectable";
|
|
||||||
import getHelmReleaseValuesInjectable from "./helm/helm-service/get-helm-release-values.injectable";
|
import getHelmReleaseValuesInjectable from "./helm/helm-service/get-helm-release-values.injectable";
|
||||||
import installHelmChartInjectable from "./helm/helm-service/install-helm-chart.injectable";
|
import installHelmChartInjectable from "./helm/helm-service/install-helm-chart.injectable";
|
||||||
import listHelmReleasesInjectable from "./helm/helm-service/list-helm-releases.injectable";
|
import listHelmReleasesInjectable from "./helm/helm-service/list-helm-releases.injectable";
|
||||||
import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable";
|
import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable";
|
||||||
import updateHelmReleaseInjectable from "./helm/helm-service/update-helm-release.injectable";
|
|
||||||
import waitUntilBundledExtensionsAreLoadedInjectable from "./start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable";
|
import waitUntilBundledExtensionsAreLoadedInjectable from "./start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable";
|
||||||
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
import electronInjectable from "./utils/resolve-system-proxy/electron.injectable";
|
import electronInjectable from "./utils/resolve-system-proxy/electron.injectable";
|
||||||
@ -168,12 +166,10 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
|
|||||||
listHelmChartsInjectable,
|
listHelmChartsInjectable,
|
||||||
deleteHelmReleaseInjectable,
|
deleteHelmReleaseInjectable,
|
||||||
getHelmReleaseHistoryInjectable,
|
getHelmReleaseHistoryInjectable,
|
||||||
getHelmReleaseInjectable,
|
|
||||||
getHelmReleaseValuesInjectable,
|
getHelmReleaseValuesInjectable,
|
||||||
installHelmChartInjectable,
|
installHelmChartInjectable,
|
||||||
listHelmReleasesInjectable,
|
listHelmReleasesInjectable,
|
||||||
rollbackHelmReleaseInjectable,
|
rollbackHelmReleaseInjectable,
|
||||||
updateHelmReleaseInjectable,
|
|
||||||
writeJsonFileInjectable,
|
writeJsonFileInjectable,
|
||||||
readJsonFileInjectable,
|
readJsonFileInjectable,
|
||||||
readFileInjectable,
|
readFileInjectable,
|
||||||
|
|||||||
@ -8,16 +8,20 @@ import helmBinaryPathInjectable from "../helm-binary-path.injectable";
|
|||||||
import type { AsyncResult } from "../../../common/utils/async-result";
|
import type { AsyncResult } from "../../../common/utils/async-result";
|
||||||
import { getErrorMessage } from "../../../common/utils/get-error-message";
|
import { getErrorMessage } from "../../../common/utils/get-error-message";
|
||||||
|
|
||||||
|
export type ExecHelm = (args: string[]) => Promise<AsyncResult<string>>;
|
||||||
|
|
||||||
const execHelmInjectable = getInjectable({
|
const execHelmInjectable = getInjectable({
|
||||||
id: "exec-helm",
|
id: "exec-helm",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di): ExecHelm => {
|
||||||
const execFile = di.inject(execFileInjectable);
|
const execFile = di.inject(execFileInjectable);
|
||||||
const helmBinaryPath = di.inject(helmBinaryPathInjectable);
|
const helmBinaryPath = di.inject(helmBinaryPathInjectable);
|
||||||
|
|
||||||
return async (...args: string[]): Promise<AsyncResult<string>> => {
|
return async (args) => {
|
||||||
try {
|
try {
|
||||||
const response = await execFile(helmBinaryPath, args);
|
const response = await execFile(helmBinaryPath, args, {
|
||||||
|
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||||
|
});
|
||||||
|
|
||||||
return { callWasSuccessful: true, response };
|
return { callWasSuccessful: true, response };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const getHelmEnvInjectable = getInjectable({
|
|||||||
const execHelm = di.inject(execHelmInjectable);
|
const execHelm = di.inject(execHelmInjectable);
|
||||||
|
|
||||||
return async (): Promise<AsyncResult<HelmEnv>> => {
|
return async (): Promise<AsyncResult<HelmEnv>> => {
|
||||||
const result = await execHelm("env");
|
const result = await execHelm(["env"]);
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
if (!result.callWasSuccessful) {
|
||||||
return { callWasSuccessful: false, error: result.error };
|
return { callWasSuccessful: false, error: result.error };
|
||||||
|
|||||||
@ -7,10 +7,8 @@ import tempy from "tempy";
|
|||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { toCamelCase } from "../../common/utils/camelCase";
|
import { toCamelCase } from "../../common/utils/camelCase";
|
||||||
import { execFile } from "child_process";
|
|
||||||
import { execHelm } from "./exec";
|
import { execHelm } from "./exec";
|
||||||
import assert from "assert";
|
import type { JsonValue } from "type-fest";
|
||||||
import type { JsonObject, JsonValue } from "type-fest";
|
|
||||||
import { isObject, json } from "../../common/utils";
|
import { isObject, json } from "../../common/utils";
|
||||||
|
|
||||||
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
|
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
|
||||||
@ -77,55 +75,6 @@ export async function installChart(chart: string, values: JsonValue, name: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, kubeconfigPath: string, kubectlPath: string) {
|
|
||||||
const valuesFilePath = tempy.file({ name: "values.yaml" });
|
|
||||||
|
|
||||||
await fse.writeFile(valuesFilePath, yaml.dump(values));
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
"upgrade",
|
|
||||||
name,
|
|
||||||
chart,
|
|
||||||
"--version", version,
|
|
||||||
"--values", valuesFilePath,
|
|
||||||
"--namespace", namespace,
|
|
||||||
"--kubeconfig", kubeconfigPath,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const output = await execHelm(args);
|
|
||||||
|
|
||||||
return {
|
|
||||||
log: output,
|
|
||||||
release: await getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
await fse.unlink(valuesFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRelease(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
|
||||||
const args = [
|
|
||||||
"status",
|
|
||||||
name,
|
|
||||||
"--namespace", namespace,
|
|
||||||
"--kubeconfig", kubeconfigPath,
|
|
||||||
"--output", "json",
|
|
||||||
];
|
|
||||||
|
|
||||||
const release = json.parse(await execHelm(args, {
|
|
||||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!isObject(release) || Array.isArray(release)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);
|
|
||||||
|
|
||||||
return release;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
|
export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
|
||||||
return execHelm([
|
return execHelm([
|
||||||
"delete",
|
"delete",
|
||||||
@ -180,62 +129,3 @@ export async function rollback(name: string, namespace: string, revision: number
|
|||||||
"--kubeconfig", kubeconfigPath,
|
"--kubeconfig", kubeconfigPath,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
|
||||||
const helmArgs = [
|
|
||||||
"get",
|
|
||||||
"manifest",
|
|
||||||
name,
|
|
||||||
"--namespace", namespace,
|
|
||||||
"--kubeconfig", kubeconfigPath,
|
|
||||||
];
|
|
||||||
const kubectlArgs = [
|
|
||||||
"get",
|
|
||||||
"--kubeconfig", kubeconfigPath,
|
|
||||||
"-f", "-",
|
|
||||||
"--output", "json",
|
|
||||||
// Temporary workaround for https://github.com/lensapp/lens/issues/6031
|
|
||||||
// and other potential issues where resources can't be found. Showing
|
|
||||||
// no resources is better than the app hard-locking, and at least
|
|
||||||
// the helm metadata is shown.
|
|
||||||
"--ignore-not-found",
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const helmOutput = await execHelm(helmArgs);
|
|
||||||
|
|
||||||
return new Promise<JsonObject[]>((resolve, reject) => {
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
const kubectl = execFile(kubectlPath, kubectlArgs);
|
|
||||||
|
|
||||||
kubectl
|
|
||||||
.on("exit", (code, signal) => {
|
|
||||||
if (typeof code === "number") {
|
|
||||||
if (code === 0) {
|
|
||||||
if (stdout === "") {
|
|
||||||
resolve([]);
|
|
||||||
} else {
|
|
||||||
const output = json.parse(stdout) as { items: JsonObject[] };
|
|
||||||
|
|
||||||
resolve(output.items);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(stderr);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Kubectl exited with signal ${signal}`));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on("error", reject);
|
|
||||||
|
|
||||||
assert(kubectl.stderr && kubectl.stdout && kubectl.stdin, "For some reason the IO streams are undefined");
|
|
||||||
|
|
||||||
kubectl.stderr.on("data", output => stderr += output);
|
|
||||||
kubectl.stdout.on("data", output => stdout += output);
|
|
||||||
kubectl.stdin.end(helmOutput);
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 { AsyncResult } from "../../../../../common/utils/async-result";
|
||||||
|
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
|
||||||
|
|
||||||
|
const callForHelmManifestInjectable = getInjectable({
|
||||||
|
id: "call-for-helm-manifest",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const execHelm = di.inject(execHelmInjectable);
|
||||||
|
|
||||||
|
return async (
|
||||||
|
name: string,
|
||||||
|
namespace: string,
|
||||||
|
kubeconfigPath: string,
|
||||||
|
): Promise<AsyncResult<KubeJsonApiData[]>> => {
|
||||||
|
const result = await execHelm([
|
||||||
|
"get",
|
||||||
|
"manifest",
|
||||||
|
name,
|
||||||
|
"--namespace",
|
||||||
|
namespace,
|
||||||
|
"--kubeconfig",
|
||||||
|
kubeconfigPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
return { callWasSuccessful: false, error: result.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: yaml
|
||||||
|
.loadAll(result.response)
|
||||||
|
.filter((manifest) => !!manifest) as KubeJsonApiData[],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default callForHelmManifestInjectable;
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 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 { JsonObject } from "type-fest";
|
||||||
|
import { json } from "../../../../../common/utils";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
import execFileWithInputInjectable from "./exec-file-with-input/exec-file-with-input.injectable";
|
||||||
|
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
|
||||||
|
import { map } from "lodash/fp";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
|
||||||
|
|
||||||
|
export type CallForKubeResourcesByManifest = (
|
||||||
|
namespace: string,
|
||||||
|
kubeconfigPath: string,
|
||||||
|
kubectlPath: string,
|
||||||
|
resourceManifests: KubeJsonApiData[]
|
||||||
|
) => Promise<JsonObject[]>;
|
||||||
|
|
||||||
|
const callForKubeResourcesByManifestInjectable = getInjectable({
|
||||||
|
id: "call-for-kube-resources-by-manifest",
|
||||||
|
|
||||||
|
instantiate: (di): CallForKubeResourcesByManifest => {
|
||||||
|
const execFileWithInput = di.inject(execFileWithInputInjectable);
|
||||||
|
|
||||||
|
return async (
|
||||||
|
namespace,
|
||||||
|
kubeconfigPath,
|
||||||
|
kubectlPath,
|
||||||
|
resourceManifests,
|
||||||
|
) => {
|
||||||
|
const input = pipeline(
|
||||||
|
resourceManifests,
|
||||||
|
map((manifest) => yaml.dump(manifest)),
|
||||||
|
wideJoin("---\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await execFileWithInput({
|
||||||
|
filePath: kubectlPath,
|
||||||
|
input,
|
||||||
|
|
||||||
|
commandArguments: [
|
||||||
|
"get",
|
||||||
|
"--kubeconfig",
|
||||||
|
kubeconfigPath,
|
||||||
|
"-f",
|
||||||
|
"-",
|
||||||
|
"--namespace",
|
||||||
|
namespace,
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
const errorMessage = getErrorMessage(result.error);
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = json.parse(result.response) as { items: JsonObject[] };
|
||||||
|
|
||||||
|
return output.items;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default callForKubeResourcesByManifestInjectable;
|
||||||
|
|
||||||
|
const wideJoin = (joiner: string) => (items: string[]) =>
|
||||||
|
`${joiner}${items.join(joiner)}${joiner}`;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 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 execFileWithInputInjectable from "./exec-file-with-input.injectable";
|
||||||
|
|
||||||
|
export default getGlobalOverride(execFileWithInputInjectable, () => () => {
|
||||||
|
throw new Error(
|
||||||
|
"Tried to call exec file with input without explicit override",
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 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 { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||||
|
import nonPromiseExecFileInjectable from "./non-promise-exec-file.injectable";
|
||||||
|
import { isNumber } from "../../../../../../common/utils";
|
||||||
|
import assert from "assert";
|
||||||
|
import type { ChildProcess } from "child_process";
|
||||||
|
|
||||||
|
export type ExecFileWithInput = (options: {
|
||||||
|
filePath: string;
|
||||||
|
commandArguments: string[];
|
||||||
|
input: string;
|
||||||
|
}) => Promise<AsyncResult<string, unknown>>;
|
||||||
|
|
||||||
|
const execFileWithInputInjectable = getInjectable({
|
||||||
|
id: "exec-file-with-input",
|
||||||
|
|
||||||
|
instantiate: (di): ExecFileWithInput => {
|
||||||
|
const execFile = di.inject(nonPromiseExecFileInjectable);
|
||||||
|
|
||||||
|
return async ({ filePath, commandArguments, input }) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
let execution: ChildProcess;
|
||||||
|
|
||||||
|
try {
|
||||||
|
execution = execFile(filePath, commandArguments, {
|
||||||
|
maxBuffer: 8 * 1024 * 1024 * 1024, // 8 MiB
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ callWasSuccessful: false, error: e });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(execution.stdout, "stdout is not defined");
|
||||||
|
assert(execution.stderr, "stderr is not defined");
|
||||||
|
assert(execution.stdin, "stdin is not defined");
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
execution.stdout.on("data", (data) => {
|
||||||
|
stdout += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
execution.stderr.on("data", (data) => {
|
||||||
|
stderr += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
execution.on("error", (error) =>
|
||||||
|
resolve({ callWasSuccessful: false, error }),
|
||||||
|
);
|
||||||
|
|
||||||
|
execution.on("exit", (code, signal) => {
|
||||||
|
if (!isNumber(code)) {
|
||||||
|
/**
|
||||||
|
* According to https://nodejs.org/api/child_process.html#class-childprocess (section about the "exit" event)
|
||||||
|
* it says the following:
|
||||||
|
*
|
||||||
|
* If the process exited, code is the final exit code of the process, otherwise null.
|
||||||
|
* If the process terminated due to receipt of a signal, signal is the string name of the signal, otherwise null.
|
||||||
|
* One of the two will always be non-null.
|
||||||
|
*/
|
||||||
|
resolve({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: `Exited via ${signal}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
resolve({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: stderr ? stderr : `Failed with error: ${signal}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ callWasSuccessful: true, response: stdout });
|
||||||
|
});
|
||||||
|
|
||||||
|
execution.stdin.end(input);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default execFileWithInputInjectable;
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getDiForUnitTesting } from "../../../../../getDiForUnitTesting";
|
||||||
|
import type { ExecFileWithInput } from "./exec-file-with-input.injectable";
|
||||||
|
import execFileWithInputInjectable from "./exec-file-with-input.injectable";
|
||||||
|
import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||||
|
import nonPromiseExecFileInjectable from "./non-promise-exec-file.injectable";
|
||||||
|
import { getPromiseStatus } from "../../../../../../common/test-utils/get-promise-status";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
describe("exec-file-with-input", () => {
|
||||||
|
let execFileWithInput: ExecFileWithInput;
|
||||||
|
let execFileMock: jest.Mock;
|
||||||
|
|
||||||
|
let executionStub: EventEmitter & {
|
||||||
|
stdin: { end: (chunk: any) => void };
|
||||||
|
stdout: EventEmitter;
|
||||||
|
stderr: EventEmitter;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.unoverride(execFileWithInputInjectable);
|
||||||
|
|
||||||
|
executionStub = Object.assign(new EventEmitter(), {
|
||||||
|
stdin: { end: jest.fn() },
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
});
|
||||||
|
|
||||||
|
execFileMock = jest.fn(() => executionStub);
|
||||||
|
|
||||||
|
di.override(nonPromiseExecFileInjectable, () => execFileMock as any);
|
||||||
|
|
||||||
|
execFileWithInput = di.inject(execFileWithInputInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given call, when throws synchronously, resolves with failure", async () => {
|
||||||
|
execFileMock.mockImplementation(() => {
|
||||||
|
throw new Error("some-error");
|
||||||
|
});
|
||||||
|
|
||||||
|
const actual = await execFileWithInput({
|
||||||
|
filePath: "./irrelevant",
|
||||||
|
commandArguments: ["irrelevant"],
|
||||||
|
input: "irrelevant",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: expect.any(Error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let actualPromise: Promise<AsyncResult<string, unknown>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actualPromise = execFileWithInput({
|
||||||
|
filePath: "./some-file-path",
|
||||||
|
commandArguments: ["some-arg", "some-other-arg"],
|
||||||
|
input: "some-input",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls for file with arguments", () => {
|
||||||
|
expect(execFileMock).toHaveBeenCalledWith(
|
||||||
|
"./some-file-path",
|
||||||
|
[
|
||||||
|
"some-arg",
|
||||||
|
"some-other-arg",
|
||||||
|
],
|
||||||
|
{ "maxBuffer": 8589934592 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls with input", () => {
|
||||||
|
expect(executionStub.stdin.end).toHaveBeenCalledWith("some-input");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when stdout receives data", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
executionStub.stdout.emit("data", "some-data");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when stdout receives more data", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
executionStub.stdout.emit("data", "some-other-data");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution exits with success, resolves with result", async () => {
|
||||||
|
executionStub.emit("exit", 0);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: "some-datasome-other-data",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution exits without exit code, resolves with failure", async () => {
|
||||||
|
executionStub.emit("exit", null, "SIGKILL");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "Exited via SIGKILL",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution exits with failure, resolves with failure", async () => {
|
||||||
|
executionStub.emit("exit", 42, "some-signal");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "Failed with error: some-signal",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when stderr receives data", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
executionStub.stderr.emit("data", "some-error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when stderr receives more data", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
executionStub.stderr.emit("data", "some-other-error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve yet", async () => {
|
||||||
|
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||||
|
|
||||||
|
expect(promiseStatus.fulfilled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution exits with success, resolves with result", async () => {
|
||||||
|
executionStub.emit("exit", 0);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: "some-datasome-other-data",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution exits without exit code, resolves with failure", async () => {
|
||||||
|
executionStub.emit("exit", null, "some-signal");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "Exited via some-signal",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution exits with failure, resolves with errors", async () => {
|
||||||
|
executionStub.emit("exit", 42, "irrelevant");
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "some-errorsome-other-error",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when execution receives error, resolves with error", async () => {
|
||||||
|
executionStub.emit("error", new Error("some-error"));
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual({
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: expect.any(Error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { execFile } from "child_process";
|
||||||
|
|
||||||
|
const nonPromiseExecFileInjectable = getInjectable({
|
||||||
|
id: "non-promise-exec-file",
|
||||||
|
instantiate: () => execFile,
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default nonPromiseExecFileInjectable;
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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 callForKubeResourcesByManifestInjectable from "./call-for-kube-resources-by-manifest/call-for-kube-resources-by-manifest.injectable";
|
||||||
|
import { groupBy, map } from "lodash/fp";
|
||||||
|
import type { JsonObject } from "type-fest";
|
||||||
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
|
import callForHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable";
|
||||||
|
|
||||||
|
export type GetHelmReleaseResources = (
|
||||||
|
name: string,
|
||||||
|
namespace: string,
|
||||||
|
kubeconfigPath: string,
|
||||||
|
kubectlPath: string
|
||||||
|
) => Promise<JsonObject[]>;
|
||||||
|
|
||||||
|
const getHelmReleaseResourcesInjectable = getInjectable({
|
||||||
|
id: "get-helm-release-resources",
|
||||||
|
|
||||||
|
instantiate: (di): GetHelmReleaseResources => {
|
||||||
|
const callForHelmManifest = di.inject(callForHelmManifestInjectable);
|
||||||
|
const callForKubeResourcesByManifest = di.inject(callForKubeResourcesByManifestInjectable);
|
||||||
|
|
||||||
|
return async (name, namespace, kubeconfigPath, kubectlPath) => {
|
||||||
|
const result = await callForHelmManifest(name, namespace, kubeconfigPath);
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await pipeline(
|
||||||
|
result.response,
|
||||||
|
|
||||||
|
groupBy((item) => item.metadata.namespace || namespace),
|
||||||
|
|
||||||
|
(x) => Object.entries(x),
|
||||||
|
|
||||||
|
map(([namespace, manifest]) =>
|
||||||
|
callForKubeResourcesByManifest(namespace, kubeconfigPath, kubectlPath, manifest),
|
||||||
|
),
|
||||||
|
|
||||||
|
promises => Promise.all(promises),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.flat(1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getHelmReleaseResourcesInjectable;
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import type { GetHelmReleaseResources } from "./get-helm-release-resources.injectable";
|
||||||
|
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources.injectable";
|
||||||
|
import type { ExecHelm } from "../../exec-helm/exec-helm.injectable";
|
||||||
|
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
|
||||||
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
import type { JsonObject } from "type-fest";
|
||||||
|
import type { ExecFileWithInput } from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
|
||||||
|
import execFileWithInputInjectable from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
|
||||||
|
|
||||||
|
describe("get helm release resources", () => {
|
||||||
|
let getHelmReleaseResources: GetHelmReleaseResources;
|
||||||
|
let execHelmMock: AsyncFnMock<ExecHelm>;
|
||||||
|
let execFileWithStreamInputMock: AsyncFnMock<ExecFileWithInput>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
execHelmMock = asyncFn();
|
||||||
|
execFileWithStreamInputMock = asyncFn();
|
||||||
|
|
||||||
|
di.override(execHelmInjectable, () => execHelmMock);
|
||||||
|
|
||||||
|
di.override(
|
||||||
|
execFileWithInputInjectable,
|
||||||
|
() => execFileWithStreamInputMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when called", () => {
|
||||||
|
let actualPromise: Promise<JsonObject[]>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actualPromise = getHelmReleaseResources(
|
||||||
|
"some-release",
|
||||||
|
"some-namespace",
|
||||||
|
"/some-kubeconfig-path",
|
||||||
|
"/some-kubectl-path",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls for release manifest", () => {
|
||||||
|
expect(execHelmMock).toHaveBeenCalledWith([
|
||||||
|
"get", "manifest", "some-release", "--namespace", "some-namespace", "--kubeconfig", "/some-kubeconfig-path",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call for resources yet", () => {
|
||||||
|
expect(execFileWithStreamInputMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when call for manifest resolves without resources, resolves without resources", async () => {
|
||||||
|
await execHelmMock.resolve({
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when call for manifest resolves", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await execHelmMock.resolve({
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: `---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: SomeKind
|
||||||
|
metadata:
|
||||||
|
name: some-resource-with-same-namespace
|
||||||
|
namespace: some-namespace
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: SomeKind
|
||||||
|
metadata:
|
||||||
|
name: some-resource-without-namespace
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: SomeKind
|
||||||
|
metadata:
|
||||||
|
name: some-resource-with-different-namespace
|
||||||
|
namespace: some-other-namespace
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls for resources from each namespace separately using the manifest as input", () => {
|
||||||
|
expect(execFileWithStreamInputMock.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
filePath: "/some-kubectl-path",
|
||||||
|
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-namespace", "--output", "json"],
|
||||||
|
input: `---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: SomeKind
|
||||||
|
metadata:
|
||||||
|
name: some-resource-with-same-namespace
|
||||||
|
namespace: some-namespace
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: SomeKind
|
||||||
|
metadata:
|
||||||
|
name: some-resource-without-namespace
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
filePath: "/some-kubectl-path",
|
||||||
|
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-other-namespace", "--output", "json"],
|
||||||
|
input: `---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: SomeKind
|
||||||
|
metadata:
|
||||||
|
name: some-resource-with-different-namespace
|
||||||
|
namespace: some-other-namespace
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when all calls for resources resolve, resolves with combined result", async () => {
|
||||||
|
await execFileWithStreamInputMock.resolveSpecific(
|
||||||
|
([{ commandArguments }]) =>
|
||||||
|
commandArguments.includes("some-namespace"),
|
||||||
|
{
|
||||||
|
callWasSuccessful: true,
|
||||||
|
|
||||||
|
response: JSON.stringify({
|
||||||
|
items: [{ some: "item" }],
|
||||||
|
|
||||||
|
kind: "List",
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
resourceVersion: "",
|
||||||
|
selfLink: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await execFileWithStreamInputMock.resolveSpecific(
|
||||||
|
([{ commandArguments }]) =>
|
||||||
|
commandArguments.includes("some-other-namespace"),
|
||||||
|
{
|
||||||
|
callWasSuccessful: true,
|
||||||
|
|
||||||
|
response: JSON.stringify({
|
||||||
|
items: [{ some: "other-item" }],
|
||||||
|
|
||||||
|
kind: "List",
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
resourceVersion: "",
|
||||||
|
selfLink: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const actual = await actualPromise;
|
||||||
|
|
||||||
|
expect(actual).toEqual([{ some: "item" }, { some: "other-item" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given some call fails, when all calls have finished, rejects with failure", async () => {
|
||||||
|
await execFileWithStreamInputMock.resolveSpecific(
|
||||||
|
([{ commandArguments }]) =>
|
||||||
|
commandArguments.includes("some-namespace"),
|
||||||
|
|
||||||
|
{
|
||||||
|
callWasSuccessful: true,
|
||||||
|
|
||||||
|
response: JSON.stringify({
|
||||||
|
items: [{ some: "item" }],
|
||||||
|
|
||||||
|
kind: "List",
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
resourceVersion: "",
|
||||||
|
selfLink: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
execFileWithStreamInputMock.resolveSpecific(
|
||||||
|
([{ commandArguments }]) =>
|
||||||
|
commandArguments.includes("some-other-namespace"),
|
||||||
|
|
||||||
|
{
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: "some-error",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return expect(actualPromise).rejects.toEqual(expect.any(Error));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 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 getHelmReleaseInjectable from "./get-helm-release.injectable";
|
||||||
|
|
||||||
|
export default getGlobalOverride(getHelmReleaseInjectable, () => () => {
|
||||||
|
throw new Error("Tried to get helm release without explicit override");
|
||||||
|
});
|
||||||
@ -4,14 +4,18 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
import { getRelease } from "../helm-release-manager";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import { isObject, json } from "../../../common/utils";
|
||||||
|
import execHelmInjectable from "../exec-helm/exec-helm.injectable";
|
||||||
|
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable";
|
||||||
|
|
||||||
const getHelmReleaseInjectable = getInjectable({
|
const getHelmReleaseInjectable = getInjectable({
|
||||||
id: "get-helm-release",
|
id: "get-helm-release",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const execHelm = di.inject(execHelmInjectable);
|
||||||
|
const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
|
||||||
|
|
||||||
return async (cluster: Cluster, releaseName: string, namespace: string) => {
|
return async (cluster: Cluster, releaseName: string, namespace: string) => {
|
||||||
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
||||||
@ -20,7 +24,37 @@ const getHelmReleaseInjectable = getInjectable({
|
|||||||
|
|
||||||
logger.debug("Fetch release");
|
logger.debug("Fetch release");
|
||||||
|
|
||||||
return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
|
const args = [
|
||||||
|
"status",
|
||||||
|
releaseName,
|
||||||
|
"--namespace",
|
||||||
|
namespace,
|
||||||
|
"--kubeconfig",
|
||||||
|
kubeconfigPath,
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await execHelm(args);
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = json.parse(result.response);
|
||||||
|
|
||||||
|
if (!isObject(release) || Array.isArray(release)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
release.resources = await getHelmReleaseResources(
|
||||||
|
releaseName,
|
||||||
|
namespace,
|
||||||
|
kubeconfigPath,
|
||||||
|
kubectlPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
return release;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 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 updateHelmReleaseInjectable from "./update-helm-release.injectable";
|
||||||
|
|
||||||
|
export default getGlobalOverride(updateHelmReleaseInjectable, () => () => {
|
||||||
|
throw new Error("Tried to update helm release without explicit override");
|
||||||
|
});
|
||||||
@ -4,9 +4,13 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
import { upgradeRelease } from "../helm-release-manager";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
import type { JsonObject } from "type-fest";
|
import type { JsonObject } from "type-fest";
|
||||||
|
import { execHelm } from "../exec";
|
||||||
|
import tempy from "tempy";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
import getHelmReleaseInjectable from "./get-helm-release.injectable";
|
||||||
|
|
||||||
export interface UpdateChartArgs {
|
export interface UpdateChartArgs {
|
||||||
chart: string;
|
chart: string;
|
||||||
@ -19,23 +23,37 @@ const updateHelmReleaseInjectable = getInjectable({
|
|||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const getHelmRelease = di.inject(getHelmReleaseInjectable);
|
||||||
|
|
||||||
return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => {
|
return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
const kubectl = await cluster.ensureKubectl();
|
|
||||||
const kubectlPath = await kubectl.getPath();
|
|
||||||
|
|
||||||
logger.debug("Upgrade release");
|
logger.debug("Upgrade release");
|
||||||
|
|
||||||
return upgradeRelease(
|
const valuesFilePath = tempy.file({ name: "values.yaml" });
|
||||||
|
|
||||||
|
await fse.writeFile(valuesFilePath, yaml.dump(data.values));
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"upgrade",
|
||||||
releaseName,
|
releaseName,
|
||||||
data.chart,
|
data.chart,
|
||||||
data.values,
|
"--version", data.version,
|
||||||
namespace,
|
"--values", valuesFilePath,
|
||||||
data.version,
|
"--namespace", namespace,
|
||||||
proxyKubeconfig,
|
"--kubeconfig", proxyKubeconfig,
|
||||||
kubectlPath,
|
];
|
||||||
);
|
|
||||||
|
try {
|
||||||
|
const output = await execHelm(args);
|
||||||
|
|
||||||
|
return {
|
||||||
|
log: output,
|
||||||
|
release: await getHelmRelease(cluster, releaseName, namespace),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await fse.unlink(valuesFilePath);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -43,3 +61,4 @@ const updateHelmReleaseInjectable = getInjectable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default updateHelmReleaseInjectable;
|
export default updateHelmReleaseInjectable;
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const addHelmRepositoryInjectable = getInjectable({
|
|||||||
args.push("--cert-file", certFile);
|
args.push("--cert-file", certFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await execHelm(...args);
|
return await execHelm(args);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -75,7 +75,7 @@ const getActiveHelmRepositoriesInjectable = getInjectable({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateResult = await execHelm("repo", "update");
|
const updateResult = await execHelm(["repo", "update"]);
|
||||||
|
|
||||||
if (!updateResult.callWasSuccessful) {
|
if (!updateResult.callWasSuccessful) {
|
||||||
if (!updateResult.error.includes(internalHelmErrorForNoRepositoriesFound)) {
|
if (!updateResult.error.includes(internalHelmErrorForNoRepositoriesFound)) {
|
||||||
@ -84,7 +84,7 @@ const getActiveHelmRepositoriesInjectable = getInjectable({
|
|||||||
error: `Error updating Helm repositories: ${updateResult.error}`,
|
error: `Error updating Helm repositories: ${updateResult.error}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const resultOfAddingDefaultRepository = await execHelm("repo", "add", "bitnami", "https://charts.bitnami.com/bitnami");
|
const resultOfAddingDefaultRepository = await execHelm(["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"]);
|
||||||
|
|
||||||
if (!resultOfAddingDefaultRepository.callWasSuccessful) {
|
if (!resultOfAddingDefaultRepository.callWasSuccessful) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -17,11 +17,11 @@ const removeHelmRepositoryInjectable = getInjectable({
|
|||||||
return async (repo: HelmRepo) => {
|
return async (repo: HelmRepo) => {
|
||||||
logger.info(`[HELM]: removing repo ${repo.name} (${repo.url})`);
|
logger.info(`[HELM]: removing repo ${repo.name} (${repo.url})`);
|
||||||
|
|
||||||
return execHelm(
|
return execHelm([
|
||||||
"repo",
|
"repo",
|
||||||
"remove",
|
"remove",
|
||||||
repo.name,
|
repo.name,
|
||||||
);
|
]);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import "./release-details.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Drawer, DrawerItem, DrawerTitle } from "../../drawer";
|
import { DrawerItem, DrawerTitle } from "../../drawer";
|
||||||
import { cssNames, stopPropagation } from "../../../utils";
|
import { stopPropagation } from "../../../utils";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import type { ConfigurationInput, MinimalResourceGroup, OnlyUserSuppliedValuesAreShownToggle, ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
|
import type { ConfigurationInput, MinimalResourceGroup, OnlyUserSuppliedValuesAreShownToggle, ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
|
||||||
@ -20,7 +20,6 @@ import { Badge } from "../../badge";
|
|||||||
import { SubTitle } from "../../layout/sub-title";
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||||
import { ReactiveDuration } from "../../duration/reactive-duration";
|
import { ReactiveDuration } from "../../duration/reactive-duration";
|
||||||
import { HelmReleaseMenu } from "../release-menu";
|
|
||||||
import { Checkbox } from "../../checkbox";
|
import { Checkbox } from "../../checkbox";
|
||||||
import { MonacoEditor } from "../../monaco-editor";
|
import { MonacoEditor } from "../../monaco-editor";
|
||||||
import { Spinner } from "../../spinner";
|
import { Spinner } from "../../spinner";
|
||||||
@ -35,97 +34,86 @@ interface Dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NonInjectedReleaseDetailsContent = observer(({ model }: Dependencies & ReleaseDetailsContentProps) => {
|
const NonInjectedReleaseDetailsContent = observer(({ model }: Dependencies & ReleaseDetailsContentProps) => {
|
||||||
const isLoading = model.isLoading.get();
|
const loadingError = model.loadingError.get();
|
||||||
|
|
||||||
|
if (loadingError) {
|
||||||
|
return (
|
||||||
|
<div data-testid="helm-release-detail-error">
|
||||||
|
Failed to load release:
|
||||||
|
{" "}
|
||||||
|
{loadingError}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<div>
|
||||||
className={cssNames("ReleaseDetails", model.activeTheme)}
|
<DrawerItem name="Chart" className="chart">
|
||||||
usePortal={true}
|
<div className="flex gaps align-center">
|
||||||
open={true}
|
<span>{model.release.chart}</span>
|
||||||
title={isLoading ? "" : model.release.getName()}
|
|
||||||
onClose={model.close}
|
<Button
|
||||||
testIdForClose="close-helm-release-detail"
|
primary
|
||||||
toolbar={
|
label="Upgrade"
|
||||||
!isLoading && (
|
className="box right upgrade"
|
||||||
<HelmReleaseMenu
|
onClick={model.startUpgradeProcess}
|
||||||
release={model.release}
|
data-testid="helm-release-upgrade-button"
|
||||||
toolbar
|
|
||||||
hideDetails={model.close}
|
|
||||||
/>
|
/>
|
||||||
)
|
</div>
|
||||||
}
|
</DrawerItem>
|
||||||
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
|
<DrawerItem name="Updated">
|
||||||
primary
|
{`${model.release.getUpdated()} ago (${model.release.updated})`}
|
||||||
label="Upgrade"
|
</DrawerItem>
|
||||||
className="box right upgrade"
|
|
||||||
onClick={model.startUpgradeProcess}
|
|
||||||
data-testid="helm-release-upgrade-button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DrawerItem>
|
|
||||||
|
|
||||||
<DrawerItem name="Updated">
|
<DrawerItem name="Namespace">{model.release.getNs()}</DrawerItem>
|
||||||
{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="Version" onClick={stopPropagation}>
|
<DrawerItem
|
||||||
<div className="version flex gaps align-center">
|
name="Status"
|
||||||
<span>{model.release.getVersion()}</span>
|
className="status"
|
||||||
</div>
|
labelsOnly>
|
||||||
</DrawerItem>
|
<Badge
|
||||||
|
label={model.release.getStatus()}
|
||||||
|
className={kebabCase(model.release.getStatus())}
|
||||||
|
/>
|
||||||
|
</DrawerItem>
|
||||||
|
|
||||||
<DrawerItem
|
<ReleaseValues
|
||||||
name="Status"
|
configuration={model.configuration}
|
||||||
className="status"
|
onlyUserSuppliedValuesAreShown={
|
||||||
labelsOnly>
|
model.onlyUserSuppliedValuesAreShown
|
||||||
<Badge
|
}
|
||||||
label={model.release.getStatus()}
|
/>
|
||||||
className={kebabCase(model.release.getStatus())}
|
|
||||||
/>
|
|
||||||
</DrawerItem>
|
|
||||||
|
|
||||||
<ReleaseValues
|
<DrawerTitle>Notes</DrawerTitle>
|
||||||
configuration={model.configuration}
|
|
||||||
onlyUserSuppliedValuesAreShown={
|
|
||||||
model.onlyUserSuppliedValuesAreShown
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DrawerTitle>Notes</DrawerTitle>
|
{model.notes && <div className="notes">{model.notes}</div>}
|
||||||
|
|
||||||
{model.notes && <div className="notes">{model.notes}</div>}
|
<DrawerTitle>Resources</DrawerTitle>
|
||||||
|
|
||||||
<DrawerTitle>Resources</DrawerTitle>
|
{model.groupedResources.length > 0 && (
|
||||||
|
<div className="resources">
|
||||||
{model.groupedResources.length > 0 && (
|
{model.groupedResources.map((group) => (
|
||||||
<div className="resources">
|
<ResourceGroup key={group.kind} group={group} />
|
||||||
{model.groupedResources.map((group) => (
|
))}
|
||||||
<ResourceGroup key={group.kind} group={group} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ReleaseDetailsContent = withInjectables<Dependencies, ReleaseDetailsContentProps>(NonInjectedReleaseDetailsContent, {
|
export const ReleaseDetailsContent = withInjectables<Dependencies, ReleaseDetailsContentProps>(NonInjectedReleaseDetailsContent, {
|
||||||
getProps: (di, props) => ({
|
getPlaceholder: () => <Spinner center data-testid="helm-release-detail-content-spinner" />,
|
||||||
model: di.inject(releaseDetailsModelInjectable, props.targetRelease),
|
|
||||||
|
getProps: async (di, props) => ({
|
||||||
|
model: await di.inject(releaseDetailsModelInjectable, props.targetRelease),
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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 { observer } from "mobx-react";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import type { TargetHelmRelease } from "./target-helm-release.injectable";
|
||||||
|
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||||
|
import type { ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
|
||||||
|
import releaseDetailsModelInjectable from "./release-details-model/release-details-model.injectable";
|
||||||
|
import { HelmReleaseMenu } from "../release-menu";
|
||||||
|
|
||||||
|
interface ReleaseDetailsDrawerProps {
|
||||||
|
targetRelease: TargetHelmRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
model: ReleaseDetailsModel;
|
||||||
|
navigateToHelmReleases: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NonInjectedReleaseDetailsDrawerToolbar = observer(({
|
||||||
|
model,
|
||||||
|
navigateToHelmReleases,
|
||||||
|
}: Dependencies & ReleaseDetailsDrawerProps) => (
|
||||||
|
model.loadingError.get()
|
||||||
|
? null
|
||||||
|
: (
|
||||||
|
<HelmReleaseMenu
|
||||||
|
release={model.release}
|
||||||
|
toolbar
|
||||||
|
hideDetails={navigateToHelmReleases}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
export const ReleaseDetailsDrawerToolbar = withInjectables<
|
||||||
|
Dependencies,
|
||||||
|
ReleaseDetailsDrawerProps
|
||||||
|
>(NonInjectedReleaseDetailsDrawerToolbar, {
|
||||||
|
getPlaceholder: () => <></>,
|
||||||
|
|
||||||
|
getProps: async (di, props) => ({
|
||||||
|
model: await di.inject(releaseDetailsModelInjectable, props.targetRelease),
|
||||||
|
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Drawer } from "../../drawer";
|
||||||
|
import { cssNames } from "../../../utils";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import type { TargetHelmRelease } from "./target-helm-release.injectable";
|
||||||
|
import type { ActiveThemeType } from "../../../themes/active-type.injectable";
|
||||||
|
import activeThemeTypeInjectable from "../../../themes/active-type.injectable";
|
||||||
|
import { ReleaseDetailsContent } from "./release-details-content";
|
||||||
|
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||||
|
import { ReleaseDetailsDrawerToolbar } from "./release-details-drawer-toolbar";
|
||||||
|
|
||||||
|
interface ReleaseDetailsDrawerProps {
|
||||||
|
targetRelease: TargetHelmRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
activeThemeType: ActiveThemeType;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NonInjectedReleaseDetailsDrawer = observer(({
|
||||||
|
activeThemeType,
|
||||||
|
closeDrawer,
|
||||||
|
targetRelease,
|
||||||
|
}: Dependencies & ReleaseDetailsDrawerProps) => (
|
||||||
|
<Drawer
|
||||||
|
className={cssNames("ReleaseDetails", activeThemeType.get())}
|
||||||
|
usePortal={true}
|
||||||
|
open={true}
|
||||||
|
title={targetRelease.name}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
testIdForClose="close-helm-release-detail"
|
||||||
|
toolbar={<ReleaseDetailsDrawerToolbar targetRelease={targetRelease} />}
|
||||||
|
data-testid={`helm-release-details-for-${targetRelease.namespace}/${targetRelease.name}`}
|
||||||
|
>
|
||||||
|
<ReleaseDetailsContent targetRelease={targetRelease} />
|
||||||
|
</Drawer>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const ReleaseDetailsDrawer = withInjectables<
|
||||||
|
Dependencies,
|
||||||
|
ReleaseDetailsDrawerProps
|
||||||
|
>(NonInjectedReleaseDetailsDrawer, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
activeThemeType: di.inject(activeThemeTypeInjectable),
|
||||||
|
closeDrawer: di.inject(navigateToHelmReleasesInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -7,6 +7,7 @@ import type { HelmReleaseDto } from "../../../../../../common/k8s-api/endpoints/
|
|||||||
import callForHelmReleasesInjectable from "../../../call-for-helm-releases/call-for-helm-releases.injectable";
|
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 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";
|
import callForHelmReleaseDetailsInjectable from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||||
|
import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||||
|
|
||||||
export interface DetailedHelmRelease {
|
export interface DetailedHelmRelease {
|
||||||
release: HelmReleaseDto;
|
release: HelmReleaseDto;
|
||||||
@ -16,7 +17,7 @@ export interface DetailedHelmRelease {
|
|||||||
export type CallForHelmRelease = (
|
export type CallForHelmRelease = (
|
||||||
name: string,
|
name: string,
|
||||||
namespace: string
|
namespace: string
|
||||||
) => Promise<DetailedHelmRelease | undefined>;
|
) => Promise<AsyncResult<DetailedHelmRelease | undefined>>;
|
||||||
|
|
||||||
const callForHelmReleaseInjectable = getInjectable({
|
const callForHelmReleaseInjectable = getInjectable({
|
||||||
id: "call-for-helm-release",
|
id: "call-for-helm-release",
|
||||||
@ -36,10 +37,13 @@ const callForHelmReleaseInjectable = getInjectable({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!release) {
|
if (!release) {
|
||||||
return undefined;
|
return {
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: `Release ${name} didn't exist in ${namespace} namespace.`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { release, details };
|
return { callWasSuccessful: true, response: { release, details }};
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,16 +26,14 @@ import showSuccessNotificationInjectable from "../../../notifications/show-succe
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import createUpgradeChartTabInjectable from "../../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
|
import createUpgradeChartTabInjectable from "../../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
|
||||||
import type { HelmRelease } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
|
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 navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
|
|
||||||
import activeThemeInjectable from "../../../../themes/active.injectable";
|
import activeThemeInjectable from "../../../../themes/active.injectable";
|
||||||
|
|
||||||
const releaseDetailsModelInjectable = getInjectable({
|
const releaseDetailsModelInjectable = getInjectable({
|
||||||
id: "release-details-model",
|
id: "release-details-model",
|
||||||
|
|
||||||
instantiate: (di, targetRelease: TargetHelmRelease) => {
|
instantiate: async (di, targetRelease: TargetHelmRelease) => {
|
||||||
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
|
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
|
||||||
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
|
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
|
||||||
const activeTheme = di.inject(activeThemeInjectable);
|
const activeTheme = di.inject(activeThemeInjectable);
|
||||||
@ -45,7 +43,6 @@ const releaseDetailsModelInjectable = getInjectable({
|
|||||||
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
|
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
|
||||||
const createUpgradeChartTab = di.inject(createUpgradeChartTabInjectable);
|
const createUpgradeChartTab = di.inject(createUpgradeChartTabInjectable);
|
||||||
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
|
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
|
||||||
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
|
|
||||||
|
|
||||||
const model = new ReleaseDetailsModel({
|
const model = new ReleaseDetailsModel({
|
||||||
callForHelmRelease,
|
callForHelmRelease,
|
||||||
@ -60,56 +57,52 @@ const releaseDetailsModelInjectable = getInjectable({
|
|||||||
navigateToHelmReleases,
|
navigateToHelmReleases,
|
||||||
});
|
});
|
||||||
|
|
||||||
const load = withOrphanPromise(model.load);
|
await model.load();
|
||||||
|
|
||||||
// TODO: Reorganize Drawer to allow setting of header-bar in children to make "getPlaceholder" from injectable usable.
|
|
||||||
load();
|
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
},
|
},
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.keyedSingleton({
|
lifecycle: lifecycleEnum.keyedSingleton({
|
||||||
getInstanceKey: (di, release: TargetHelmRelease) =>
|
getInstanceKey: (di, release: TargetHelmRelease) => `${release.namespace}/${release.name}`,
|
||||||
`${release.namespace}/${release.name}`,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default releaseDetailsModelInjectable;
|
export default releaseDetailsModelInjectable;
|
||||||
|
|
||||||
export interface OnlyUserSuppliedValuesAreShownToggle {
|
export interface OnlyUserSuppliedValuesAreShownToggle {
|
||||||
value: IObservableValue<boolean>;
|
readonly value: IObservableValue<boolean>;
|
||||||
toggle: () => Promise<void>;
|
toggle: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigurationInput {
|
export interface ConfigurationInput {
|
||||||
nonSavedValue: IObservableValue<string>;
|
readonly nonSavedValue: IObservableValue<string>;
|
||||||
isLoading: IObservableValue<boolean>;
|
readonly isLoading: IObservableValue<boolean>;
|
||||||
isSaving: IObservableValue<boolean>;
|
readonly isSaving: IObservableValue<boolean>;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
save: () => Promise<void>;
|
save: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
|
readonly targetRelease: TargetHelmRelease;
|
||||||
|
readonly activeTheme: IComputedValue<LensTheme>;
|
||||||
callForHelmRelease: CallForHelmRelease;
|
callForHelmRelease: CallForHelmRelease;
|
||||||
targetRelease: TargetHelmRelease;
|
|
||||||
activeTheme: IComputedValue<LensTheme>;
|
|
||||||
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
|
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
|
||||||
getResourceDetailsUrl: GetResourceDetailsUrl;
|
getResourceDetailsUrl: GetResourceDetailsUrl;
|
||||||
updateRelease: CallForHelmReleaseUpdate;
|
updateRelease: CallForHelmReleaseUpdate;
|
||||||
showCheckedErrorNotification: ShowCheckedErrorNotification;
|
showCheckedErrorNotification: ShowCheckedErrorNotification;
|
||||||
showSuccessNotification: ShowNotification;
|
showSuccessNotification: ShowNotification;
|
||||||
createUpgradeChartTab: (release: HelmRelease) => string;
|
createUpgradeChartTab: (release: HelmRelease) => string;
|
||||||
navigateToHelmReleases: NavigateToHelmReleases;
|
navigateToHelmReleases: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReleaseDetailsModel {
|
export class ReleaseDetailsModel {
|
||||||
id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
|
readonly id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
|
||||||
|
|
||||||
constructor(private dependencies: Dependencies) {}
|
constructor(private readonly dependencies: Dependencies) {}
|
||||||
|
|
||||||
private detailedRelease = observable.box<DetailedHelmRelease | undefined>();
|
private readonly detailedRelease = observable.box<DetailedHelmRelease | undefined>();
|
||||||
|
|
||||||
readonly isLoading = observable.box(false);
|
readonly loadingError = observable.box<string>();
|
||||||
|
|
||||||
readonly configuration: ConfigurationInput = {
|
readonly configuration: ConfigurationInput = {
|
||||||
nonSavedValue: observable.box(""),
|
nonSavedValue: observable.box(""),
|
||||||
@ -176,26 +169,26 @@ export class ReleaseDetailsModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
load = async () => {
|
load = async () => {
|
||||||
runInAction(() => {
|
|
||||||
this.isLoading.set(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { name, namespace } = this.dependencies.targetRelease;
|
const { name, namespace } = this.dependencies.targetRelease;
|
||||||
|
|
||||||
const detailedRelease = await this.dependencies.callForHelmRelease(
|
const result = await this.dependencies.callForHelmRelease(
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loadingError.set(result.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.detailedRelease.set(detailedRelease);
|
this.detailedRelease.set(result.response);
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.loadConfiguration();
|
await this.loadConfiguration();
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.isLoading.set(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private loadConfiguration = async () => {
|
private loadConfiguration = async () => {
|
||||||
@ -269,7 +262,7 @@ export class ReleaseDetailsModel {
|
|||||||
startUpgradeProcess = () => {
|
startUpgradeProcess = () => {
|
||||||
this.dependencies.createUpgradeChartTab(this.release);
|
this.dependencies.createUpgradeChartTab(this.release);
|
||||||
|
|
||||||
this.close();
|
this.dependencies.navigateToHelmReleases();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,16 +49,19 @@
|
|||||||
.TableCell {
|
.TableCell {
|
||||||
text-overflow: unset;
|
text-overflow: unset;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
min-width: 100px;
|
|
||||||
|
|
||||||
&.name {
|
&.name {
|
||||||
flex-basis: auto;
|
flex-grow: 3;
|
||||||
flex-grow: 0;
|
|
||||||
width: 230px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.volume {
|
&.namespace {
|
||||||
flex-basis: 30%;
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.age {
|
||||||
|
flex-basis: 100px;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ import { observer } from "mobx-react";
|
|||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
|
||||||
import type { IComputedValue } from "mobx";
|
import type { IComputedValue } from "mobx";
|
||||||
import { ReleaseDetailsContent } from "./release-details-content";
|
|
||||||
import type { TargetHelmRelease } from "./target-helm-release.injectable";
|
import type { TargetHelmRelease } from "./target-helm-release.injectable";
|
||||||
import targetHelmReleaseInjectable from "./target-helm-release.injectable";
|
import targetHelmReleaseInjectable from "./target-helm-release.injectable";
|
||||||
|
import { ReleaseDetailsDrawer } from "./release-details-drawer";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
targetRelease: IComputedValue<
|
targetRelease: IComputedValue<
|
||||||
@ -25,7 +25,7 @@ const NonInjectedReleaseDetails = observer(
|
|||||||
({ targetRelease }: Dependencies) => {
|
({ targetRelease }: Dependencies) => {
|
||||||
const release = targetRelease.get();
|
const release = targetRelease.get();
|
||||||
|
|
||||||
return release ? <ReleaseDetailsContent targetRelease={release} /> : null;
|
return release ? <ReleaseDetailsDrawer targetRelease={release} /> : null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
23
src/renderer/themes/active-type.injectable.ts
Normal file
23
src/renderer/themes/active-type.injectable.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import type { LensThemeType } from "./store";
|
||||||
|
import themeStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
export type ActiveThemeType = IComputedValue<LensThemeType>;
|
||||||
|
|
||||||
|
const activeThemeTypeInjectable = getInjectable({
|
||||||
|
id: "active-theme-type",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const store = di.inject(themeStoreInjectable);
|
||||||
|
|
||||||
|
return computed(() => store.activeTheme.type);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default activeThemeTypeInjectable;
|
||||||
@ -17,10 +17,10 @@ import type { ReadonlyDeep } from "type-fest/source/readonly-deep";
|
|||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
export type ThemeId = string;
|
export type ThemeId = string;
|
||||||
|
export type LensThemeType = "dark" | "light";
|
||||||
export interface LensTheme {
|
export interface LensTheme {
|
||||||
name: string;
|
name: string;
|
||||||
type: "dark" | "light";
|
type: LensThemeType;
|
||||||
colors: Record<string, string>;
|
colors: Record<string, string>;
|
||||||
description: string;
|
description: string;
|
||||||
author: string;
|
author: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user