mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-26 06:18:04 +00:00
Add a "Force upgrade" checkbox in the upgrade modal footer that passes the --force flag to helm upgrade, causing resources to be deleted and recreated. Also fix the version selector flashing the URL input while loading by showing a spinner. Closes #505 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
322 lines
9.6 KiB
TypeScript
322 lines
9.6 KiB
TypeScript
import { useMutation } from "@tanstack/react-query";
|
|
import {
|
|
useEffect,
|
|
useEffectEvent,
|
|
useMemo,
|
|
useState,
|
|
lazy,
|
|
Suspense,
|
|
} from "react";
|
|
import { useParams } from "react-router";
|
|
|
|
import { BsPencil, BsX } from "react-icons/bs";
|
|
|
|
import apiService from "../../../API/apiService";
|
|
import type { LatestChartVersion } from "../../../API/interfaces";
|
|
import {
|
|
type VersionData,
|
|
useChartReleaseValues,
|
|
useGetReleaseManifest,
|
|
useGetVersions,
|
|
useVersionData,
|
|
} from "../../../API/releases";
|
|
import { useChartRepoValues } from "../../../API/repositories";
|
|
import { useDiffData } from "../../../API/shared";
|
|
import type { InstallChartModalProps } from "../../../data/types";
|
|
import useCustomSearchParams from "../../../hooks/useCustomSearchParams";
|
|
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
|
|
import { isNoneEmptyArray } from "../../../utils";
|
|
import Spinner from "../../Spinner";
|
|
import Modal, { ModalButtonStyle } from "../Modal";
|
|
import { GeneralDetails } from "./GeneralDetails";
|
|
import { VersionToInstall } from "./VersionToInstall";
|
|
|
|
import { InstallUpgradeTitle } from "./InstallUpgradeTitle";
|
|
|
|
const DefinedValues = lazy(() => import("./DefinedValues"));
|
|
const ManifestDiff = lazy(() => import("./ManifestDiff"));
|
|
|
|
export const InstallReleaseChartModal = ({
|
|
isOpen,
|
|
onClose,
|
|
chartName,
|
|
currentlyInstalledChartVersion,
|
|
isUpgrade = false,
|
|
latestRevision,
|
|
}: InstallChartModalProps) => {
|
|
const navigate = useNavigateWithSearchParams();
|
|
const [userValues, setUserValues] = useState<string>("");
|
|
const [installError, setInstallError] = useState("");
|
|
const [forceUpgrade, setForceUpgrade] = useState(false);
|
|
|
|
const {
|
|
namespace: queryNamespace,
|
|
chart: _releaseName,
|
|
context: selectedCluster,
|
|
} = useParams();
|
|
const { searchParamsObject } = useCustomSearchParams();
|
|
const { filteredNamespace } = searchParamsObject;
|
|
const [namespace, setNamespace] = useState(queryNamespace || "");
|
|
const [releaseName, setReleaseName] = useState(_releaseName || "");
|
|
|
|
const {
|
|
error: versionsError,
|
|
data: _versions = [],
|
|
isSuccess,
|
|
isLoading: isLoadingVersions,
|
|
} = useGetVersions(chartName);
|
|
|
|
const [selectedVersionData, setSelectedVersionData] = useState<VersionData>();
|
|
|
|
const [versions, setVersions] = useState<
|
|
Array<LatestChartVersion & { isChartVersion: boolean }>
|
|
>([]);
|
|
|
|
const onSuccess = useEffectEvent(() => {
|
|
const empty = { version: "", repository: "", urls: [] };
|
|
setSelectedVersionData(_versions[0] ?? empty);
|
|
setVersions(
|
|
_versions?.map((v) => ({
|
|
...v,
|
|
isChartVersion: v.version === currentlyInstalledChartVersion,
|
|
}))
|
|
);
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isSuccess && _versions.length) {
|
|
onSuccess();
|
|
}
|
|
}, [isSuccess, _versions]);
|
|
|
|
const selectedVersion = selectedVersionData?.version || "";
|
|
const selectedRepo = selectedVersionData?.repository || "";
|
|
|
|
const [chartURL, setChartURL] = useState("");
|
|
const [useURLMode, setUseURLMode] = useState(false);
|
|
|
|
const repoChartAddress = useMemo(() => {
|
|
if (!selectedVersionData || !selectedVersionData.repository) return "";
|
|
|
|
return selectedVersionData.urls?.[0]?.startsWith("file://")
|
|
? selectedVersionData.urls[0]
|
|
: `${selectedVersionData.repository}/${chartName}`;
|
|
}, [selectedVersionData, chartName]);
|
|
|
|
const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
|
|
|
|
// the original chart values
|
|
const { data: chartValues = "" } = useChartRepoValues({
|
|
version: selectedVersion,
|
|
chart: chartAddress,
|
|
});
|
|
|
|
// The user defined values (if any we're set)
|
|
const { data: releaseValues = "", isLoading: loadingReleaseValues } =
|
|
useChartReleaseValues({
|
|
namespace,
|
|
release: String(releaseName),
|
|
revision: latestRevision ? latestRevision : undefined,
|
|
});
|
|
|
|
// This hold the selected version manifest, we use it for the diff
|
|
const { data: selectedVerData = {}, error: selectedVerDataError } =
|
|
useVersionData({
|
|
version: selectedVersion,
|
|
userValues,
|
|
chartAddress,
|
|
releaseValues,
|
|
namespace,
|
|
releaseName,
|
|
});
|
|
|
|
const { data: currentVerManifest, error: currentVerManifestError } =
|
|
useGetReleaseManifest({
|
|
namespace,
|
|
chartName: _releaseName || "",
|
|
});
|
|
|
|
const {
|
|
data: diffData,
|
|
isLoading: isLoadingDiff,
|
|
error: diffError,
|
|
} = useDiffData({
|
|
selectedRepo,
|
|
versionsError: versionsError as unknown as string, // TODO fix it
|
|
currentVerManifest: currentVerManifest as unknown as string, // TODO fix it
|
|
selectedVerData,
|
|
chart: chartAddress,
|
|
});
|
|
|
|
// Confirm method (install)
|
|
const setReleaseVersionMutation = useMutation<VersionData, Error>({
|
|
mutationKey: [
|
|
"setVersion",
|
|
namespace,
|
|
releaseName,
|
|
selectedVersion,
|
|
selectedRepo,
|
|
selectedCluster,
|
|
chartAddress,
|
|
],
|
|
mutationFn: async () => {
|
|
setInstallError("");
|
|
const formData = new FormData();
|
|
formData.append("preview", "false");
|
|
if (chartAddress) {
|
|
formData.append("chart", chartAddress);
|
|
}
|
|
formData.append("version", selectedVersion || "");
|
|
formData.append("values", userValues || releaseValues || ""); // if userValues is empty, we use the release values
|
|
if (forceUpgrade) {
|
|
formData.append("force", "true");
|
|
}
|
|
const url = `/api/helm/releases/${
|
|
namespace ? namespace : "default"
|
|
}/${releaseName}`;
|
|
|
|
return await apiService.fetchWithSafeDefaults<VersionData>({
|
|
url,
|
|
options: {
|
|
method: "post",
|
|
body: formData,
|
|
},
|
|
fallback: { version: "", urls: [""] },
|
|
});
|
|
},
|
|
onSuccess: async (response) => {
|
|
onClose();
|
|
setSelectedVersionData({ version: "", urls: [] }); //cleanup
|
|
await navigate(
|
|
`/${
|
|
namespace ? namespace : "default"
|
|
}/${releaseName}/installed/revision/${response.version}`
|
|
);
|
|
window.location.reload();
|
|
},
|
|
onError: (error) => {
|
|
setInstallError(error?.message || "Failed to update");
|
|
},
|
|
});
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={() => {
|
|
setSelectedVersionData({ version: "", urls: [] });
|
|
setUserValues(releaseValues);
|
|
onClose();
|
|
}}
|
|
title={
|
|
<InstallUpgradeTitle
|
|
isUpgrade={isUpgrade}
|
|
releaseValues={isUpgrade || !!releaseValues}
|
|
chartName={chartName}
|
|
/>
|
|
}
|
|
containerClassNames="w-full text-2xl h-2/3"
|
|
bottomContent={
|
|
isUpgrade ? (
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={forceUpgrade}
|
|
onChange={(e) => setForceUpgrade(e.target.checked)}
|
|
/>
|
|
Force upgrade
|
|
</label>
|
|
) : undefined
|
|
}
|
|
actions={[
|
|
{
|
|
id: "1",
|
|
callback: setReleaseVersionMutation.mutate,
|
|
variant: ModalButtonStyle.info,
|
|
isLoading: setReleaseVersionMutation.isPending,
|
|
disabled:
|
|
loadingReleaseValues ||
|
|
isLoadingDiff ||
|
|
setReleaseVersionMutation.isPending,
|
|
},
|
|
]}
|
|
>
|
|
{isLoadingVersions ? (
|
|
<Spinner />
|
|
) : !useURLMode && versions && isNoneEmptyArray(versions) ? (
|
|
<div className="flex items-center gap-2">
|
|
<VersionToInstall
|
|
versions={versions}
|
|
initialVersion={selectedVersionData}
|
|
onSelectVersion={setSelectedVersionData}
|
|
showCurrentVersion
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
|
|
title="Switch to URL"
|
|
onClick={() => setUseURLMode(true)}
|
|
>
|
|
<BsPencil className="text-lg" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-end gap-2">
|
|
<div className="flex-1">
|
|
<h4 className="text-lg">Chart URL:</h4>
|
|
<input
|
|
className="w-full rounded-sm border border-1 border-gray-300 bg-white px-2 py-1 text-lg"
|
|
value={chartURL}
|
|
onChange={(e) => setChartURL(e.target.value)}
|
|
placeholder="oci://registry-1.docker.io/example/chart"
|
|
/>
|
|
</div>
|
|
{versions && isNoneEmptyArray(versions) && (
|
|
<button
|
|
type="button"
|
|
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
|
|
title="Switch to repository"
|
|
onClick={() => {
|
|
setUseURLMode(false);
|
|
setChartURL("");
|
|
}}
|
|
>
|
|
<BsX className="text-2xl" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<GeneralDetails
|
|
releaseName={releaseName}
|
|
disabled
|
|
namespace={namespace ? namespace : filteredNamespace}
|
|
onReleaseNameInput={setReleaseName}
|
|
onNamespaceInput={setNamespace}
|
|
/>
|
|
|
|
<Suspense fallback={<Spinner />}>
|
|
<DefinedValues
|
|
initialValue={releaseValues}
|
|
onUserValuesChange={(values: string) => setUserValues(values)}
|
|
chartValues={chartValues}
|
|
loading={loadingReleaseValues}
|
|
/>
|
|
</Suspense>
|
|
|
|
<Suspense fallback={<Spinner />}>
|
|
<ManifestDiff
|
|
diff={diffData as string}
|
|
isLoading={isLoadingDiff}
|
|
error={
|
|
(currentVerManifestError as unknown as string) || // TODO fix it
|
|
(selectedVerDataError as unknown as string) ||
|
|
(diffError as unknown as string) ||
|
|
installError ||
|
|
(versionsError as unknown as string)
|
|
}
|
|
/>
|
|
</Suspense>
|
|
</Modal>
|
|
);
|
|
};
|