import { useEffect, useRef, useState } from "react";
import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-slim.js";
import {
BsPencil,
BsTrash3,
BsHourglassSplit,
BsArrowRepeat,
BsArrowUp,
BsCheckCircle,
} from "react-icons/bs";
import { Release, ReleaseRevision } from "../../data/types";
import StatusLabel, { DeploymentStatus } from "../common/StatusLabel";
import { useNavigate, useParams, useSearchParams } from "react-router";
import {
useGetReleaseInfoByType,
useGetLatestVersion,
useGetResources,
useRollbackRelease,
useTestRelease,
} from "../../API/releases";
import RevisionDiff from "./RevisionDiff";
import RevisionResource from "./RevisionResource";
import Tabs from "../Tabs";
import { type UseQueryResult, useMutation } from "@tanstack/react-query";
import Modal, { ModalButtonStyle } from "../modal/Modal";
import Spinner from "../Spinner";
import useAlertError from "../../hooks/useAlertError";
import Button from "../Button";
import { InstallReleaseChartModal } from "../modal/InstallChartModal/InstallReleaseChartModal";
import { diffConfiguration, isNewerVersion } from "../../utils";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import apiService from "../../API/apiService";
type RevisionTagProps = {
caption: string;
text: string;
};
type RevisionDetailsProps = {
release: Release;
installedRevision: ReleaseRevision;
isLatest: boolean;
latestRevision: number;
};
export default function RevisionDetails({
release,
installedRevision,
isLatest,
latestRevision,
}: RevisionDetailsProps) {
const [searchParams] = useSearchParams();
const revisionTabs = [
{
value: "resources",
label: "Resources",
content: ,
},
{
value: "manifests",
label: "Manifests",
content: ,
},
{
value: "values",
label: "Values",
content: (
),
},
{
value: "notes",
label: "Notes",
content: ,
},
];
const { context, namespace, chart } = useParams();
const tab = searchParams.get("tab");
const selectedTab =
revisionTabs.find((t) => t.value === tab) || revisionTabs[0];
const [isReconfigureModalOpen, setIsReconfigureModalOpen] = useState(false);
const {
data: latestVerData,
refetch: refetchLatestVersion,
isLoading: isLoadingLatestVersion,
isRefetching: isRefetchingLatestVersion,
} = useGetLatestVersion(release.chart_name);
const [showTestsResults, setShowTestResults] = useState(false);
const { setShowErrorModal } = useAlertError();
const {
mutate: runTests,
isPending: isRunningTests,
data: testResults,
} = useTestRelease({
onError: (error) => {
setShowTestResults(false);
setShowErrorModal({
title: "Failed to run tests for chart " + chart,
msg: (error as Error).message,
});
console.error("Failed to execute test for chart", error);
},
});
const handleRunTests = () => {
if (!namespace || !chart) {
setShowErrorModal({
title: "Missing data to run test",
msg: "Missing chart and/or namespace",
});
return;
}
try {
runTests({
ns: namespace,
name: chart,
});
} catch (error: unknown) {
setShowErrorModal({
title: "Test failed to run",
msg: (error as Error).message,
});
}
setShowTestResults(true);
};
const displayTestResults = () => {
if (!testResults || (testResults as []).length === 0) {
return (
Tests executed successfully
Empty response from API
);
} else {
return (
{(testResults as string).split("\n").map((line, index) => (
{line}
))}
);
}
};
const Header = () => {
const navigate = useNavigate();
return (
);
};
const canUpgrade = !latestVerData?.[0]?.version
? false
: isNewerVersion(installedRevision.chart_ver, latestVerData?.[0]?.version);
return (
Revision #{release.revision}
{new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
}).format(new Date(release.updated))}
{release.description}
);
}
function RevisionTag({ caption, text }: RevisionTagProps) {
return (
{caption}:
{text}
);
}
const Rollback = ({
release,
installedRevision,
}: {
release: Release;
installedRevision: ReleaseRevision;
}) => {
const { chart, namespace, revision } = useParams();
const navigate = useNavigateWithSearchParams();
const [showRollbackDiff, setShowRollbackDiff] = useState(false);
const revisionInt = parseInt(revision || "", 10);
const { mutate: rollbackRelease, isPending: isRollingBackRelease } =
useRollbackRelease({
onSuccess: () => {
navigate(
`/${namespace}/${chart}/installed/revision/${revisionInt + 1}`
);
window.location.reload();
},
});
const handleRollback = () => {
setShowRollbackDiff(true);
};
if (!chart || !namespace || !revision) {
return null;
}
if (release.revision <= 1) return null;
const rollbackRevision =
installedRevision.revision === release.revision
? installedRevision.revision - 1
: revisionInt;
const rollbackTitle = (
Rollback {chart} from revision{" "}
{installedRevision.revision} to {rollbackRevision}
);
const RollbackModal = () => {
const response = useGetReleaseInfoByType(
{
chart,
namespace,
revision: rollbackRevision.toString(),
tab: "manifests",
},
`&revisionDiff=${installedRevision.revision}`
);
return (
setShowRollbackDiff(false)}
containerClassNames="w-4/5"
actions={[
{
id: "1",
callback: () => {
rollbackRelease({
ns: namespace,
name: String(chart),
revision: release.revision,
});
},
variant: ModalButtonStyle.info,
isLoading: isRollingBackRelease,
text: isRollingBackRelease ? "Rolling back" : "Confirm",
},
]}
>
);
};
const RollbackModalContent = ({
dataResponse,
}: {
dataResponse: UseQueryResult;
}) => {
const {
data,
isLoading,
isSuccess: fetchedDataSuccessfully,
} = dataResponse;
const diffElement = useRef(null);
useEffect(() => {
if (data && fetchedDataSuccessfully && diffElement?.current) {
const diff2htmlUi = new Diff2HtmlUI(
diffElement.current,
data,
diffConfiguration
);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
}
}, [data, isLoading, fetchedDataSuccessfully]);
return (
{isLoading ? (
Loading changes that will happen to cluster
) : data ? (
Following changes will happen to cluster:
) : (
No changes will happen to cluster
)}
);
};
return (
<>
{showRollbackDiff && }
>
);
};
const Uninstall = () => {
const [isOpen, setIsOpen] = useState(false);
const { namespace = "", chart = "" } = useParams();
const { data: resources } = useGetResources(namespace, chart, isOpen);
const uninstallMutation = useMutation({
mutationKey: ["uninstall", namespace, chart],
mutationFn: () =>
apiService.fetchWithDefaults(
"/api/helm/releases/" + namespace + "/" + chart,
{
method: "delete",
}
),
onSuccess: () => {
window.location.href = "/";
},
});
const uninstallTitle = (
Uninstall {chart} from namespace{" "}
{namespace}
);
return (
<>
{resources?.length ? (
setIsOpen(false)}
actions={[
{
id: "1",
callback: uninstallMutation.mutate,
variant: ModalButtonStyle.info,
isLoading: uninstallMutation.isPending,
},
]}
containerClassNames="w-[800px]"
>
Following resources will be deleted from the cluster:
{resources?.map((resource) => (
{resource.kind}
{resource.metadata.name}
))}
) : null}
>
);
};