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,39 @@
import hljs from "highlight.js";
import Spinner from "../../Spinner";
export const ChartValues = ({
chartValues,
loading,
}: {
chartValues: string;
loading: boolean;
}) => {
return (
<div className="w-1/2">
<label
className="block tracking-wide text-gray-700 text-xl font-medium mb-2"
htmlFor="grid-user-defined-values"
>
Chart Value Reference:
</label>
<pre
className="text-base bg-chart-values p-2 rounded font-medium w-full max-h-[330px] block overflow-y-auto font-sf-mono"
dangerouslySetInnerHTML={
chartValues && !loading
? {
__html: hljs.highlight(chartValues, {
language: "yaml",
}).value,
}
: undefined
}
>
{loading ? (
<Spinner />
) : !chartValues && !loading ? (
"No original values information found"
) : null}
</pre>
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { ChartValues } from "./ChartValues";
import { UserDefinedValues } from "./UserDefinedValues";
interface DefinedValuesProps {
initialValue: string;
onUserValuesChange: (values: string) => void;
chartValues: string;
loading: boolean;
}
export const DefinedValues = ({
initialValue,
chartValues,
onUserValuesChange,
loading,
}: DefinedValuesProps) => {
return (
<div className="flex w-full gap-6 mt-4">
<UserDefinedValues
initialValue={initialValue}
onValuesChange={onUserValuesChange}
/>
<ChartValues chartValues={chartValues} loading={loading} />
</div>
);
};

View File

@@ -0,0 +1,56 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import useDebounce from "../../../hooks/useDebounce";
export const GeneralDetails = ({
releaseName,
namespace = "",
disabled,
onNamespaceInput,
onReleaseNameInput,
}: {
releaseName: string;
namespace?: string;
disabled: boolean;
onNamespaceInput: (namespace: string) => void;
onReleaseNameInput: (chartName: string) => void;
}) => {
const [namespaceInputValue, setNamespaceInputValue] = useState(namespace);
const namespaceInputValueDebounced = useDebounce<string>(namespaceInputValue, 500);
useEffect(() => {
onNamespaceInput(namespaceInputValueDebounced);
}, [namespaceInputValueDebounced, onNamespaceInput]);
const { context } = useParams();
const inputClassName = ` text-lg py-1 px-2 border border-1 border-gray-300 ${
disabled ? "bg-gray-200" : "bg-white "
} rounded`;
return (
<div className="flex gap-8">
<div>
<h4 className="text-lg">Release name:</h4>
<input
className={inputClassName}
value={releaseName}
disabled={disabled}
onChange={(e) => onReleaseNameInput(e.target.value)}
></input>
</div>
<div>
<h4 className="text-lg">Namespace (optional):</h4>
<input
className={inputClassName}
value={namespaceInputValue}
disabled={disabled}
onChange={(e) => setNamespaceInputValue(e.target.value)}
></input>
</div>
{context ? (
<div className="flex">
<h4 className="text-lg">Cluster:</h4>
<p className="text-lg">{context}</p>
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,260 @@
import { useParams } from "react-router-dom";
import useAlertError from "../../../hooks/useAlertError";
import { useMemo, useState } from "react";
import {
useChartReleaseValues,
useGetReleaseManifest,
useGetVersions,
useVersionData,
} from "../../../API/releases";
import Modal, { ModalButtonStyle } from "../Modal";
import { GeneralDetails } from "./GeneralDetails";
import { ManifestDiff } from "./ManifestDiff";
import { useMutation } from "@tanstack/react-query";
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
import { VersionToInstall } from "./VersionToInstall";
import { isNewerVersion, isNoneEmptyArray } from "../../../utils";
import useCustomSearchParams from "../../../hooks/useCustomSearchParams";
import { useChartRepoValues } from "../../../API/repositories";
import { useDiffData } from "../../../API/shared";
import { InstallChartModalProps } from "../../../data/types";
import { DefinedValues } from "./DefinedValues";
export const InstallReleaseChartModal = ({
isOpen,
onClose,
chartName,
currentlyInstalledChartVersion,
latestVersion,
isUpgrade = false,
latestRevision,
}: InstallChartModalProps) => {
const navigate = useNavigateWithSearchParams();
const { setShowErrorModal } = useAlertError();
const [userValues, setUserValues] = useState<string>();
const [installError, setInstallError] = useState("");
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 } = useGetVersions(chartName, {
select: (data) => {
return data?.sort((a, b) =>
isNewerVersion(a.version, b.version) ? 1 : -1
);
},
onSuccess: (data) => {
const empty = { version: "", repository: "", urls: [] };
return setSelectedVersionData(data[0] ?? empty);
},
});
const versions = _versions?.map((v) => ({
...v,
isChartVersion: v.version === currentlyInstalledChartVersion,
}));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
latestVersion = latestVersion ?? currentlyInstalledChartVersion; // a guard for typescript, latestVersion is always defined
const [selectedVersionData, setSelectedVersionData] = useState<{
version: string;
repository?: string;
urls: string[];
}>();
const selectedVersion = useMemo(() => {
return selectedVersionData?.version;
}, [selectedVersionData]);
const selectedRepo = useMemo(() => {
return selectedVersionData?.repository || "";
}, [selectedVersionData]);
const chartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData.repository) return "";
return selectedVersionData.urls?.[0]?.startsWith("file://")
? selectedVersionData.urls[0]
: `${selectedVersionData.repository}/${chartName}`;
}, [selectedVersionData, chartName]);
// 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: 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 string,
currentVerManifest,
selectedVerData,
chart: chartAddress,
});
// Confirm method (install)
const setReleaseVersionMutation = useMutation(
[
"setVersion",
namespace,
releaseName,
selectedVersion,
selectedRepo,
selectedCluster,
chartAddress,
],
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
const res = await fetch(
// Todo: Change to BASE_URL from env
`/api/helm/releases/${
namespace ? namespace : "default"
}${`/${releaseName}`}`,
{
method: "post",
body: formData,
headers: {
"X-Kubecontext": selectedCluster as string,
},
}
);
if (!res.ok) {
setShowErrorModal({
title: "Failed to upgrade the chart",
msg: String(await res.text()),
});
}
return res.json();
},
{
onSuccess: async (response) => {
onClose();
setSelectedVersionData({ version: "", urls: [] }); //cleanup
navigate(
`/${selectedCluster}/${
namespace ? namespace : "default"
}/${releaseName}/installed/revision/${response.version}`
);
window.location.reload();
},
onError: (error) => {
setInstallError((error as Error)?.message || "Failed to update");
},
}
);
return (
<Modal
isOpen={isOpen}
onClose={() => {
setSelectedVersionData({ version: "", urls: [] });
setUserValues(releaseValues);
onClose();
}}
title={
<div className="font-bold">
{`${isUpgrade ? "Upgrade" : "Install"} `}
{(isUpgrade || releaseValues) && (
<span className="text-green-700 ">{chartName}</span>
)}
</div>
}
containerClassNames="w-full text-2xl h-2/3"
actions={[
{
id: "1",
callback: setReleaseVersionMutation.mutate,
variant: ModalButtonStyle.info,
isLoading: setReleaseVersionMutation.isLoading,
disabled:
loadingReleaseValues ||
isLoadingDiff ||
setReleaseVersionMutation.isLoading,
},
]}
>
{versions && isNoneEmptyArray(versions) && (
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion
/>
)}
<GeneralDetails
releaseName={releaseName}
disabled
namespace={namespace ? namespace : filteredNamespace}
onReleaseNameInput={setReleaseName}
onNamespaceInput={setNamespace}
/>
<DefinedValues
initialValue={releaseValues}
onUserValuesChange={(values: string) => setUserValues(values)}
chartValues={chartValues}
loading={loadingReleaseValues}
/>
<ManifestDiff
diff={diffData as string}
isLoading={isLoadingDiff}
error={
(currentVerManifestError as string) ||
(selectedVerDataError as string) ||
(diffError as string) ||
installError ||
(versionsError as string)
}
/>
</Modal>
);
};

View File

@@ -0,0 +1,229 @@
import { useParams } from "react-router-dom";
import useAlertError from "../../../hooks/useAlertError";
import { useMemo, useState } from "react";
import { useGetVersions, useVersionData } from "../../../API/releases";
import Modal, { ModalButtonStyle } from "../Modal";
import { GeneralDetails } from "./GeneralDetails";
import { ManifestDiff } from "./ManifestDiff";
import { useMutation } from "@tanstack/react-query";
import { useChartRepoValues } from "../../../API/repositories";
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
import { VersionToInstall } from "./VersionToInstall";
import { isNewerVersion, isNoneEmptyArray } from "../../../utils";
import { useDiffData } from "../../../API/shared";
import { InstallChartModalProps } from "../../../data/types";
import { DefinedValues } from "./DefinedValues";
export const InstallRepoChartModal = ({
isOpen,
onClose,
chartName,
currentlyInstalledChartVersion,
latestVersion,
}: InstallChartModalProps) => {
const navigate = useNavigateWithSearchParams();
const { setShowErrorModal } = useAlertError();
const [userValues, setUserValues] = useState("");
const [installError, setInstallError] = useState("");
const { context: selectedCluster, selectedRepo: currentRepoCtx } =
useParams();
const [namespace, setNamespace] = useState("");
const [releaseName, setReleaseName] = useState(chartName);
const { error: versionsError, data: _versions } = useGetVersions(chartName, {
select: (data) => {
return data?.sort((a, b) =>
isNewerVersion(a.version, b.version) ? 1 : -1
);
},
onSuccess: (data) => {
const empty = { version: "", repository: "", urls: [] };
const versionsToRepo = data.filter(
(v) => v.repository === currentRepoCtx
);
return setSelectedVersionData(versionsToRepo[0] ?? empty);
},
});
const versions = _versions?.map((v) => ({
...v,
isChartVersion: v.version === currentlyInstalledChartVersion,
}));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
latestVersion = latestVersion ?? currentlyInstalledChartVersion; // a guard for typescript, latestVersion is always defined
const [selectedVersionData, setSelectedVersionData] = useState<{
version: string;
repository?: string;
urls: string[];
}>();
const selectedVersion = useMemo(() => {
return selectedVersionData?.version;
}, [selectedVersionData]);
const selectedRepo = useMemo(() => {
return selectedVersionData?.repository;
}, [selectedVersionData]);
const chartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData?.repository) {
return "";
}
return selectedVersionData?.urls?.[0]?.startsWith("file://")
? selectedVersionData?.urls[0]
: `${selectedVersionData?.repository}/${chartName}`;
}, [selectedVersionData, chartName]);
const { data: chartValues, isLoading: loadingChartValues } =
useChartRepoValues({
version: selectedVersion || "",
chart: chartAddress,
});
// This hold the selected version manifest, we use it for the diff
const { data: selectedVerData, error: selectedVerDataError } = useVersionData(
{
version: selectedVersion || "",
userValues,
chartAddress,
releaseValues: userValues,
namespace,
releaseName,
isInstallRepoChart: true,
options: {
enabled: Boolean(chartAddress),
},
}
);
const {
data: diffData,
isLoading: isLoadingDiff,
error: diffError,
} = useDiffData({
selectedRepo: selectedRepo || "",
versionsError: versionsError as string,
currentVerManifest: "", // current version manifest should always be empty since its a fresh install
selectedVerData,
chart: chartAddress,
});
// Confirm method (install)
const setReleaseVersionMutation = useMutation(
[
"setVersion",
namespace,
releaseName,
selectedVersion,
selectedRepo,
selectedCluster,
chartAddress,
],
async () => {
setInstallError("");
const formData = new FormData();
formData.append("preview", "false");
formData.append("chart", chartAddress);
formData.append("version", selectedVersion || "");
formData.append("values", userValues);
formData.append("name", releaseName || "");
const res = await fetch(
// Todo: Change to BASE_URL from env
`/api/helm/releases/${namespace ? namespace : "default"}`,
{
method: "post",
body: formData,
headers: {
"X-Kubecontext": selectedCluster as string,
},
}
);
if (!res.ok) {
setShowErrorModal({
title: "Failed to install the chart",
msg: String(await res.text()),
});
}
return res.json();
},
{
onSuccess: async (response) => {
onClose();
navigate(
`/${selectedCluster}/${response.namespace}/${response.name}/installed/revision/1`
);
},
onError: (error) => {
setInstallError((error as Error)?.message || "Failed to update");
},
}
);
return (
<Modal
isOpen={isOpen}
onClose={() => {
setSelectedVersionData({ version: "", urls: [] });
onClose();
}}
title={
<div className="font-bold">
Install <span className="text-green-700 ">{chartName}</span>
</div>
}
containerClassNames="w-full text-2xl h-2/3"
actions={[
{
id: "1",
callback: setReleaseVersionMutation.mutate,
variant: ModalButtonStyle.info,
isLoading: setReleaseVersionMutation.isLoading,
disabled:
loadingChartValues ||
isLoadingDiff ||
setReleaseVersionMutation.isLoading,
},
]}
>
{versions && isNoneEmptyArray(versions) && (
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion={false}
/>
)}
<GeneralDetails
releaseName={releaseName ?? ""}
disabled={false}
namespace={namespace}
onReleaseNameInput={setReleaseName}
onNamespaceInput={setNamespace}
/>
<DefinedValues
initialValue={""}
onUserValuesChange={setUserValues}
chartValues={chartValues}
loading={loadingChartValues}
/>
<ManifestDiff
diff={diffData as string}
isLoading={isLoadingDiff}
error={
(selectedVerDataError as string) ||
(diffError as string) ||
installError ||
(versionsError as string)
}
/>
</Modal>
);
};

View File

@@ -0,0 +1,65 @@
import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-base";
import hljs from "highlight.js";
import { useEffect, useRef } from "react";
import Spinner from "../../Spinner";
import { diffConfiguration } from "../../../utils";
interface ManifestDiffProps {
diff?: string;
isLoading: boolean;
error: string;
}
export const ManifestDiff = ({ diff, isLoading, error }: ManifestDiffProps) => {
const diffContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (isLoading) {
// we're listening to isLoading to draw new diffs which are not
// always rerender, probably because of the use of ref
return;
}
if (diff && diffContainerRef.current) {
const diff2htmlUi = new Diff2HtmlUI(
diffContainerRef.current,
diff,
diffConfiguration,
hljs
);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
}
}, [diff, isLoading]);
if (isLoading && !error) {
return (
<div className="flex text-lg items-end">
<Spinner />
Calculating diff...
</div>
);
}
return (
<div>
<h4 className="text-xl">Manifest changes:</h4>
{error ? (
<p className="text-red-600 text-lg">
Failed to get upgrade info: {error.toString()}
</p>
) : diff ? (
<div
ref={diffContainerRef}
className="relative overflow-y-auto leading-5"
></div>
) : (
<pre className="font-roboto text-base">
No changes will happen to the cluster
</pre>
)}
</div>
);
};

View File

@@ -0,0 +1,39 @@
import { useEffect, useState } from "react";
import useDebounce from "../../../hooks/useDebounce";
export const UserDefinedValues = ({
initialValue,
onValuesChange,
}: {
initialValue: string;
onValuesChange: (val: string) => void;
}) => {
const [userDefinedValues, setUserDefinedValues] = useState(initialValue);
const debouncedValue = useDebounce<string>(userDefinedValues, 500);
useEffect(() => {
if (!debouncedValue || debouncedValue === initialValue) {
return;
}
onValuesChange(debouncedValue);
}, [debouncedValue, onValuesChange, initialValue]);
return (
<div className="w-1/2 ">
<label
className="block tracking-wide text-gray-700 text-xl font-medium mb-2"
htmlFor="grid-user-defined-values"
>
User-Defined Values:
</label>
<textarea
value={userDefinedValues}
defaultValue={initialValue}
onChange={(e) => setUserDefinedValues(e.target.value)}
rows={14}
className="block p-2.5 w-full text-md text-gray-900 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 resize-none font-monospace"
></textarea>
</div>
);
};

View File

@@ -0,0 +1,113 @@
import { useMemo, useState } from "react";
import Select, { components } from "react-select";
import { BsCheck2 } from "react-icons/bs";
import { NonEmptyArray } from "../../../data/types";
interface Version {
repository: string;
version: string;
isChartVersion: boolean;
urls: string[];
}
export const VersionToInstall: React.FC<{
versions: NonEmptyArray<Version>;
initialVersion?: {
repository?: string;
version?: string;
};
onSelectVersion: (props: {
version: string;
repository: string;
urls: string[];
}) => void;
showCurrentVersion: boolean;
}> = ({ versions, onSelectVersion, showCurrentVersion, initialVersion }) => {
const chartVersion = useMemo(
() => versions.find(({ isChartVersion }) => isChartVersion)?.version,
[versions]
);
const currentVersion =
chartVersion && showCurrentVersion ? (
<p className="text-xl text-muted ml-2">
{"(current version is "}
<span className="text-green-700">{`${chartVersion}`}</span>
{")"}
</p>
) : null;
// Prepare your options for react-select
const options = useMemo(
() =>
versions.map(({ repository, version, urls }) => ({
value: { repository, version, urls },
label: `${repository} @ ${version}`,
check: chartVersion === version,
})) || [],
[chartVersion, versions]
);
const [selectedOption, setSelectedOption] =
useState<(typeof options)[number]>();
const initOpt = useMemo(
() =>
options.find(
({ value }) =>
value.version === initialVersion?.version &&
value.repository === initialVersion?.repository
),
[options, initialVersion]
);
return (
<div className="flex gap-2 text-xl items-center">
{versions?.length && (selectedOption || initOpt) ? (
<>
Version to install:{" "}
<Select
className="basic-single cursor-pointer min-w-[272px]"
classNamePrefix="select"
isClearable={false}
isSearchable={false}
name="version"
options={options}
onChange={(selectedOption) => {
if (selectedOption) {
setSelectedOption(selectedOption);
onSelectVersion(selectedOption.value);
}
}}
value={selectedOption ?? initOpt}
components={{
SingleValue: ({ children, ...props }) => (
<components.SingleValue {...props}>
<span className="text-green-700 font-bold">{children}</span>
{props.data.check && showCurrentVersion && (
<BsCheck2 className="inline-block ml-2 text-green-700 font-bold" />
)}
</components.SingleValue>
),
Option: ({ children, innerProps, data }) => (
<div
className={
"flex items-center py-2 pl-4 pr-2 text-green-700 hover:bg-blue-100"
}
{...innerProps}
>
<div className="width-auto">{children}</div>
{data.check && showCurrentVersion && (
<BsCheck2
fontWeight={"bold"}
className="inline-block ml-2 text-green-700 font-bold"
/>
)}
</div>
),
}} // Use the custom Option component
/>
</>
) : null}
{currentVersion}
</div>
);
};