mirror of https://github.com/portainer/portainer
chore(app): Migrate helm templates list to react (#492)
parent
58317edb6d
commit
993f69db37
|
@ -1,9 +0,0 @@
|
|||
.helm-template-item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.helm-template-item-details .helm-template-item-details-sub {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<!-- helm chart -->
|
||||
<div ng-class="{ 'blocklist-item--selected': $ctrl.model.Selected }" class="blocklist-item template-item mx-0" ng-click="$ctrl.onSelect($ctrl.model)" role="listitem">
|
||||
<div class="blocklist-item-box">
|
||||
<!-- helmchart-image -->
|
||||
<span class="shrink-0">
|
||||
<fallback-image src="$ctrl.model.icon" fallback-icon="$ctrl.fallbackIcon" class-name="'blocklist-item-logo h-16 w-auto'" size="'3xl'"></fallback-image>
|
||||
</span>
|
||||
<!-- helmchart-details -->
|
||||
<div class="col-sm-12 helm-template-item-details">
|
||||
<!-- blocklist-item-line1 -->
|
||||
<div class="blocklist-item-line">
|
||||
<span>
|
||||
<span class="blocklist-item-title">
|
||||
{{ $ctrl.model.name }}
|
||||
</span>
|
||||
<span class="space-left blocklist-item-subtitle">
|
||||
<span class="vertical-center">
|
||||
<pr-icon icon="'svg-helm'" mode="'primary'"></pr-icon>
|
||||
</span>
|
||||
<span> Helm </span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !blocklist-item-line1 -->
|
||||
<span class="blocklist-item-actions" ng-transclude="actions"></span>
|
||||
<!-- blocklist-item-line2 -->
|
||||
<div class="blocklist-item-line helm-template-item-details-sub">
|
||||
<span class="blocklist-item-desc">
|
||||
{{ $ctrl.model.description }}
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="$ctrl.model.annotations.category">
|
||||
{{ $ctrl.model.annotations.category }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- !blocklist-item-line2 -->
|
||||
</div>
|
||||
<!-- !helmchart-details -->
|
||||
</div>
|
||||
<!-- !helm chart -->
|
||||
</div>
|
|
@ -1,17 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import './helm-templates-list-item.css';
|
||||
import { HelmIcon } from '../../HelmIcon';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesListItem', {
|
||||
templateUrl: './helm-templates-list-item.html',
|
||||
bindings: {
|
||||
model: '<',
|
||||
onSelect: '<',
|
||||
},
|
||||
transclude: {
|
||||
actions: '?templateItemActions',
|
||||
},
|
||||
controller() {
|
||||
this.fallbackIcon = HelmIcon;
|
||||
},
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
export default class HelmTemplatesListController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, HelmService, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$scope = $scope;
|
||||
this.HelmService = HelmService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.state = {
|
||||
textFilter: '',
|
||||
selectedCategory: '',
|
||||
categories: [],
|
||||
};
|
||||
|
||||
this.updateCategories = this.updateCategories.bind(this);
|
||||
this.onCategoryChange = this.onCategoryChange.bind(this);
|
||||
}
|
||||
|
||||
async updateCategories() {
|
||||
try {
|
||||
const annotationCategories = this.charts
|
||||
.map((t) => t.annotations) // get annotations
|
||||
.filter((a) => a) // filter out undefined/nulls
|
||||
.map((c) => c.category); // get annotation category
|
||||
const availableCategories = [...new Set(annotationCategories)].sort(); // unique and sort
|
||||
this.state.categories = availableCategories.map((cat) => ({ label: cat, value: cat }));
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm charts categories');
|
||||
}
|
||||
}
|
||||
|
||||
onCategoryChange(value) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.state.selectedCategory = value || '';
|
||||
});
|
||||
}
|
||||
|
||||
$onChanges() {
|
||||
if (this.charts.length > 0) {
|
||||
this.updateCategories();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
<section class="datatable" aria-label="Helm charts">
|
||||
<div class="toolBar vertical-center relative w-full flex-wrap !gap-x-5 !gap-y-1 !px-0">
|
||||
<div class="toolBarTitle vertical-center"> {{ $ctrl.titleText }} </div>
|
||||
|
||||
<div class="searchBar vertical-center !mr-0">
|
||||
<pr-icon icon="'search'" class="searchIcon"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
data-cy="helm-templates-search"
|
||||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
aria-label="Search input"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/5">
|
||||
<por-select
|
||||
placeholder="'Select a category'"
|
||||
value="$ctrl.state.selectedCategory"
|
||||
options="$ctrl.state.categories"
|
||||
on-change="($ctrl.onCategoryChange)"
|
||||
is-clearable="true"
|
||||
bind-to-body="true"
|
||||
></por-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="small text-muted mb-2"
|
||||
>Select the Helm chart to use. Bring further Helm charts into your selection list via
|
||||
<a ui-sref="portainer.account({'#': 'helm-repositories'})">User settings - Helm repositories</a>.</div
|
||||
>
|
||||
<div class="relative flex w-fit gap-1 rounded-lg bg-gray-modern-3 p-4 text-sm th-highcontrast:bg-legacy-grey-3 th-dark:bg-legacy-grey-3 mt-2">
|
||||
<div class="mt-0.5 shrink-0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-lightbulb h-4 text-warning-7 th-highcontrast:text-warning-6 th-dark:text-warning-6"
|
||||
>
|
||||
<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path>
|
||||
<path d="M9 18h6"></path>
|
||||
<path d="M10 22h4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="align-middle text-[0.9em] font-medium pr-10 mb-2">Disclaimer</p>
|
||||
<div class="small">
|
||||
At present Portainer does not support OCI format Helm charts. Support for OCI charts will be available in a future release.<br />
|
||||
If you would like to provide feedback on OCI support or get access to early releases to test this functionality,
|
||||
<a href="https://bit.ly/3WVkayl" target="_blank" rel="noopener noreferrer">please get in touch</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blocklist !px-0" role="list">
|
||||
<helm-templates-list-item
|
||||
ng-repeat="chart in allCharts = ($ctrl.charts | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory)"
|
||||
model="chart"
|
||||
type-label="helm"
|
||||
on-select="($ctrl.selectAction)"
|
||||
>
|
||||
</helm-templates-list-item>
|
||||
<div ng-if="!$ctrl.loading && !allCharts.length && $ctrl.charts.length !== 0" class="text-muted small mt-4"> No Helm charts found </div>
|
||||
<div ng-if="$ctrl.loading" class="text-muted text-center">
|
||||
Loading...
|
||||
<div class="text-muted text-center"> Initial download of Helm charts can take a few minutes </div>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.loading && $ctrl.charts.length === 0" class="text-muted text-center"> No helm charts available. </div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,14 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import controller from './helm-templates-list.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesList', {
|
||||
templateUrl: './helm-templates-list.html',
|
||||
controller,
|
||||
bindings: {
|
||||
loading: '<',
|
||||
titleText: '@',
|
||||
charts: '<',
|
||||
tableKey: '@',
|
||||
selectAction: '<',
|
||||
},
|
||||
});
|
|
@ -101,7 +101,7 @@
|
|||
<div class="row" ng-if="!$ctrl.state.chart">
|
||||
<div class="col-sm-12 p-0">
|
||||
<helm-templates-list
|
||||
title-text="Helm chart"
|
||||
title-text="'Helm chart'"
|
||||
charts="$ctrl.state.charts"
|
||||
table-key="$ctrl.state.charts"
|
||||
select-action="$ctrl.selectHelmChart"
|
||||
|
|
|
@ -58,6 +58,8 @@ import { AppDeploymentTypeFormSection } from '@/react/kubernetes/applications/co
|
|||
import { EnvironmentVariablesFormSection } from '@/react/kubernetes/applications/components/EnvironmentVariablesFormSection/EnvironmentVariablesFormSection';
|
||||
import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/components/EnvironmentVariablesFormSection/kubeEnvVarValidationSchema';
|
||||
import { IntegratedAppsDatatable } from '@/react/kubernetes/components/IntegratedAppsDatatable/IntegratedAppsDatatable';
|
||||
import { HelmTemplatesList } from '@/react/kubernetes/helm/HelmTemplates/HelmTemplatesList';
|
||||
import { HelmTemplatesListItem } from '@/react/kubernetes/helm/HelmTemplates/HelmTemplatesListItem';
|
||||
|
||||
import { namespacesModule } from './namespaces';
|
||||
import { clusterManagementModule } from './clusterManagement';
|
||||
|
@ -205,6 +207,19 @@ export const ngModule = angular
|
|||
'tableTitle',
|
||||
'dataCy',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'helmTemplatesList',
|
||||
r2a(withUIRouter(withCurrentUser(HelmTemplatesList)), [
|
||||
'loading',
|
||||
'titleText',
|
||||
'charts',
|
||||
'selectAction',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'helmTemplatesListItem',
|
||||
r2a(HelmTemplatesListItem, ['model', 'onSelect', 'actions'])
|
||||
);
|
||||
|
||||
export const componentsModule = ngModule.name;
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
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 { HelmTemplatesList } from './HelmTemplatesList';
|
||||
import { Chart } from './HelmTemplatesListItem';
|
||||
|
||||
// Sample test data
|
||||
const mockCharts: Chart[] = [
|
||||
{
|
||||
name: 'test-chart-1',
|
||||
description: 'Test Chart 1 Description',
|
||||
annotations: {
|
||||
category: 'database',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test-chart-2',
|
||||
description: 'Test Chart 2 Description',
|
||||
annotations: {
|
||||
category: 'database',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'nginx-chart',
|
||||
description: 'Nginx Web Server',
|
||||
annotations: {
|
||||
category: 'web',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const selectActionMock = vi.fn();
|
||||
|
||||
function renderComponent({
|
||||
loading = false,
|
||||
titleText = 'Test Helm Templates',
|
||||
charts = mockCharts,
|
||||
selectAction = selectActionMock,
|
||||
} = {}) {
|
||||
const user = new UserViewModel({ Username: 'user' });
|
||||
const Wrapped = withTestQueryProvider(
|
||||
withUserProvider(
|
||||
withTestRouter(() => (
|
||||
<HelmTemplatesList
|
||||
loading={loading}
|
||||
titleText={titleText}
|
||||
charts={charts}
|
||||
selectAction={selectAction}
|
||||
/>
|
||||
)),
|
||||
user
|
||||
)
|
||||
);
|
||||
return { ...render(<Wrapped />), user };
|
||||
}
|
||||
|
||||
describe('HelmTemplatesList', () => {
|
||||
beforeEach(() => {
|
||||
selectActionMock.mockClear();
|
||||
});
|
||||
|
||||
it('should display title and charts list', async () => {
|
||||
renderComponent();
|
||||
|
||||
// Check for the title
|
||||
expect(screen.getByText('Test Helm Templates')).toBeInTheDocument();
|
||||
|
||||
// Check for charts
|
||||
expect(screen.getByText('test-chart-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Chart 1 Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('nginx-chart')).toBeInTheDocument();
|
||||
expect(screen.getByText('Nginx Web Server')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call selectAction when a chart is clicked', async () => {
|
||||
renderComponent();
|
||||
|
||||
// Find the first chart item
|
||||
const firstChartItem = screen.getByText('test-chart-1').closest('button');
|
||||
expect(firstChartItem).not.toBeNull();
|
||||
|
||||
// Click on the chart item
|
||||
if (firstChartItem) {
|
||||
fireEvent.click(firstChartItem);
|
||||
}
|
||||
|
||||
// Check if selectAction was called with the correct chart
|
||||
expect(selectActionMock).toHaveBeenCalledWith(mockCharts[0]);
|
||||
});
|
||||
|
||||
it('should filter charts by text search', async () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Find search input and type "nginx"
|
||||
const searchInput = screen.getByPlaceholderText('Search...');
|
||||
await user.type(searchInput, 'nginx');
|
||||
|
||||
// Wait 300ms for debounce
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Should show only nginx chart
|
||||
expect(screen.getByText('nginx-chart')).toBeInTheDocument();
|
||||
expect(screen.queryByText('test-chart-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('test-chart-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should filter charts by category', async () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Find the category select
|
||||
const categorySelect = screen.getByText('Select a category');
|
||||
await user.click(categorySelect);
|
||||
|
||||
// Select "web" category
|
||||
const webCategory = screen.getByText('web', {
|
||||
selector: '[tabindex="-1"]',
|
||||
});
|
||||
await user.click(webCategory);
|
||||
|
||||
// Should show only web category charts
|
||||
expect(screen.queryByText('nginx-chart')).toBeInTheDocument();
|
||||
expect(screen.queryByText('test-chart-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('test-chart-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show loading message when loading prop is true', async () => {
|
||||
renderComponent({ loading: true });
|
||||
|
||||
// Check for loading message
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Initial download of Helm charts can take a few minutes')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show empty message when no charts are available', async () => {
|
||||
renderComponent({ charts: [] });
|
||||
|
||||
// Check for empty message
|
||||
expect(screen.getByText('No helm charts available.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show no results message when search has no matches', async () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Find search input and type text that won't match any charts
|
||||
const searchInput = screen.getByPlaceholderText('Search...');
|
||||
await user.type(searchInput, 'nonexistent chart');
|
||||
|
||||
// Wait 300ms for debounce
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Check for no results message
|
||||
expect(screen.getByText('No Helm charts found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle keyboard navigation and selection', async () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Find the first chart item
|
||||
const firstChartItem = screen.getByText('test-chart-1').closest('button');
|
||||
expect(firstChartItem).not.toBeNull();
|
||||
|
||||
// Focus and press Enter
|
||||
if (firstChartItem) {
|
||||
(firstChartItem as HTMLElement).focus();
|
||||
await user.keyboard('{Enter}');
|
||||
}
|
||||
|
||||
// Check if selectAction was called with the correct chart
|
||||
expect(selectActionMock).toHaveBeenCalledWith(mockCharts[0]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
|
||||
import { PortainerSelect } from '@/react/components/form-components/PortainerSelect';
|
||||
import { Link } from '@/react/components/Link';
|
||||
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
import { SearchBar } from '@@/datatables/SearchBar';
|
||||
|
||||
import { Chart, HelmTemplatesListItem } from './HelmTemplatesListItem';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
titleText: string;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
export function HelmTemplatesList({
|
||||
loading,
|
||||
titleText,
|
||||
charts = [],
|
||||
selectAction,
|
||||
}: Props) {
|
||||
const [textFilter, setTextFilter] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
|
||||
const categories = useMemo(() => getCategories(charts), [charts]);
|
||||
|
||||
const filteredCharts = useMemo(
|
||||
() => getFilteredCharts(charts, textFilter, selectedCategory),
|
||||
[charts, textFilter, selectedCategory]
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
<SearchBar
|
||||
value={textFilter}
|
||||
onChange={(value) => setTextFilter(value)}
|
||||
placeholder="Search..."
|
||||
data-cy="helm-templates-search"
|
||||
className="!mr-0 h-9"
|
||||
/>
|
||||
|
||||
<div className="w-full sm:w-1/5">
|
||||
<PortainerSelect
|
||||
placeholder="Select a category"
|
||||
value={selectedCategory}
|
||||
options={categories}
|
||||
onChange={(value) => setSelectedCategory(value)}
|
||||
isClearable
|
||||
bindToBody
|
||||
data-cy="helm-category-select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-fit">
|
||||
<div className="small text-muted mb-2">
|
||||
Select the Helm chart to use. Bring further Helm charts into your
|
||||
selection list via{' '}
|
||||
<Link
|
||||
to="portainer.account"
|
||||
params={{ '#': 'helm-repositories' }}
|
||||
data-cy="helm-repositories-link"
|
||||
>
|
||||
User settings - Helm repositories
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
|
||||
<InsightsBox
|
||||
header="Disclaimer"
|
||||
type="slim"
|
||||
content={
|
||||
<>
|
||||
At present Portainer does not support OCI format Helm charts.
|
||||
Support for OCI charts will be available in a future release.
|
||||
<br />
|
||||
If you would like to provide feedback on OCI support or get access
|
||||
to early releases to test this functionality,{' '}
|
||||
<a
|
||||
href="https://bit.ly/3WVkayl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
please get in touch
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="blocklist !px-0" role="list">
|
||||
{filteredCharts.map((chart) => (
|
||||
<HelmTemplatesListItem
|
||||
key={chart.name}
|
||||
model={chart}
|
||||
onSelect={selectAction}
|
||||
/>
|
||||
))}
|
||||
|
||||
{filteredCharts.length === 0 && textFilter && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && charts.length === 0 && (
|
||||
<div className="text-muted text-center">
|
||||
No helm charts available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
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;
|
||||
};
|
||||
}
|
||||
|
||||
interface HelmTemplatesListItemProps {
|
||||
model: Chart;
|
||||
onSelect: (model: Chart) => void;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function HelmTemplatesListItem(props: HelmTemplatesListItemProps) {
|
||||
const { model, onSelect, actions } = props;
|
||||
|
||||
function handleSelect() {
|
||||
onSelect(model);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="blocklist-item mx-0 bg-inherit text-start"
|
||||
onClick={handleSelect}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="blocklist-item-box">
|
||||
<span className="shrink-0">
|
||||
<FallbackImage
|
||||
src={model.icon}
|
||||
fallbackIcon={HelmIcon}
|
||||
className="blocklist-item-logo h-16 w-auto"
|
||||
alt="Helm chart icon"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className="col-sm-12 flex flex-wrap justify-between gap-2">
|
||||
<div className="blocklist-item-line">
|
||||
<span>
|
||||
<span className="blocklist-item-title">{model.name}</span>
|
||||
<span className="space-left blocklist-item-subtitle">
|
||||
<span className="vertical-center">
|
||||
<Svg icon="helm" className="icon icon-primary" />
|
||||
</span>
|
||||
<span> Helm </span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="blocklist-item-actions">{actions}</span>
|
||||
|
||||
<div className="blocklist-item-line w-full">
|
||||
<span className="blocklist-item-desc">{model.description}</span>
|
||||
{model.annotations?.category && (
|
||||
<span className="small text-muted">
|
||||
{model.annotations.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue