feat: install and upgrade charts from URLs (#663)

* feat: add support for installing and upgrading charts from URLs

Adds "Install from URL" button to the repositories page, allowing users
to install charts directly from OCI registries and other URLs without
adding them as repositories first. Also adds URL mode to the upgrade
modal (via pencil/X toggle) for charts not found in any configured repo.

Closes #660

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Alter

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrey Pokhilko
2026-03-17 15:09:11 +02:00
committed by GitHub
parent 5d2a61c2ff
commit 123f674e2f
3 changed files with 134 additions and 22 deletions

View File

@@ -9,6 +9,8 @@ import {
} from "react";
import { useParams } from "react-router";
import { BsPencil, BsX } from "react-icons/bs";
import apiService from "../../../API/apiService";
import type { LatestChartVersion } from "../../../API/interfaces";
import {
@@ -88,7 +90,10 @@ export const InstallReleaseChartModal = ({
const selectedVersion = selectedVersionData?.version || "";
const selectedRepo = selectedVersionData?.repository || "";
const chartAddress = useMemo(() => {
const [chartURL, setChartURL] = useState("");
const [useURLMode, setUseURLMode] = useState(false);
const repoChartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData.repository) return "";
return selectedVersionData.urls?.[0]?.startsWith("file://")
@@ -96,6 +101,8 @@ export const InstallReleaseChartModal = ({
: `${selectedVersionData.repository}/${chartName}`;
}, [selectedVersionData, chartName]);
const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
// the original chart values
const { data: chartValues = "" } = useChartRepoValues({
version: selectedVersion,
@@ -216,13 +223,48 @@ export const InstallReleaseChartModal = ({
},
]}
>
{versions && isNoneEmptyArray(versions) && (
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion
/>
{!useURLMode && versions && isNoneEmptyArray(versions) ? (
<div className="flex items-center gap-2">
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion
/>
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to URL"
onClick={() => setUseURLMode(true)}
>
<BsPencil className="text-lg" />
</button>
</div>
) : (
<div className="flex items-end gap-2">
<div className="flex-1">
<h4 className="text-lg">Chart URL:</h4>
<input
className="w-full rounded-sm border border-1 border-gray-300 bg-white px-2 py-1 text-lg"
value={chartURL}
onChange={(e) => setChartURL(e.target.value)}
placeholder="oci://registry-1.docker.io/example/chart"
/>
</div>
{versions && isNoneEmptyArray(versions) && (
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to repository"
onClick={() => {
setUseURLMode(false);
setChartURL("");
}}
>
<BsX className="text-2xl" />
</button>
)}
</div>
)}
<GeneralDetails

View File

@@ -9,6 +9,8 @@ import {
} from "react";
import { useParams } from "react-router";
import { BsPencil, BsX } from "react-icons/bs";
import apiService from "../../../API/apiService";
import type { LatestChartVersion } from "../../../API/interfaces";
import { useGetVersions, useVersionData } from "../../../API/releases";
@@ -32,7 +34,8 @@ export const InstallRepoChartModal = ({
onClose,
chartName,
currentlyInstalledChartVersion,
}: InstallChartModalProps) => {
urlMode: initialURLMode = false,
}: InstallChartModalProps & { urlMode?: boolean }) => {
const navigate = useNavigateWithSearchParams();
const [userValues, setUserValues] = useState("");
const [installError, setInstallError] = useState("");
@@ -83,7 +86,10 @@ export const InstallRepoChartModal = ({
const selectedRepo = selectedVersionData?.repository;
const chartAddress = useMemo(() => {
const [chartURL, setChartURL] = useState("");
const [useURLMode, setUseURLMode] = useState(initialURLMode);
const repoChartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData?.repository) {
return "";
}
@@ -92,6 +98,8 @@ export const InstallRepoChartModal = ({
: `${selectedVersionData?.repository}/${chartName}`;
}, [selectedVersionData, chartName]);
const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
const { data: chartValues = "", isLoading: loadingChartValues } =
useChartRepoValues({
version: selectedVersion || "",
@@ -175,11 +183,15 @@ export const InstallRepoChartModal = ({
onClose();
}}
title={
<InstallUpgradeTitle
isUpgrade={false}
releaseValues={false}
chartName={chartName}
/>
initialURLMode ? (
<div className="font-bold">Install from URL</div>
) : (
<InstallUpgradeTitle
isUpgrade={false}
releaseValues={false}
chartName={chartName}
/>
)
}
containerClassNames="w-full text-2xl h-2/3"
actions={[
@@ -195,13 +207,48 @@ export const InstallRepoChartModal = ({
},
]}
>
{versions && isNoneEmptyArray(versions) && (
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion={false}
/>
{!useURLMode && versions && isNoneEmptyArray(versions) ? (
<div className="flex items-center gap-2">
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion={false}
/>
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to URL"
onClick={() => setUseURLMode(true)}
>
<BsPencil className="text-lg" />
</button>
</div>
) : (
<div className="flex items-end gap-2">
<div className="flex-1">
<h4 className="text-lg">Chart URL:</h4>
<input
className="w-full rounded-sm border border-1 border-gray-300 bg-white px-2 py-1 text-lg"
value={chartURL}
onChange={(e) => setChartURL(e.target.value)}
placeholder="oci://registry-1.docker.io/example/chart:1.0.0"
/>
</div>
{versions && isNoneEmptyArray(versions) && (
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to repository"
onClick={() => {
setUseURLMode(false);
setChartURL("");
}}
>
<BsX className="text-2xl" />
</button>
)}
</div>
)}
<GeneralDetails

View File

@@ -1,6 +1,7 @@
import type { Repository } from "../../data/types";
import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import AddRepositoryModal from "../modal/AddRepositoryModal";
import { InstallRepoChartModal } from "../modal/InstallChartModal/InstallRepoChartModal";
type RepositoriesListProps = {
selectedRepository: Repository | undefined;
@@ -23,6 +24,14 @@ function RepositoriesList({
removeSearchParam("add_repo");
}
};
const showInstallURLModal = searchParamsObject["install_url"] === "true";
const setShowInstallURLModal = (value: boolean) => {
if (value) {
upsertSearchParams("install_url", "true");
} else {
removeSearchParam("install_url");
}
};
return (
<>
@@ -61,6 +70,14 @@ function RepositoriesList({
>
+ Add Repository
</button>
<button
data-cy="install-url-button"
type="button"
className="flex h-8 w-fit cursor-pointer items-center gap-2 rounded-sm border border-gray-300 px-3 py-1 text-sm font-semibold text-muted"
onClick={() => setShowInstallURLModal(true)}
>
Install from URL
</button>
<p className="text-xs">
Charts developers: you can also add local directories as chart source.
Use{" "}
@@ -72,6 +89,12 @@ function RepositoriesList({
isOpen={showAddRepositoryModal}
onClose={() => setShowAddRepositoryModal(false)}
/>
<InstallRepoChartModal
isOpen={showInstallURLModal}
onClose={() => setShowInstallURLModal(false)}
chartName=""
urlMode
/>
</>
);
}