mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 13:25:26 +02:00
feat(helm): helm actions [r8s-259] (#715)
Co-authored-by: James Player <james.player@portainer.io> Co-authored-by: Cara Ryan <cara.ryan@portainer.io> Co-authored-by: stevensbkang <skan070@gmail.com>
This commit is contained in:
parent
dfa32b6755
commit
4ee349bd6b
117 changed files with 4161 additions and 696 deletions
|
@ -3,8 +3,11 @@ import { useState } from 'react';
|
|||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
|
||||
import { Chart } from '../types';
|
||||
import {
|
||||
useHelmChartList,
|
||||
useHelmRepositories,
|
||||
} from '../queries/useHelmChartList';
|
||||
|
||||
import { useHelmChartList } from './queries/useHelmChartList';
|
||||
import { HelmTemplatesList } from './HelmTemplatesList';
|
||||
import { HelmTemplatesSelectedItem } from './HelmTemplatesSelectedItem';
|
||||
|
||||
|
@ -18,10 +21,8 @@ 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
|
||||
);
|
||||
|
||||
const helmReposQuery = useHelmRepositories(user.Id);
|
||||
const chartListQuery = useHelmChartList(user.Id, helmReposQuery.data ?? []);
|
||||
function clearHelmChart() {
|
||||
setSelectedChart(null);
|
||||
onSelectHelmChart('');
|
||||
|
@ -44,9 +45,9 @@ export function HelmTemplates({ onSelectHelmChart, namespace, name }: Props) {
|
|||
/>
|
||||
) : (
|
||||
<HelmTemplatesList
|
||||
charts={charts}
|
||||
charts={chartListQuery.data}
|
||||
selectAction={handleChartSelection}
|
||||
loading={chartsLoading}
|
||||
isLoading={chartListQuery.isInitialLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -50,7 +50,7 @@ function renderComponent({
|
|||
withUserProvider(
|
||||
withTestRouter(() => (
|
||||
<HelmTemplatesList
|
||||
loading={loading}
|
||||
isLoading={loading}
|
||||
charts={charts}
|
||||
selectAction={selectAction}
|
||||
/>
|
||||
|
@ -137,10 +137,10 @@ describe('HelmTemplatesList', () => {
|
|||
});
|
||||
|
||||
it('should show loading message when loading prop is true', async () => {
|
||||
renderComponent({ loading: true });
|
||||
renderComponent({ loading: true, charts: [] });
|
||||
|
||||
// Check for loading message
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(screen.getByText('Loading helm charts...')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Initial download of Helm charts can take a few minutes')
|
||||
).toBeInTheDocument();
|
||||
|
|
|
@ -5,13 +5,14 @@ import { Link } from '@/react/components/Link';
|
|||
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
import { SearchBar } from '@@/datatables/SearchBar';
|
||||
import { InlineLoader } from '@@/InlineLoader';
|
||||
|
||||
import { Chart } from '../types';
|
||||
|
||||
import { HelmTemplatesListItem } from './HelmTemplatesListItem';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
isLoading: boolean;
|
||||
charts?: Chart[];
|
||||
selectAction: (chart: Chart) => void;
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ function getFilteredCharts(
|
|||
}
|
||||
|
||||
export function HelmTemplatesList({
|
||||
loading,
|
||||
isLoading,
|
||||
charts = [],
|
||||
selectAction,
|
||||
}: Props) {
|
||||
|
@ -159,16 +160,20 @@ export function HelmTemplatesList({
|
|||
<div className="text-muted small mt-4">No Helm charts found</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-muted text-center">
|
||||
Loading...
|
||||
<div className="text-muted text-center">
|
||||
Initial download of Helm charts can take a few minutes
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex flex-col">
|
||||
<InlineLoader className="justify-center">
|
||||
Loading helm charts...
|
||||
</InlineLoader>
|
||||
{charts.length === 0 && (
|
||||
<div className="text-muted text-center">
|
||||
Initial download of Helm charts can take a few minutes
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && charts.length === 0 && (
|
||||
{!isLoading && charts.length === 0 && (
|
||||
<div className="text-muted text-center">
|
||||
No helm charts available.
|
||||
</div>
|
||||
|
|
|
@ -106,10 +106,7 @@ describe('HelmTemplatesSelectedItem', () => {
|
|||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// First show the editor
|
||||
await user.click(await screen.findByText('Custom values'));
|
||||
|
||||
// Verify editor is visible
|
||||
// Verify editor is visible by default
|
||||
expect(screen.getByTestId('helm-app-creation-editor')).toBeInTheDocument();
|
||||
|
||||
// Now hide the editor
|
||||
|
|
|
@ -143,7 +143,12 @@ export function HelmTemplatesSelectedItem({
|
|||
{({ values, setFieldValue }) => (
|
||||
<Form className="form-horizontal">
|
||||
<div className="form-group !m-0">
|
||||
<FormSection title="Custom values" isFoldable className="mt-4">
|
||||
<FormSection
|
||||
title="Custom values"
|
||||
isFoldable
|
||||
defaultFolded={false}
|
||||
className="mt-4"
|
||||
>
|
||||
{loadingValues && (
|
||||
<div className="col-sm-12 p-0">
|
||||
<InlineLoader>Loading values.yaml...</InlineLoader>
|
||||
|
@ -156,7 +161,7 @@ export function HelmTemplatesSelectedItem({
|
|||
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"
|
||||
textTip="Define or paste the content of your values yaml file here"
|
||||
>
|
||||
You can get more information about Helm values file format
|
||||
in the{' '}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
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'),
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue