mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-28 23:38:04 +00:00
Rename frontend directory (#472)
* Rename directory * Cleanup * Recover lost images * remove lint
This commit is contained in:
533
frontend/src/components/revision/RevisionDetails.tsx
Normal file
533
frontend/src/components/revision/RevisionDetails.tsx
Normal file
@@ -0,0 +1,533 @@
|
||||
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 { 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: <RevisionResource isLatest={isLatest} />,
|
||||
},
|
||||
{
|
||||
value: "manifests",
|
||||
label: "Manifests",
|
||||
content: <RevisionDiff latestRevision={latestRevision} />,
|
||||
},
|
||||
{
|
||||
value: "values",
|
||||
label: "Values",
|
||||
content: (
|
||||
<RevisionDiff
|
||||
latestRevision={latestRevision}
|
||||
includeUserDefineOnly={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "notes",
|
||||
label: "Notes",
|
||||
content: <RevisionDiff latestRevision={latestRevision} />,
|
||||
},
|
||||
];
|
||||
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: any) {
|
||||
setShowErrorModal({
|
||||
title: "Test failed to run",
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
setShowTestResults(true);
|
||||
};
|
||||
|
||||
const displayTestResults = () => {
|
||||
if (!testResults || (testResults as []).length === 0) {
|
||||
return (
|
||||
<div>
|
||||
Tests executed successfully
|
||||
<br />
|
||||
<br />
|
||||
<pre>Empty response from API</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
{(testResults as string).split("\n").map((line, index) => (
|
||||
<div key={index} className="mb-2">
|
||||
{line}
|
||||
<br />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<header className="flex flex-wrap justify-between">
|
||||
<h1 className=" text-3xl font-semibold float-left mb-1 font-roboto-slab">
|
||||
{chart}
|
||||
</h1>
|
||||
<div className="flex flex-row flex-wrap gap-3 float-right h-fit">
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
className="flex justify-center items-center gap-2 min-w-[150px] text-sm font-semibold disabled:bg-gray-200"
|
||||
onClick={() => setIsReconfigureModalOpen(true)}
|
||||
disabled={isLoadingLatestVersion || isRefetchingLatestVersion}
|
||||
>
|
||||
{isLoadingLatestVersion || isRefetchingLatestVersion ? (
|
||||
<>
|
||||
<BsHourglassSplit />
|
||||
Checking...
|
||||
</>
|
||||
) : canUpgrade ? (
|
||||
<>
|
||||
<BsArrowUp />
|
||||
Upgrade to {latestVerData?.[0]?.version}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BsPencil />
|
||||
Reconfigure
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isReconfigureModalOpen && (
|
||||
<InstallReleaseChartModal
|
||||
isOpen={isReconfigureModalOpen}
|
||||
chartName={release.chart_name}
|
||||
currentlyInstalledChartVersion={installedRevision.chart_ver}
|
||||
latestVersion={latestVerData?.[0]?.version}
|
||||
isUpgrade={canUpgrade}
|
||||
onClose={() => {
|
||||
setIsReconfigureModalOpen(false);
|
||||
}}
|
||||
latestRevision={latestRevision}
|
||||
/>
|
||||
)}
|
||||
{latestVerData?.[0]?.isSuggestedRepo ? (
|
||||
<span
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/${context}/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}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
onClick={() => refetchLatestVersion()}
|
||||
className="underline cursor-pointer text-xs"
|
||||
>
|
||||
Check for new version
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Rollback release={release} installedRevision={installedRevision} />
|
||||
{release.has_tests ? (
|
||||
<>
|
||||
{" "}
|
||||
<Button
|
||||
onClick={handleRunTests}
|
||||
className="flex items-center gap-2 h-1/2 text-sm font-semibold"
|
||||
>
|
||||
<BsCheckCircle />
|
||||
Run tests
|
||||
</Button>
|
||||
<Modal
|
||||
containerClassNames="w-4/5"
|
||||
title="Test results"
|
||||
isOpen={showTestsResults}
|
||||
onClose={() => setShowTestResults(false)}
|
||||
>
|
||||
{isRunningTests ? (
|
||||
<div className="flex mr-2 items-center">
|
||||
<Spinner /> Waiting for completion..
|
||||
</div>
|
||||
) : (
|
||||
displayTestResults()
|
||||
)}
|
||||
</Modal>{" "}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Uninstall />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const canUpgrade = !latestVerData?.[0]?.version
|
||||
? false
|
||||
: isNewerVersion(installedRevision.chart_ver, latestVerData?.[0]?.version);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col px-16 pt-5 gap-3">
|
||||
<StatusLabel status={release.status} />
|
||||
<Header />
|
||||
<div className="flex flex-row gap-6 text-sm -mt-4">
|
||||
<span>
|
||||
Revision <span className="font-semibold">#{release.revision}</span>
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{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))}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<RevisionTag caption="chart version" text={release.chart} />
|
||||
<RevisionTag
|
||||
caption="app version"
|
||||
text={release.app_version || "N/A"}
|
||||
/>
|
||||
<RevisionTag caption="namespace" text={namespace ?? ""} />
|
||||
<RevisionTag caption="cluster" text={context ?? ""} />
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`text-sm ${
|
||||
release.status === DeploymentStatus.FAILED ? "text-red-600" : ""
|
||||
}`}
|
||||
>
|
||||
{release.description}
|
||||
</span>
|
||||
<Tabs tabs={revisionTabs} selectedTab={selectedTab} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionTag({ caption, text }: RevisionTagProps) {
|
||||
return (
|
||||
<span className="bg-revision p-1 rounded px-2 text-sm">
|
||||
<span>{caption}:</span>
|
||||
<span className="font-bold"> {text}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const Rollback = ({
|
||||
release,
|
||||
installedRevision,
|
||||
}: {
|
||||
release: Release;
|
||||
installedRevision: ReleaseRevision;
|
||||
}) => {
|
||||
const { chart, namespace, revision, context } = useParams();
|
||||
const navigate = useNavigateWithSearchParams();
|
||||
|
||||
const [showRollbackDiff, setShowRollbackDiff] = useState(false);
|
||||
const revisionInt = parseInt(revision || "", 10);
|
||||
|
||||
const { mutate: rollbackRelease, isLoading: isRollingBackRelease } =
|
||||
useRollbackRelease({
|
||||
onSuccess: () => {
|
||||
navigate(
|
||||
`/${context}/${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 = (
|
||||
<div className="font-semibold text-lg">
|
||||
Rollback <span className="text-red-500">{chart}</span> from revision{" "}
|
||||
{installedRevision.revision} to {rollbackRevision}
|
||||
</div>
|
||||
);
|
||||
|
||||
const RollbackModal = () => {
|
||||
const response = useGetReleaseInfoByType(
|
||||
{
|
||||
chart,
|
||||
namespace,
|
||||
revision: rollbackRevision.toString(),
|
||||
tab: "manifests",
|
||||
},
|
||||
`&revisionDiff=${installedRevision.revision}`
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={rollbackTitle}
|
||||
isOpen={showRollbackDiff}
|
||||
onClose={() => setShowRollbackDiff(false)}
|
||||
containerClassNames="w-4/5"
|
||||
actions={[
|
||||
{
|
||||
id: "1",
|
||||
callback: () => {
|
||||
rollbackRelease({
|
||||
ns: namespace as string,
|
||||
name: String(chart),
|
||||
revision: release.revision,
|
||||
});
|
||||
},
|
||||
variant: ModalButtonStyle.info,
|
||||
isLoading: isRollingBackRelease,
|
||||
text: isRollingBackRelease ? "Rolling back" : "Confirm",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<RollbackModalContent dataResponse={response} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const RollbackModalContent = ({ dataResponse }: { dataResponse: any }) => {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isSuccess: fetchedDataSuccessfully,
|
||||
} = dataResponse;
|
||||
const diffElement = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && fetchedDataSuccessfully && diffElement?.current) {
|
||||
const diff2htmlUi = new Diff2HtmlUI(
|
||||
diffElement.current,
|
||||
data,
|
||||
diffConfiguration
|
||||
);
|
||||
diff2htmlUi.draw();
|
||||
diff2htmlUi.highlightCode();
|
||||
}
|
||||
}, [data, isLoading, fetchedDataSuccessfully]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex gap-2 text-sm">
|
||||
<Spinner />
|
||||
<p>Loading changes that will happen to cluster</p>
|
||||
</div>
|
||||
) : data ? (
|
||||
<p className="text-sm">Following changes will happen to cluster:</p>
|
||||
) : (
|
||||
<p className="text-base">No changes will happen to cluster</p>
|
||||
)}
|
||||
<div className="relative leading-5" ref={diffElement} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleRollback}
|
||||
className="flex items-center gap-2 h-1/2 text-sm font-semibold"
|
||||
>
|
||||
<BsArrowRepeat />
|
||||
Rollback to #{rollbackRevision}
|
||||
</Button>
|
||||
{showRollbackDiff && <RollbackModal />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<div className="font-semibold text-lg">
|
||||
Uninstall <span className="text-red-500">{chart}</span> from namespace{" "}
|
||||
<span className="text-red-500">{namespace}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="flex items-center gap-2 hover:bg-red-200 h-1/2 text-sm font-semibold"
|
||||
>
|
||||
<BsTrash3 />
|
||||
Uninstall
|
||||
</Button>
|
||||
{resources?.length ? (
|
||||
<Modal
|
||||
title={uninstallTitle}
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
actions={[
|
||||
{
|
||||
id: "1",
|
||||
callback: uninstallMutation.mutate,
|
||||
variant: ModalButtonStyle.info,
|
||||
isLoading: uninstallMutation.isLoading,
|
||||
},
|
||||
]}
|
||||
containerClassNames="w-[800px]"
|
||||
>
|
||||
<div>Following resources will be deleted from the cluster:</div>
|
||||
<div>
|
||||
{resources?.map((resource) => (
|
||||
<div
|
||||
key={
|
||||
resource.apiVersion + resource.kind + resource.metadata.name
|
||||
}
|
||||
className="flex justify-start gap-1 w-full mb-3"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
textAlign: "end",
|
||||
paddingRight: "30px",
|
||||
}}
|
||||
className=" w-3/5 italic"
|
||||
>
|
||||
{resource.kind}
|
||||
</span>
|
||||
<span className=" w-4/5 font-semibold">
|
||||
{resource.metadata.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
232
frontend/src/components/revision/RevisionDiff.tsx
Normal file
232
frontend/src/components/revision/RevisionDiff.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { ChangeEvent, useMemo, useState, useRef, useEffect } from "react";
|
||||
import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-slim.js";
|
||||
import { useGetReleaseInfoByType } from "../../API/releases";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useCustomSearchParams from "../../hooks/useCustomSearchParams";
|
||||
|
||||
import parse from "html-react-parser";
|
||||
|
||||
import hljs from "highlight.js";
|
||||
import Spinner from "../Spinner";
|
||||
import { diffConfiguration } from "../../utils";
|
||||
|
||||
type RevisionDiffProps = {
|
||||
includeUserDefineOnly?: boolean;
|
||||
latestRevision: number;
|
||||
};
|
||||
|
||||
const VIEW_MODE_VIEW_ONLY = "view";
|
||||
const VIEW_MODE_DIFF_PREV = "diff-with-previous";
|
||||
const VIEW_MODE_DIFF_SPECIFIC = "diff-with-specific-revision";
|
||||
|
||||
function RevisionDiff({
|
||||
includeUserDefineOnly,
|
||||
latestRevision,
|
||||
}: RevisionDiffProps) {
|
||||
const params = useParams();
|
||||
|
||||
const [specificVersion, setSpecificVersion] = useState(latestRevision);
|
||||
const {
|
||||
searchParamsObject: searchParams,
|
||||
upsertSearchParams,
|
||||
removeSearchParam,
|
||||
} = useCustomSearchParams();
|
||||
const {
|
||||
tab,
|
||||
mode: viewMode = VIEW_MODE_VIEW_ONLY,
|
||||
"user-defined": userDefinedValue,
|
||||
} = searchParams;
|
||||
|
||||
//@ts-ignore
|
||||
const diffElement = useRef<HTMLElement>({});
|
||||
|
||||
const handleChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
upsertSearchParams("mode", e.target.value);
|
||||
};
|
||||
|
||||
const handleUserDefinedCheckbox = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
upsertSearchParams("user-defined", `${e.target.checked}`);
|
||||
} else {
|
||||
removeSearchParam("user-defined");
|
||||
}
|
||||
};
|
||||
const revisionInt = parseInt(params.revision || "", 10);
|
||||
const hasMultipleRevisions = revisionInt > 1;
|
||||
|
||||
const additionalParams = useMemo(() => {
|
||||
let additionalParamStr = "";
|
||||
if (userDefinedValue) {
|
||||
additionalParamStr += "&userDefined=true";
|
||||
}
|
||||
if (viewMode === VIEW_MODE_DIFF_PREV && hasMultipleRevisions) {
|
||||
additionalParamStr += `&revisionDiff=${revisionInt - 1}`;
|
||||
}
|
||||
const specificRevisionInt = parseInt(specificVersion?.toString() || "", 10);
|
||||
if (
|
||||
viewMode === VIEW_MODE_DIFF_SPECIFIC &&
|
||||
hasMultipleRevisions &&
|
||||
!Number.isNaN(specificRevisionInt)
|
||||
) {
|
||||
additionalParamStr += `&revisionDiff=${specificVersion}`;
|
||||
}
|
||||
return additionalParamStr;
|
||||
}, [
|
||||
viewMode,
|
||||
userDefinedValue,
|
||||
specificVersion,
|
||||
revisionInt,
|
||||
hasMultipleRevisions,
|
||||
]);
|
||||
const hasRevisionToDiff = !!additionalParams;
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isSuccess: fetchedDataSuccessfully,
|
||||
} = useGetReleaseInfoByType({ ...params, tab }, additionalParams);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (
|
||||
data &&
|
||||
!isLoading &&
|
||||
(viewMode === VIEW_MODE_VIEW_ONLY || !hasRevisionToDiff)
|
||||
) {
|
||||
return hljs.highlight(data, { language: "yaml" }).value;
|
||||
}
|
||||
if (fetchedDataSuccessfully && !data && viewMode === VIEW_MODE_VIEW_ONLY) {
|
||||
return "No value to display";
|
||||
}
|
||||
return "";
|
||||
}, [data, isLoading, viewMode, hasRevisionToDiff, fetchedDataSuccessfully]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
viewMode !== VIEW_MODE_VIEW_ONLY &&
|
||||
hasRevisionToDiff &&
|
||||
data &&
|
||||
!isLoading
|
||||
) {
|
||||
const diff2htmlUi = new Diff2HtmlUI(
|
||||
diffElement!.current!,
|
||||
data,
|
||||
diffConfiguration
|
||||
);
|
||||
diff2htmlUi.draw();
|
||||
diff2htmlUi.highlightCode();
|
||||
} else if (viewMode === VIEW_MODE_VIEW_ONLY && diffElement.current) {
|
||||
diffElement.current.innerHTML = "";
|
||||
} else if (
|
||||
fetchedDataSuccessfully &&
|
||||
(!hasRevisionToDiff || !data) &&
|
||||
diffElement.current
|
||||
) {
|
||||
diffElement.current.innerHTML = "No differences to display";
|
||||
}
|
||||
}, [
|
||||
viewMode,
|
||||
hasRevisionToDiff,
|
||||
data,
|
||||
isLoading,
|
||||
fetchedDataSuccessfully,
|
||||
diffElement,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex mb-3 p-2 border border-revision flex-row items-center justify-between w-full bg-white rounded">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
checked={viewMode === "view"}
|
||||
onChange={handleChanged}
|
||||
id="view"
|
||||
type="radio"
|
||||
value="view"
|
||||
name="notes-view"
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300"
|
||||
/>
|
||||
<label
|
||||
htmlFor="view"
|
||||
className="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
View
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
checked={viewMode === "diff-with-previous"}
|
||||
onChange={handleChanged}
|
||||
id="diff-with-previous"
|
||||
type="radio"
|
||||
value="diff-with-previous"
|
||||
name="notes-view"
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300"
|
||||
/>
|
||||
<label
|
||||
htmlFor="diff-with-previous"
|
||||
className="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
Diff with previous
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
checked={viewMode === "diff-with-specific-revision"}
|
||||
onChange={handleChanged}
|
||||
id="diff-with-specific-revision"
|
||||
type="radio"
|
||||
value="diff-with-specific-revision"
|
||||
name="notes-view"
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300"
|
||||
/>
|
||||
<label
|
||||
htmlFor="diff-with-specific-revision"
|
||||
className="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
<div>
|
||||
Diff with specific revision:
|
||||
<input
|
||||
className="border ml-2 border-gray-500 w-10 p-1 rounded-sm"
|
||||
type="text"
|
||||
value={specificVersion}
|
||||
onChange={(e) => setSpecificVersion(Number(e.target.value))}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{includeUserDefineOnly && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="user-define-only-checkbox"
|
||||
type="checkbox"
|
||||
onChange={handleUserDefinedCheckbox}
|
||||
checked={!!userDefinedValue}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
htmlFor="user-define-only-checkbox"
|
||||
className="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
User-defined only
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isLoading ? <Spinner /> : ""}
|
||||
{viewMode === VIEW_MODE_VIEW_ONLY && content ? (
|
||||
<div className="bg-white overflow-x-auto w-full p-3 relative">
|
||||
<pre className="bg-white rounded font-sf-mono">{parse(content)}</pre>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div
|
||||
className="bg-white w-full relative leading-5 font-sf-mono"
|
||||
//@ts-ignore
|
||||
ref={diffElement}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RevisionDiff;
|
||||
223
frontend/src/components/revision/RevisionResource.tsx
Normal file
223
frontend/src/components/revision/RevisionResource.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import hljs from "highlight.js";
|
||||
import { RiExternalLinkLine } from "react-icons/ri";
|
||||
|
||||
import {
|
||||
StructuredResources,
|
||||
useGetResourceDescription,
|
||||
useGetResources,
|
||||
} from "../../API/releases";
|
||||
import closeIcon from "../../assets/close.png";
|
||||
|
||||
import Drawer from "react-modern-drawer";
|
||||
import "react-modern-drawer/dist/index.css";
|
||||
|
||||
import Button from "../Button";
|
||||
import Badge, { getBadgeType } from "../Badge";
|
||||
import Spinner from "../Spinner";
|
||||
import { Troubleshoot } from "../Troubleshoot";
|
||||
|
||||
interface Props {
|
||||
isLatest: boolean;
|
||||
}
|
||||
|
||||
export default function RevisionResource({ isLatest }: Props) {
|
||||
const { namespace = "", chart = "" } = useParams();
|
||||
const { data: resources, isLoading } = useGetResources(namespace, chart);
|
||||
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
||||
|
||||
return (
|
||||
<table
|
||||
cellPadding={6}
|
||||
className="border-spacing-y-2 font-semibold border-separate w-full text-xs "
|
||||
>
|
||||
<thead className="bg-zinc-200 font-bold h-8 rounded">
|
||||
<tr>
|
||||
<td className="pl-6 rounded">RESOURCE TYPE</td>
|
||||
<td>NAME</td>
|
||||
<td>STATUS</td>
|
||||
<td>STATUS MESSAGE</td>
|
||||
<td className="rounded"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<tbody className="bg-white mt-4 h-8 rounded w-full">
|
||||
{resources?.length ? (
|
||||
resources
|
||||
.sort(function (a, b) {
|
||||
return (
|
||||
interestingResources.indexOf(a.kind.toUpperCase()) -
|
||||
interestingResources.indexOf(b.kind.toUpperCase())
|
||||
);
|
||||
})
|
||||
.reverse()
|
||||
.map((resource: StructuredResources) => (
|
||||
<ResourceRow
|
||||
key={
|
||||
resource.apiVersion + resource.kind + resource.metadata.name
|
||||
}
|
||||
resource={resource}
|
||||
isLatest={isLatest}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<div className="bg-white rounded shadow display-none no-charts mt-3 text-sm p-4">
|
||||
Looks like you don't have any resources.{" "}
|
||||
<RiExternalLinkLine className="ml-2 text-lg" />
|
||||
</div>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const ResourceRow = ({
|
||||
resource,
|
||||
isLatest,
|
||||
}: {
|
||||
resource: StructuredResources;
|
||||
isLatest: boolean;
|
||||
}) => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { name },
|
||||
status: { conditions },
|
||||
} = resource;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggleDrawer = () => {
|
||||
setIsOpen((prevState) => !prevState);
|
||||
};
|
||||
const { reason = "", status = "", message = "" } = conditions?.[0] || {};
|
||||
|
||||
const badgeType = getBadgeType(status);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className="min-w-[100%] min-h[70px] text-sm py-2">
|
||||
<td className="pl-6 rounded text-sm font-normal w-48">{kind}</td>
|
||||
<td className="font-bold text-sm w-56">{name}</td>
|
||||
<td>{reason ? <Badge type={badgeType}>{reason}</Badge> : null}</td>
|
||||
<td className="rounded text-gray-100">
|
||||
<div className="flex flex-col space-y-1 justify-start items-start ">
|
||||
{message && (
|
||||
<div className="text-gray-500 font-thin">{message}</div>
|
||||
)}
|
||||
{(badgeType === "error" || badgeType === "warning") && (
|
||||
<Troubleshoot />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="rounded">
|
||||
{isLatest && reason !== "NotFound" ? (
|
||||
<div className="flex justify-end items-center mr-36">
|
||||
<Button className="px-1 text-xs" onClick={toggleDrawer}>
|
||||
Describe
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
<Drawer
|
||||
open={isOpen}
|
||||
onClose={toggleDrawer}
|
||||
direction="right"
|
||||
className="min-w-[85%] "
|
||||
>
|
||||
{isOpen ? (
|
||||
<DescribeResource
|
||||
resource={resource}
|
||||
closeDrawer={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DescribeResource = ({
|
||||
resource,
|
||||
closeDrawer,
|
||||
}: {
|
||||
resource: StructuredResources;
|
||||
closeDrawer: () => void;
|
||||
}) => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { name },
|
||||
status: { conditions },
|
||||
} = resource;
|
||||
|
||||
const { status, reason = "" } = conditions?.[0] || {};
|
||||
const { namespace = "", chart = "" } = useParams();
|
||||
const { data, isLoading } = useGetResourceDescription(
|
||||
resource.kind,
|
||||
namespace,
|
||||
chart
|
||||
);
|
||||
const [yamlFormattedData, setYamlFormattedData] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const val = hljs.highlight(data, { language: "yaml" }).value;
|
||||
setYamlFormattedData(val);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const badgeType = getBadgeType(status);
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between px-3 py-4 border-b ">
|
||||
<div>
|
||||
<div className="flex gap-3">
|
||||
<h3 className="font-medium text-xl font-poppins">{name}</h3>
|
||||
<Badge type={badgeType}>{reason}</Badge>
|
||||
</div>
|
||||
<p className="m-0 mt-4 font-inter text-sm font-normal">{kind}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 pr-4">
|
||||
<a
|
||||
href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
|
||||
className="bg-primary text-white p-1.5 text-sm flex items-center rounded"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
See more details in Komodor
|
||||
<RiExternalLinkLine className="ml-2 text-lg" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="h-fit"
|
||||
data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<img src={closeIcon} alt="close" className="w-[16px] h-[16px]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto ">
|
||||
<pre
|
||||
className="bg-white rounded p-4 font-medium text-base font-sf-mono"
|
||||
style={{ overflow: "unset" }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: yamlFormattedData,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
97
frontend/src/components/revision/RevisionsList.tsx
Normal file
97
frontend/src/components/revision/RevisionsList.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { BsArrowDownRight, BsArrowUpRight } from "react-icons/bs";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { compare } from "compare-versions";
|
||||
|
||||
import { ReleaseRevision } from "../../data/types";
|
||||
import { getAge } from "../../timeUtils";
|
||||
import StatusLabel from "../common/StatusLabel";
|
||||
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
type RevisionsListProps = {
|
||||
releaseRevisions: ReleaseRevision[];
|
||||
selectedRevision: number;
|
||||
};
|
||||
|
||||
export default function RevisionsList({
|
||||
releaseRevisions,
|
||||
selectedRevision,
|
||||
}: RevisionsListProps) {
|
||||
const navigate = useNavigateWithSearchParams();
|
||||
const { context, namespace, chart } = useParams();
|
||||
const changeRelease = (newRevision: number) => {
|
||||
navigate(
|
||||
`/${context}/${namespace}/${chart}/installed/revision/${newRevision}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{releaseRevisions?.map((release, idx) => {
|
||||
const hasMultipleReleases =
|
||||
releaseRevisions.length > 1 && idx < releaseRevisions.length - 1;
|
||||
const prevRelease = hasMultipleReleases
|
||||
? releaseRevisions[idx + 1]
|
||||
: null;
|
||||
const isRollback = release.description.startsWith("Rollback to ");
|
||||
return (
|
||||
<div
|
||||
title={
|
||||
isRollback ? `Rollback to ${Number(release.revision) - 1}` : ""
|
||||
}
|
||||
onClick={() => changeRelease(release.revision)}
|
||||
key={release.revision}
|
||||
className={`flex flex-col border rounded-md mx-5 p-2 gap-4 cursor-pointer ${
|
||||
release.revision === selectedRevision
|
||||
? "border-revision-dark bg-white"
|
||||
: "border-revision-light bg-body-background"
|
||||
}`}
|
||||
>
|
||||
<div className="flex row justify-between">
|
||||
<StatusLabel status={release.status} isRollback={isRollback} />
|
||||
<span className="font-bold">#{release.revision}</span>
|
||||
</div>
|
||||
<div
|
||||
className="self-end text-muted text-xs flex flex-wrap gap-1"
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{prevRelease
|
||||
? compare(prevRelease.chart_ver, release.chart_ver, "!=") && (
|
||||
<>
|
||||
<span className="line-through">
|
||||
{prevRelease.chart_ver}
|
||||
</span>
|
||||
{compare(
|
||||
prevRelease.chart_ver,
|
||||
release.chart_ver,
|
||||
">"
|
||||
) ? (
|
||||
<BsArrowDownRight />
|
||||
) : (
|
||||
<BsArrowUpRight />
|
||||
)}
|
||||
<span>{release.chart_ver}</span>
|
||||
</>
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
<span title={DateTime.fromISO(release.updated).toString()}>
|
||||
AGE:{getAge(release, releaseRevisions[idx - 1])}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user