1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

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

This commit is contained in:
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,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;
});
}