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-dom"; 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, { cacheTime: 0 }); const [showTestsResults, setShowTestResults] = useState(false); const { setShowErrorModal } = useAlertError(); const { mutate: runTests, isLoading: 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 (

{chart}

{isReconfigureModalOpen && ( { setIsReconfigureModalOpen(false); }} latestRevision={latestRevision} /> )} {latestVerData?.[0]?.isSuggestedRepo ? ( { navigate( `/repository?add_repo=true&repo_url=${latestVerData[0].urls[0]}&repo_name=${latestVerData[0].repository}` ); }} className="underline text-sm cursor-pointer text-blue-600" > Add repository for it: {latestVerData[0].repository} ) : ( refetchLatestVersion()} className="underline cursor-pointer text-xs" > Check for new version )}
{release.has_tests ? ( <> {" "} setShowTestResults(false)} > {isRunningTests ? (
Waiting for completion..
) : ( displayTestResults() )}
{" "} ) : null}
); }; 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, isLoading: 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, { enabled: isOpen, }); const uninstallMutation = useMutation( ["uninstall", namespace, chart], () => 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.isLoading, }, ]} containerClassNames="w-[800px]" >
Following resources will be deleted from the cluster:
{resources?.map((resource) => (
{resource.kind} {resource.metadata.name}
))}
) : null} ); };