Rename frontend directory (#472)

* Rename directory

* Cleanup

* Recover lost images

* remove lint
This commit is contained in:
Andrey Pokhilko
2023-09-26 10:04:44 +01:00
committed by GitHub
parent 133eef6745
commit dd7aca70ff
146 changed files with 595 additions and 309 deletions

View 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}
</>
);
};

View 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;

View 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&apos;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&amp;utm_source=helm-dash&amp;utm_medium=cta&amp;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>
)}
</>
);
};

View 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>
);
})}
</>
);
}