feat(helm): add registry dropdown [r8s-340] (#779)

release/2.31.0
Ali 2025-06-09 20:08:50 +12:00 committed by GitHub
parent c9e3717ce3
commit 1963edda66
16 changed files with 288 additions and 190 deletions

View File

@ -1,4 +1,4 @@
import { CellContext } from '@tanstack/react-table';
import { CellContext, Row } from '@tanstack/react-table';
import clsx from 'clsx';
import {
@ -6,14 +6,22 @@ import {
KubernetesApplicationTypes,
} from '@/kubernetes/models/application/models/appConstants';
import { filterHOC } from '@@/datatables/Filter';
import styles from './columns.status.module.css';
import { helper } from './columns.helper';
import { ApplicationRowData } from './types';
export const status = helper.accessor('Status', {
export const status = helper.accessor(getStatusSummary, {
header: 'Status',
cell: Cell,
enableSorting: false,
meta: {
filter: filterHOC('Filter by status'),
},
enableColumnFilter: true,
filterFn: (row: Row<ApplicationRowData>, _: string, filterValue: string[]) =>
filterValue.length === 0 ||
filterValue.includes(getStatusSummary(row.original)),
});
function Cell({
@ -67,3 +75,17 @@ function Cell({
</>
);
}
function getStatusSummary(item: ApplicationRowData): 'Ready' | 'Not Ready' {
if (
item.ApplicationType === KubernetesApplicationTypes.Pod &&
item.Pods &&
item.Pods.length > 0
) {
return item.Pods[0].Status === 'Running' ? 'Ready' : 'Not Ready';
}
return item.TotalPodsCount > 0 &&
item.TotalPodsCount === item.RunningPodsCount
? 'Ready'
: 'Not Ready';
}

View File

@ -1,10 +1,11 @@
import { CellContext } from '@tanstack/react-table';
import { CellContext, Row } from '@tanstack/react-table';
import { isoDate, truncate } from '@/portainer/filters/filters';
import { useIsSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { Link } from '@@/Link';
import { SystemBadge } from '@@/Badge/SystemBadge';
import { filterHOC } from '@@/datatables/Filter';
import { Application } from './types';
import { helper } from './columns.helper';
@ -49,7 +50,15 @@ export const image = helper.accessor('Image', {
});
export const appType = helper.accessor('ApplicationType', {
header: 'Application Type',
header: 'Application type',
meta: {
filter: filterHOC('Filter by application type'),
},
enableColumnFilter: true,
filterFn: (row: Row<Application>, _: string, filterValue: string[]) =>
filterValue.length === 0 ||
(!!row.original.ApplicationType &&
filterValue.includes(row.original.ApplicationType)),
});
export const published = helper.accessor('Services', {

View File

@ -9,7 +9,7 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider';
import {
useHelmRepoVersions,
ChartVersion,
} from '../../queries/useHelmRepositories';
} from '../../queries/useHelmRepoVersions';
import { HelmRelease } from '../../types';
import { openUpgradeHelmModal } from './UpgradeHelmModal';
@ -25,16 +25,18 @@ vi.mock('@/portainer/services/notifications', () => ({
notifySuccess: vi.fn(),
}));
// Mock the useHelmRepoVersions and useHelmRepositories hooks
vi.mock('../../queries/useHelmRepositories', () => ({
useHelmRepoVersions: vi.fn(),
useHelmRepositories: vi.fn(() => ({
vi.mock('../../queries/useHelmRegistries', () => ({
useHelmRegistries: vi.fn(() => ({
data: ['repo1', 'repo2'],
isInitialLoading: false,
isError: false,
})),
}));
vi.mock('../../queries/useHelmRepoVersions', () => ({
useHelmRepoVersions: vi.fn(),
}));
// Mock the useHelmRelease hook
vi.mock('../queries/useHelmRelease', () => ({
useHelmRelease: vi.fn(() => ({

View File

@ -13,11 +13,9 @@ import { Link } from '@@/Link';
import { HelmRelease, UpdateHelmReleasePayload } from '../../types';
import { useUpdateHelmReleaseMutation } from '../../queries/useUpdateHelmReleaseMutation';
import {
useHelmRepoVersions,
useHelmRepositories,
} from '../../queries/useHelmRepositories';
import { useHelmRepoVersions } from '../../queries/useHelmRepoVersions';
import { useHelmRelease } from '../queries/useHelmRelease';
import { useHelmRegistries } from '../../queries/useHelmRegistries';
import { openUpgradeHelmModal } from './UpgradeHelmModal';
@ -38,11 +36,11 @@ export function UpgradeButton({
const [useCache, setUseCache] = useState(true);
const updateHelmReleaseMutation = useUpdateHelmReleaseMutation(environmentId);
const repositoriesQuery = useHelmRepositories();
const registriesQuery = useHelmRegistries();
const helmRepoVersionsQuery = useHelmRepoVersions(
release?.chart.metadata?.name || '',
60 * 60 * 1000, // 1 hour
repositoriesQuery.data,
registriesQuery.data,
useCache
);
const versions = helmRepoVersionsQuery.data;
@ -50,8 +48,8 @@ export function UpgradeButton({
// Combined loading state
const isLoading =
repositoriesQuery.isInitialLoading || helmRepoVersionsQuery.isFetching; // use 'isFetching' for helmRepoVersionsQuery because we want to show when it's refetching
const isError = repositoriesQuery.isError || helmRepoVersionsQuery.isError;
registriesQuery.isInitialLoading || helmRepoVersionsQuery.isFetching; // use 'isFetching' for helmRepoVersionsQuery because we want to show when it's refetching
const isError = registriesQuery.isError || helmRepoVersionsQuery.isError;
const latestVersionQuery = useHelmRelease(
environmentId,
releaseName,

View File

@ -3,7 +3,7 @@ import { ArrowUp } from 'lucide-react';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { ChartVersion } from '@/react/kubernetes/helm/queries/useHelmRepositories';
import { ChartVersion } from '@/react/kubernetes/helm/queries/useHelmRepoVersions';
import { Modal, OnSubmit, openModal } from '@@/modals';
import { Button } from '@@/buttons';

View File

@ -43,7 +43,7 @@ vi.mock('../queries/useUpdateHelmReleaseMutation', () => ({
})),
}));
vi.mock('../queries/useHelmRepositories', () => ({
vi.mock('../queries/useHelmRepoVersions', () => ({
useHelmRepoVersions: vi.fn(() => ({
data: [
{ Version: '1.0.0', AppVersion: '1.0.0' },
@ -75,6 +75,8 @@ const mockChart: Chart = {
annotations: {
category: 'database',
},
version: '1.0.1',
versions: ['1.0.0', '1.0.1'],
};
const mockRouterStateService = {

View File

@ -11,10 +11,6 @@ import { confirmGenericDiscard } from '@@/modals/confirm';
import { Option } from '@@/form-components/PortainerSelect';
import { Chart } from '../types';
import {
ChartVersion,
useHelmRepoVersions,
} from '../queries/useHelmRepositories';
import { useUpdateHelmReleaseMutation } from '../queries/useUpdateHelmReleaseMutation';
import { HelmInstallInnerForm } from './HelmInstallInnerForm';
@ -30,23 +26,16 @@ export function HelmInstallForm({ selectedChart, namespace, name }: Props) {
const environmentId = useEnvironmentId();
const router = useRouter();
const analytics = useAnalytics();
const helmRepoVersionsQuery = useHelmRepoVersions(
selectedChart.name,
60 * 60 * 1000, // 1 hour
[selectedChart.repo],
false
);
const versions = helmRepoVersionsQuery.data;
const versionOptions: Option<ChartVersion>[] = versions.map(
const versionOptions: Option<string>[] = selectedChart.versions.map(
(version, index) => ({
label: index === 0 ? `${version.Version} (latest)` : version.Version,
label: index === 0 ? `${version} (latest)` : version,
value: version,
})
);
const defaultVersion = versionOptions[0]?.value;
const initialValues: HelmInstallFormValues = {
values: '',
version: defaultVersion?.Version ?? '',
version: defaultVersion ?? '',
};
const installHelmChartMutation = useUpdateHelmReleaseMutation(environmentId);
@ -66,7 +55,6 @@ export function HelmInstallForm({ selectedChart, namespace, name }: Props) {
namespace={namespace}
name={name}
versionOptions={versionOptions}
isLoadingVersions={helmRepoVersionsQuery.isInitialLoading}
/>
</Formik>
);

View File

@ -6,7 +6,6 @@ import { FormControl } from '@@/form-components/FormControl';
import { Option, PortainerSelect } from '@@/form-components/PortainerSelect';
import { FormSection } from '@@/form-components/FormSection';
import { ChartVersion } from '../queries/useHelmRepositories';
import { Chart } from '../types';
import { useHelmChartValues } from '../queries/useHelmChartValues';
import { HelmValuesInput } from '../components/HelmValuesInput';
@ -17,8 +16,7 @@ type Props = {
selectedChart: Chart;
namespace?: string;
name?: string;
versionOptions: Option<ChartVersion>[];
isLoadingVersions: boolean;
versionOptions: Option<string>[];
};
export function HelmInstallInnerForm({
@ -26,7 +24,6 @@ export function HelmInstallInnerForm({
namespace,
name,
versionOptions,
isLoadingVersions,
}: Props) {
const { values, setFieldValue, isSubmitting } =
useFormikContext<HelmInstallFormValues>();
@ -39,7 +36,7 @@ export function HelmInstallInnerForm({
const selectedVersion = useMemo(
() =>
versionOptions.find((v) => v.value.Version === values.version)?.value ??
versionOptions.find((v) => v.value === values.version)?.value ??
versionOptions[0]?.value,
[versionOptions, values.version]
);
@ -51,15 +48,14 @@ export function HelmInstallInnerForm({
<FormControl
label="Version"
inputId="version-input"
isLoading={isLoadingVersions}
loadingText="Loading versions..."
>
<PortainerSelect<ChartVersion>
<PortainerSelect<string>
value={selectedVersion}
options={versionOptions}
onChange={(version) => {
if (version) {
setFieldValue('version', version.Version);
setFieldValue('version', version);
}
}}
data-cy="helm-version-input"

View File

@ -1,12 +1,11 @@
import { useState } from 'react';
import { compact } from 'lodash';
import { useCurrentUser } from '@/react/hooks/useUser';
import { Chart } from '../types';
import {
useHelmChartList,
useHelmRepositories,
} from '../queries/useHelmChartList';
import { useHelmChartList } from '../queries/useHelmChartList';
import { useHelmRegistries } from '../queries/useHelmRegistries';
import { HelmTemplatesList } from './HelmTemplatesList';
import { HelmTemplatesSelectedItem } from './HelmTemplatesSelectedItem';
@ -20,10 +19,11 @@ interface Props {
export function HelmTemplates({ onSelectHelmChart, namespace, name }: Props) {
const [selectedChart, setSelectedChart] = useState<Chart | null>(null);
const [selectedRegistry, setSelectedRegistry] = useState<string | null>(null);
const { user } = useCurrentUser();
const helmReposQuery = useHelmRepositories(user.Id);
const chartListQuery = useHelmChartList(user.Id, helmReposQuery.data ?? []);
const helmReposQuery = useHelmRegistries();
const chartListQuery = useHelmChartList(user.Id, compact([selectedRegistry]));
function clearHelmChart() {
setSelectedChart(null);
onSelectHelmChart('');
@ -54,6 +54,9 @@ export function HelmTemplates({ onSelectHelmChart, namespace, name }: Props) {
charts={chartListQuery.data}
selectAction={handleChartSelection}
isLoading={chartListQuery.isInitialLoading}
registries={helmReposQuery.data ?? []}
selectedRegistry={selectedRegistry}
setSelectedRegistry={setSelectedRegistry}
/>
)}
</div>

View File

@ -19,6 +19,8 @@ const mockCharts: Chart[] = [
annotations: {
category: 'database',
},
version: '1.0.0',
versions: ['1.0.0', '1.0.1'],
},
{
name: 'test-chart-2',
@ -27,14 +29,18 @@ const mockCharts: Chart[] = [
annotations: {
category: 'database',
},
version: '1.0.0',
versions: ['1.0.0', '1.0.1'],
},
{
name: 'nginx-chart',
description: 'Nginx Web Server',
repo: 'https://example.com',
repo: 'https://example.com/2',
annotations: {
category: 'web',
},
version: '1.0.0',
versions: ['1.0.0', '1.0.1'],
},
];
@ -44,8 +50,11 @@ function renderComponent({
loading = false,
charts = mockCharts,
selectAction = selectActionMock,
selectedRegistry = '',
} = {}) {
const user = new UserViewModel({ Username: 'user' });
const registries = ['https://example.com', 'https://example.com/2'];
const Wrapped = withTestQueryProvider(
withUserProvider(
withTestRouter(() => (
@ -53,6 +62,9 @@ function renderComponent({
isLoading={loading}
charts={charts}
selectAction={selectAction}
registries={registries}
selectedRegistry={selectedRegistry}
setSelectedRegistry={() => {}}
/>
)),
user
@ -77,6 +89,7 @@ describe('HelmTemplatesList', () => {
expect(screen.getByText('Test Chart 1 Description')).toBeInTheDocument();
expect(screen.getByText('nginx-chart')).toBeInTheDocument();
expect(screen.getByText('Nginx Web Server')).toBeInTheDocument();
expect(screen.getByText('https://example.com/2')).toBeInTheDocument();
});
it('should call selectAction when a chart is clicked', async () => {
@ -146,11 +159,24 @@ describe('HelmTemplatesList', () => {
).toBeInTheDocument();
});
it('should show empty message when no charts are available', async () => {
renderComponent({ charts: [] });
it('should show empty message when no charts are available and a registry is selected', async () => {
renderComponent({ charts: [], selectedRegistry: 'https://example.com' });
// Check for empty message
expect(screen.getByText('No helm charts available.')).toBeInTheDocument();
expect(
screen.getByText('No helm charts available in this registry.')
).toBeInTheDocument();
});
it("should show 'select registry' message when no charts are available and no registry is selected", async () => {
renderComponent({ charts: [] });
// Check for message
expect(
screen.getByText(
'Please select a registry to view available Helm charts.'
)
).toBeInTheDocument();
});
it('should show no results message when search has no matches', async () => {

View File

@ -1,6 +1,10 @@
import { useState, useMemo } from 'react';
import { components, OptionProps } from 'react-select';
import { PortainerSelect } from '@/react/components/form-components/PortainerSelect';
import {
PortainerSelect,
Option,
} from '@/react/components/form-components/PortainerSelect';
import { Link } from '@/react/components/Link';
import { InsightsBox } from '@@/InsightsBox';
@ -15,70 +19,31 @@ interface Props {
isLoading: boolean;
charts?: Chart[];
selectAction: (chart: Chart) => void;
}
/**
* Get categories from charts
* @param charts - The charts to get the categories from
* @returns Categories
*/
function getCategories(charts: Chart[]) {
const annotationCategories = charts
.map((chart) => chart.annotations?.category) // get category
.filter((c): c is string => !!c); // filter out nulls/undefined
const availableCategories = [...new Set(annotationCategories)].sort(); // unique and sort
// Create options array in the format expected by PortainerSelect
return availableCategories.map((cat) => ({
label: cat,
value: cat,
}));
}
/**
* Get filtered charts
* @param charts - The charts to get the filtered charts from
* @param textFilter - The text filter
* @param selectedCategory - The selected category
* @returns Filtered charts
*/
function getFilteredCharts(
charts: Chart[],
textFilter: string,
selectedCategory: string | null
) {
return charts.filter((chart) => {
// Text filter
if (
textFilter &&
!chart.name.toLowerCase().includes(textFilter.toLowerCase()) &&
!chart.description.toLowerCase().includes(textFilter.toLowerCase())
) {
return false;
}
// Category filter
if (
selectedCategory &&
(!chart.annotations || chart.annotations.category !== selectedCategory)
) {
return false;
}
return true;
});
registries: string[];
selectedRegistry: string | null;
setSelectedRegistry: (registry: string | null) => void;
}
export function HelmTemplatesList({
isLoading,
charts = [],
selectAction,
registries,
selectedRegistry,
setSelectedRegistry,
}: Props) {
const [textFilter, setTextFilter] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const categories = useMemo(() => getCategories(charts), [charts]);
const registryOptions = useMemo(
() =>
registries.map((registry) => ({
label: registry,
value: registry,
})),
[registries]
);
const filteredCharts = useMemo(
() => getFilteredCharts(charts, textFilter, selectedCategory),
@ -87,7 +52,7 @@ export function HelmTemplatesList({
return (
<section className="datatable" aria-label="Helm charts">
<div className="toolBar vertical-center relative w-full flex-wrap !gap-x-5 !gap-y-1 !px-0">
<div className="toolBar vertical-center relative w-full flex-wrap !gap-x-5 !gap-y-1 !px-0 !overflow-visible">
<div className="toolBarTitle vertical-center">Helm chart</div>
<SearchBar
@ -98,12 +63,25 @@ export function HelmTemplatesList({
className="!mr-0 h-9"
/>
<div className="w-full sm:w-1/5">
<div className="w-full sm:w-1/4">
<PortainerSelect
placeholder="Select a registry"
value={selectedRegistry ?? ''}
options={registryOptions}
onChange={setSelectedRegistry}
isClearable
bindToBody
components={{ Option: RegistryOption }}
data-cy="helm-registry-select"
/>
</div>
<div className="w-full sm:w-1/4">
<PortainerSelect
placeholder="Select a category"
value={selectedCategory}
options={categories}
onChange={(value) => setSelectedCategory(value)}
onChange={setSelectedCategory}
isClearable
bindToBody
data-cy="helm-category-select"
@ -173,12 +151,85 @@ export function HelmTemplatesList({
</div>
)}
{!isLoading && charts.length === 0 && (
{!isLoading && charts.length === 0 && selectedRegistry && (
<div className="text-muted text-center">
No helm charts available.
No helm charts available in this registry.
</div>
)}
{!selectedRegistry && (
<div className="text-muted text-center">
Please select a registry to view available Helm charts.
</div>
)}
</div>
</section>
);
}
// truncate the registry text, because some registry names are urls, which are too long
function RegistryOption(props: OptionProps<Option<string>>) {
const { data: registry } = props;
return (
<div title={registry.value}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<components.Option {...props} className="whitespace-nowrap truncate">
{registry.value}
</components.Option>
</div>
);
}
/**
* Get categories from charts
* @param charts - The charts to get the categories from
* @returns Categories
*/
function getCategories(charts: Chart[]) {
const annotationCategories = charts
.map((chart) => chart.annotations?.category) // get category
.filter((c): c is string => !!c); // filter out nulls/undefined
const availableCategories = [...new Set(annotationCategories)].sort(); // unique and sort
// Create options array in the format expected by PortainerSelect
return availableCategories.map((cat) => ({
label: cat,
value: cat,
}));
}
/**
* Get filtered charts
* @param charts - The charts to get the filtered charts from
* @param textFilter - The text filter
* @param selectedCategory - The selected category
* @returns Filtered charts
*/
function getFilteredCharts(
charts: Chart[],
textFilter: string,
selectedCategory: string | null
) {
return charts.filter((chart) => {
// Text filter
if (
textFilter &&
!chart.name.toLowerCase().includes(textFilter.toLowerCase()) &&
!chart.description.toLowerCase().includes(textFilter.toLowerCase())
) {
return false;
}
// Category filter
if (
selectedCategory &&
(!chart.annotations || chart.annotations.category !== selectedCategory)
) {
return false;
}
return true;
});
}

View File

@ -19,6 +19,8 @@ const mockChart: Chart = {
annotations: {
category: 'database',
},
version: '1.0.1',
versions: ['1.0.0', '1.0.1'],
};
const clearHelmChartMock = vi.fn();

View File

@ -1,66 +1,11 @@
import { useQuery, useQueries } from '@tanstack/react-query';
import { useQueries } from '@tanstack/react-query';
import { compact, flatMap } from 'lodash';
import { useMemo } from 'react';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import axios from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
import { UserId } from '@/portainer/users/types';
import { Chart, HelmChartsResponse, HelmRepositoriesResponse } from '../types';
/**
* Get Helm repositories for user
*/
export async function getHelmRepositories(userId: UserId) {
try {
const { data } = await axios.get<HelmRepositoriesResponse>(
`users/${userId}/helm/repositories`
);
const repos = compact([
// compact will remove the global repository if it's empty
data.GlobalRepository.toLowerCase(),
...data.UserRepositories.map((repo) => repo.URL.toLowerCase()),
]);
return [...new Set(repos)];
} catch (err) {
throw parseAxiosError(err, 'Unable to retrieve helm repositories for user');
}
}
async function getChartsFromRepo(repo: string): Promise<Chart[]> {
try {
// Construct the URL with required repo parameter
const response = await axios.get<HelmChartsResponse>('templates/helm', {
params: { repo },
});
return compact(
Object.values(response.data.entries).map((versions) =>
versions[0] ? { ...versions[0], repo } : null
)
);
} catch (error) {
// Ignore errors from chart repositories as some may error but others may not
return [];
}
}
/**
* Hook to fetch all accessible Helm repositories for a user
*
* @param userId User ID
* @returns Query result with list of repository URLs
*/
export function useHelmRepositories(userId: number) {
return useQuery(
[userId, 'helm-repositories'],
() => getHelmRepositories(userId),
{
enabled: !!userId,
...withGlobalError('Unable to retrieve Helm repositories'),
}
);
}
import { Chart, HelmChartsResponse } from '../types';
/**
* React hook to fetch helm charts from the provided repositories
@ -103,3 +48,28 @@ export function useHelmChartList(userId: number, repositories: string[] = []) {
isError: chartQueries.some((q) => q.isError),
};
}
async function getChartsFromRepo(repo: string): Promise<Chart[]> {
try {
// Construct the URL with required repo parameter
const response = await axios.get<HelmChartsResponse>('templates/helm', {
params: { repo },
});
return compact(
Object.values(response.data.entries).map((versions) =>
versions[0]
? {
...versions[0],
repo,
// versions are within this response too, so we don't need a new query to fetch versions when this is used
versions: versions.map((v) => v.version),
}
: null
)
);
} catch (error) {
// Ignore errors from chart repositories as some may error but others may not
return [];
}
}

View File

@ -0,0 +1,43 @@
import { useQuery } from '@tanstack/react-query';
import { compact } from 'lodash';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { UserId } from '@/portainer/users/types';
import { withGlobalError } from '@/react-tools/react-query';
import { useCurrentUser } from '@/react/hooks/useUser';
import { HelmRegistriesResponse } from '../types';
/**
* Hook to fetch all Helm registries for the current user
*/
export function useHelmRegistries() {
const { user } = useCurrentUser();
return useQuery(
['helm', 'registries'],
async () => getHelmRegistries(user.Id),
{
enabled: !!user.Id,
...withGlobalError('Unable to retrieve helm registries'),
}
);
}
/**
* Get Helm registries for user
*/
async function getHelmRegistries(userId: UserId) {
try {
const { data } = await axios.get<HelmRegistriesResponse>(
`users/${userId}/helm/repositories`
);
const repos = compact([
// compact will remove the global repository if it's empty
data.GlobalRepository.toLowerCase(),
...data.UserRepositories.map((repo) => repo.URL.toLowerCase()),
]);
return [...new Set(repos)];
} catch (err) {
throw parseAxiosError(err, 'Unable to retrieve helm repositories for user');
}
}

View File

@ -1,12 +1,9 @@
import { useQuery, useQueries } from '@tanstack/react-query';
import { useQueries } from '@tanstack/react-query';
import { useMemo } from 'react';
import { compact, flatMap } from 'lodash';
import { withGlobalError } from '@/react-tools/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { useCurrentUser } from '@/react/hooks/useUser';
import { getHelmRepositories } from './useHelmChartList';
interface HelmSearch {
entries: Entries;
@ -21,26 +18,13 @@ export interface ChartVersion {
Version: string;
}
/**
* Hook to fetch all Helm repositories for the current user
*/
export function useHelmRepositories() {
const { user } = useCurrentUser();
return useQuery(
['helm', 'repositories'],
async () => getHelmRepositories(user.Id),
{
enabled: !!user.Id,
...withGlobalError('Unable to retrieve helm repositories'),
}
);
}
/**
* React hook to get a list of available versions for a chart from specified repositories
*
* @param chart The chart name to get versions for
* @param repositories Array of repository URLs to search in
* @param staleTime Stale time for the query
* @param useCache Whether to use the cache for the query
*/
export function useHelmRepoVersions(
chart: string,

View File

@ -87,6 +87,8 @@ export interface HelmChartResponse {
annotations?: {
category?: string;
};
version: string;
versions: string[];
}
export interface HelmRepositoryResponse {
@ -95,7 +97,7 @@ export interface HelmRepositoryResponse {
URL: string;
}
export interface HelmRepositoriesResponse {
export interface HelmRegistriesResponse {
GlobalRepository: string;
UserRepositories: HelmRepositoryResponse[];
}