mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
chore(kubernetes): Migrate Helm Templates View to React R8S-239 (#587)
This commit is contained in:
parent
ad89df4d0d
commit
264ff5457b
20 changed files with 635 additions and 372 deletions
5
app/react/kubernetes/helm/HelmTemplates/HelmIcon.tsx
Normal file
5
app/react/kubernetes/helm/HelmTemplates/HelmIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import helm from '@/assets/ico/vendor/helm.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const HelmIcon = <BadgeIcon icon={helm} />;
|
55
app/react/kubernetes/helm/HelmTemplates/HelmTemplates.tsx
Normal file
55
app/react/kubernetes/helm/HelmTemplates/HelmTemplates.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { useHelmChartList } from './queries/useHelmChartList';
|
||||
import { HelmTemplatesList } from './HelmTemplatesList';
|
||||
import { HelmTemplatesSelectedItem } from './HelmTemplatesSelectedItem';
|
||||
|
||||
interface Props {
|
||||
onSelectHelmChart: (chartName: string) => void;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function HelmTemplates({ onSelectHelmChart, namespace, name }: Props) {
|
||||
const [selectedChart, setSelectedChart] = useState<Chart | null>(null);
|
||||
|
||||
const { user } = useCurrentUser();
|
||||
const { data: charts = [], isLoading: chartsLoading } = useHelmChartList(
|
||||
user.Id
|
||||
);
|
||||
|
||||
function clearHelmChart() {
|
||||
setSelectedChart(null);
|
||||
onSelectHelmChart('');
|
||||
}
|
||||
|
||||
function handleChartSelection(chart: Chart) {
|
||||
setSelectedChart(chart);
|
||||
onSelectHelmChart(chart.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12 p-0">
|
||||
{selectedChart ? (
|
||||
<HelmTemplatesSelectedItem
|
||||
selectedChart={selectedChart}
|
||||
clearHelmChart={clearHelmChart}
|
||||
namespace={namespace}
|
||||
name={name}
|
||||
/>
|
||||
) : (
|
||||
<HelmTemplatesList
|
||||
charts={charts}
|
||||
selectAction={handleChartSelection}
|
||||
loading={chartsLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -6,14 +6,16 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
|||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { HelmTemplatesList } from './HelmTemplatesList';
|
||||
import { Chart } from './HelmTemplatesListItem';
|
||||
|
||||
// Sample test data
|
||||
const mockCharts: Chart[] = [
|
||||
{
|
||||
name: 'test-chart-1',
|
||||
description: 'Test Chart 1 Description',
|
||||
repo: 'https://example.com',
|
||||
annotations: {
|
||||
category: 'database',
|
||||
},
|
||||
|
@ -21,6 +23,7 @@ const mockCharts: Chart[] = [
|
|||
{
|
||||
name: 'test-chart-2',
|
||||
description: 'Test Chart 2 Description',
|
||||
repo: 'https://example.com',
|
||||
annotations: {
|
||||
category: 'database',
|
||||
},
|
||||
|
@ -28,6 +31,7 @@ const mockCharts: Chart[] = [
|
|||
{
|
||||
name: 'nginx-chart',
|
||||
description: 'Nginx Web Server',
|
||||
repo: 'https://example.com',
|
||||
annotations: {
|
||||
category: 'web',
|
||||
},
|
||||
|
@ -38,7 +42,6 @@ const selectActionMock = vi.fn();
|
|||
|
||||
function renderComponent({
|
||||
loading = false,
|
||||
titleText = 'Test Helm Templates',
|
||||
charts = mockCharts,
|
||||
selectAction = selectActionMock,
|
||||
} = {}) {
|
||||
|
@ -48,7 +51,6 @@ function renderComponent({
|
|||
withTestRouter(() => (
|
||||
<HelmTemplatesList
|
||||
loading={loading}
|
||||
titleText={titleText}
|
||||
charts={charts}
|
||||
selectAction={selectAction}
|
||||
/>
|
||||
|
@ -68,7 +70,7 @@ describe('HelmTemplatesList', () => {
|
|||
renderComponent();
|
||||
|
||||
// Check for the title
|
||||
expect(screen.getByText('Test Helm Templates')).toBeInTheDocument();
|
||||
expect(screen.getByText('Helm chart')).toBeInTheDocument();
|
||||
|
||||
// Check for charts
|
||||
expect(screen.getByText('test-chart-1')).toBeInTheDocument();
|
||||
|
|
|
@ -6,11 +6,12 @@ import { Link } from '@/react/components/Link';
|
|||
import { InsightsBox } from '@@/InsightsBox';
|
||||
import { SearchBar } from '@@/datatables/SearchBar';
|
||||
|
||||
import { Chart, HelmTemplatesListItem } from './HelmTemplatesListItem';
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { HelmTemplatesListItem } from './HelmTemplatesListItem';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
titleText: string;
|
||||
charts?: Chart[];
|
||||
selectAction: (chart: Chart) => void;
|
||||
}
|
||||
|
@ -70,7 +71,6 @@ function getFilteredCharts(
|
|||
|
||||
export function HelmTemplatesList({
|
||||
loading,
|
||||
titleText,
|
||||
charts = [],
|
||||
selectAction,
|
||||
}: Props) {
|
||||
|
@ -87,7 +87,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="toolBarTitle vertical-center">{titleText}</div>
|
||||
<div className="toolBarTitle vertical-center">Helm chart</div>
|
||||
|
||||
<SearchBar
|
||||
value={textFilter}
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HelmIcon } from '@/kubernetes/components/helm/helm-templates/HelmIcon';
|
||||
import { FallbackImage } from '@/react/components/FallbackImage';
|
||||
|
||||
import Svg from '@@/Svg';
|
||||
|
||||
export interface Chart {
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
annotations?: {
|
||||
category?: string;
|
||||
};
|
||||
}
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { HelmIcon } from './HelmIcon';
|
||||
|
||||
interface HelmTemplatesListItemProps {
|
||||
model: Chart;
|
||||
|
@ -30,7 +24,7 @@ export function HelmTemplatesListItem(props: HelmTemplatesListItemProps) {
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="blocklist-item mx-0 bg-inherit text-start"
|
||||
className="blocklist-item !mx-0 bg-inherit text-start"
|
||||
onClick={handleSelect}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MutationOptions } from '@tanstack/react-query';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { HelmTemplatesSelectedItem } from './HelmTemplatesSelectedItem';
|
||||
|
||||
const mockMutate = vi.fn();
|
||||
const mockNotifySuccess = vi.fn();
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/portainer/services/notifications', () => ({
|
||||
notifySuccess: (title: string, text: string) =>
|
||||
mockNotifySuccess(title, text),
|
||||
}));
|
||||
|
||||
vi.mock('./queries/useHelmChartValues', () => ({
|
||||
useHelmChartValues: vi.fn().mockReturnValue({
|
||||
data: { values: 'test-values' },
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('./queries/useHelmChartInstall', () => ({
|
||||
useHelmChartInstall: vi.fn().mockReturnValue({
|
||||
mutate: (params: Record<string, string>, options?: MutationOptions) =>
|
||||
mockMutate(params, options),
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/react/hooks/useAnalytics', () => ({
|
||||
useAnalytics: vi.fn().mockReturnValue({
|
||||
trackEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Sample test data
|
||||
const mockChart: Chart = {
|
||||
name: 'test-chart',
|
||||
description: 'Test Chart Description',
|
||||
repo: 'https://example.com',
|
||||
icon: 'test-icon-url',
|
||||
annotations: {
|
||||
category: 'database',
|
||||
},
|
||||
};
|
||||
|
||||
const clearHelmChartMock = vi.fn();
|
||||
const mockRouterStateService = {
|
||||
go: vi.fn(),
|
||||
};
|
||||
|
||||
function renderComponent({
|
||||
selectedChart = mockChart,
|
||||
clearHelmChart = clearHelmChartMock,
|
||||
namespace = 'test-namespace',
|
||||
name = 'test-name',
|
||||
} = {}) {
|
||||
const user = new UserViewModel({ Username: 'user' });
|
||||
|
||||
const Wrapped = withTestQueryProvider(
|
||||
withUserProvider(
|
||||
withTestRouter(() => (
|
||||
<HelmTemplatesSelectedItem
|
||||
selectedChart={selectedChart}
|
||||
clearHelmChart={clearHelmChart}
|
||||
namespace={namespace}
|
||||
name={name}
|
||||
/>
|
||||
)),
|
||||
user
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
...render(<Wrapped />),
|
||||
user,
|
||||
mockRouterStateService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('HelmTemplatesSelectedItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should display selected chart information', () => {
|
||||
renderComponent();
|
||||
|
||||
// Check for chart details
|
||||
expect(screen.getByText('test-chart')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Chart Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Clear selection')).toBeInTheDocument();
|
||||
expect(screen.getByText('Helm')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle custom values editor', async () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// First show the editor
|
||||
await user.click(await screen.findByText('Custom values'));
|
||||
|
||||
// Verify editor is visible
|
||||
expect(screen.getByTestId('helm-app-creation-editor')).toBeInTheDocument();
|
||||
|
||||
// Now hide the editor
|
||||
await user.click(await screen.findByText('Custom values'));
|
||||
|
||||
// Editor should be hidden
|
||||
expect(
|
||||
screen.queryByTestId('helm-app-creation-editor')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should install helm chart and navigate when install button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
// Click install button
|
||||
await user.click(screen.getByText('Install'));
|
||||
|
||||
// Check mutate was called with correct values
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
Name: 'test-name',
|
||||
Repo: 'https://example.com',
|
||||
Chart: 'test-chart',
|
||||
Values: 'test-values',
|
||||
Namespace: 'test-namespace',
|
||||
}),
|
||||
expect.objectContaining({ onSuccess: expect.any(Function) })
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable install button when namespace or name is undefined', () => {
|
||||
renderComponent({ namespace: '' });
|
||||
expect(screen.getByText('Install')).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
import { useRef } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { Form, Formik, FormikProps } from 'formik';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||
import { useCanExit } from '@/react/hooks/useCanExit';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { Button } from '@@/buttons/Button';
|
||||
import { FallbackImage } from '@@/FallbackImage';
|
||||
import Svg from '@@/Svg';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { WebEditorForm } from '@@/WebEditorForm';
|
||||
import { confirmGenericDiscard } from '@@/modals/confirm';
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { InlineLoader } from '@@/InlineLoader';
|
||||
import { FormActions } from '@@/form-components/FormActions';
|
||||
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { useHelmChartValues } from './queries/useHelmChartValues';
|
||||
import { HelmIcon } from './HelmIcon';
|
||||
import { useHelmChartInstall } from './queries/useHelmChartInstall';
|
||||
|
||||
type Props = {
|
||||
selectedChart: Chart;
|
||||
clearHelmChart: () => void;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
values: string;
|
||||
};
|
||||
|
||||
const emptyValues: FormValues = {
|
||||
values: '',
|
||||
};
|
||||
|
||||
export function HelmTemplatesSelectedItem({
|
||||
selectedChart,
|
||||
clearHelmChart,
|
||||
namespace,
|
||||
name,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const { mutate: installHelmChart, isLoading: isInstalling } =
|
||||
useHelmChartInstall();
|
||||
const { data: initialValues, isLoading: loadingValues } =
|
||||
useHelmChartValues(selectedChart);
|
||||
|
||||
const formikRef = useRef<FormikProps<FormValues>>(null);
|
||||
useCanExit(() => !formikRef.current?.dirty || confirmGenericDiscard());
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
if (!name || !namespace) {
|
||||
// Theoretically this should never happen and is mainly to keep typescript happy
|
||||
return;
|
||||
}
|
||||
|
||||
installHelmChart(
|
||||
{
|
||||
Name: name,
|
||||
Repo: selectedChart.repo,
|
||||
Chart: selectedChart.name,
|
||||
Values: values.values,
|
||||
Namespace: namespace,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
analytics.trackEvent('kubernetes-helm-install', {
|
||||
category: 'kubernetes',
|
||||
metadata: {
|
||||
'chart-name': selectedChart.name,
|
||||
},
|
||||
});
|
||||
notifySuccess('Success', 'Helm chart successfully installed');
|
||||
|
||||
// Reset the form so page can be navigated away from without getting "Are you sure?"
|
||||
formikRef.current?.resetForm();
|
||||
router.stateService.go('kubernetes.applications');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget>
|
||||
<div className="flex">
|
||||
<div className="basis-3/4 rounded-[8px] m-2 bg-gray-4 th-highcontrast:bg-black th-highcontrast:text-white th-dark:bg-gray-iron-10 th-dark:text-white">
|
||||
<div className="vertical-center p-5">
|
||||
<FallbackImage
|
||||
src={selectedChart.icon}
|
||||
fallbackIcon={HelmIcon}
|
||||
className="h-16 w-16"
|
||||
/>
|
||||
<div className="col-sm-12">
|
||||
<div className="flex justify-between">
|
||||
<span>
|
||||
<span className="text-2xl font-bold">
|
||||
{selectedChart.name}
|
||||
</span>
|
||||
<span className="space-left pr-2 text-xs">
|
||||
<span className="vertical-center">
|
||||
<Svg icon="helm" className="icon icon-primary" />
|
||||
</span>{' '}
|
||||
<span>Helm</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted text-xs">
|
||||
{selectedChart.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="basis-1/4">
|
||||
<div className="h-full w-full vertical-center justify-end pr-5">
|
||||
<Button
|
||||
color="link"
|
||||
className="!text-gray-8 hover:no-underline th-highcontrast:!text-white th-dark:!text-white"
|
||||
onClick={clearHelmChart}
|
||||
data-cy="clear-selection"
|
||||
>
|
||||
Clear selection
|
||||
<Icon icon={X} className="ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
<Formik
|
||||
innerRef={formikRef}
|
||||
initialValues={initialValues ?? emptyValues}
|
||||
enableReinitialize
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form className="form-horizontal">
|
||||
<div className="form-group !m-0">
|
||||
<FormSection title="Custom values" isFoldable className="mt-4">
|
||||
{loadingValues && (
|
||||
<div className="col-sm-12 p-0">
|
||||
<InlineLoader>Loading values.yaml...</InlineLoader>
|
||||
</div>
|
||||
)}
|
||||
{!!initialValues && (
|
||||
<WebEditorForm
|
||||
id="helm-app-creation-editor"
|
||||
value={values.values}
|
||||
onChange={(value) => setFieldValue('values', value)}
|
||||
type="yaml"
|
||||
data-cy="helm-app-creation-editor"
|
||||
placeholder="Define or paste the content of your values yaml file here"
|
||||
>
|
||||
You can get more information about Helm values file format
|
||||
in the{' '}
|
||||
<a
|
||||
href="https://helm.sh/docs/chart_template_guide/values_files/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
official documentation
|
||||
</a>
|
||||
.
|
||||
</WebEditorForm>
|
||||
)}
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
<FormActions
|
||||
submitLabel="Install"
|
||||
loadingText="Installing Helm chart"
|
||||
isLoading={isInstalling}
|
||||
isValid={!!namespace && !!name && !loadingValues}
|
||||
data-cy="helm-install"
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import {
|
||||
queryClient,
|
||||
withGlobalError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
import { queryKeys } from '@/react/kubernetes/applications/queries/query-keys';
|
||||
|
||||
import { InstallChartPayload } from '../../types';
|
||||
|
||||
async function installHelmChart(
|
||||
payload: InstallChartPayload,
|
||||
environmentId: EnvironmentId
|
||||
) {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`endpoints/${environmentId}/kubernetes/helm`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err as Error, 'Installation error');
|
||||
}
|
||||
}
|
||||
|
||||
export function useHelmChartInstall() {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
return useMutation(
|
||||
(values: InstallChartPayload) => installHelmChart(values, environmentId),
|
||||
{
|
||||
...withGlobalError('Unable to install Helm chart'),
|
||||
...withInvalidate(queryClient, [queryKeys.applications(environmentId)]),
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { compact } from 'lodash';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
import {
|
||||
Chart,
|
||||
HelmChartsResponse,
|
||||
HelmRepositoriesResponse,
|
||||
} from '../../types';
|
||||
|
||||
async function getHelmRepositories(userId: number): Promise<string[]> {
|
||||
try {
|
||||
const response = await axios.get<HelmRepositoriesResponse>(
|
||||
`users/${userId}/helm/repositories`
|
||||
);
|
||||
const { GlobalRepository, UserRepositories } = response.data;
|
||||
|
||||
// Extract URLs from user repositories
|
||||
const userHelmReposUrls = UserRepositories.map((repo) => repo.URL);
|
||||
|
||||
// Combine global and user repositories, remove duplicates and empty values
|
||||
const uniqueHelmRepos = [
|
||||
...new Set([GlobalRepository, ...userHelmReposUrls]),
|
||||
]
|
||||
.map((url) => url.toLowerCase())
|
||||
.filter((url) => url);
|
||||
|
||||
return uniqueHelmRepos;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err, 'Failed to fetch Helm repositories');
|
||||
}
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getCharts(userId: number): Promise<Chart[]> {
|
||||
try {
|
||||
// First, get all the helm repositories
|
||||
const repos = await getHelmRepositories(userId);
|
||||
|
||||
// Then fetch charts from each repository in parallel
|
||||
const chartsPromises = repos.map((repo) => getChartsFromRepo(repo));
|
||||
const chartsArrays = await Promise.all(chartsPromises);
|
||||
|
||||
// Flatten the arrays of charts into a single array
|
||||
return chartsArrays.flat();
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err, 'Failed to fetch Helm charts');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to fetch helm charts from all accessible repositories
|
||||
* @param userId User ID
|
||||
*/
|
||||
export function useHelmChartList(userId: number) {
|
||||
return useQuery([userId, 'helm-charts'], () => getCharts(userId), {
|
||||
enabled: !!userId,
|
||||
...withGlobalError('Unable to retrieve Helm charts'),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
import { Chart } from '../../types';
|
||||
|
||||
async function getHelmChartValues(chart: string, repo: string) {
|
||||
try {
|
||||
const response = await axios.get<string>(`/templates/helm/values`, {
|
||||
params: {
|
||||
repo,
|
||||
chart,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err as Error, 'Unable to get Helm chart values');
|
||||
}
|
||||
}
|
||||
|
||||
export function useHelmChartValues(chart: Chart) {
|
||||
return useQuery({
|
||||
queryKey: ['helm-chart-values', chart.repo, chart.name],
|
||||
queryFn: () => getHelmChartValues(chart.name, chart.repo),
|
||||
enabled: !!chart.name,
|
||||
select: (data) => ({
|
||||
values: data,
|
||||
}),
|
||||
...withGlobalError('Unable to get Helm chart values'),
|
||||
});
|
||||
}
|
37
app/react/kubernetes/helm/types.ts
Normal file
37
app/react/kubernetes/helm/types.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
export interface Chart extends HelmChartResponse {
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export interface HelmChartResponse {
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
annotations?: {
|
||||
category?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HelmRepositoryResponse {
|
||||
Id: number;
|
||||
UserId: number;
|
||||
URL: string;
|
||||
}
|
||||
|
||||
export interface HelmRepositoriesResponse {
|
||||
GlobalRepository: string;
|
||||
UserRepositories: HelmRepositoryResponse[];
|
||||
}
|
||||
|
||||
export interface HelmChartsResponse {
|
||||
entries: Record<string, HelmChartResponse[]>;
|
||||
apiVersion: string;
|
||||
generated: string;
|
||||
}
|
||||
|
||||
export type InstallChartPayload = {
|
||||
Name: string;
|
||||
Repo: string;
|
||||
Chart: string;
|
||||
Values: string;
|
||||
Namespace: string;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue