mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
chore(helm): Convert helm details view to react (#476)
This commit is contained in:
parent
8e6d0e7d42
commit
52bb06eb7b
9 changed files with 287 additions and 118 deletions
|
@ -23,6 +23,7 @@ import { NamespaceView } from '@/react/kubernetes/namespaces/ItemView/NamespaceV
|
||||||
import { AccessView } from '@/react/kubernetes/namespaces/AccessView/AccessView';
|
import { AccessView } from '@/react/kubernetes/namespaces/AccessView/AccessView';
|
||||||
import { JobsView } from '@/react/kubernetes/more-resources/JobsView/JobsView';
|
import { JobsView } from '@/react/kubernetes/more-resources/JobsView/JobsView';
|
||||||
import { ClusterView } from '@/react/kubernetes/cluster/ClusterView';
|
import { ClusterView } from '@/react/kubernetes/cluster/ClusterView';
|
||||||
|
import { HelmApplicationView } from '@/react/kubernetes/helm/HelmApplicationView';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.kubernetes.react.views', [])
|
.module('portainer.kubernetes.react.views', [])
|
||||||
|
@ -79,6 +80,10 @@ export const viewsModule = angular
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'kubernetesHelmApplicationView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(HelmApplicationView))), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubernetesClusterView',
|
'kubernetesClusterView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ClusterView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ClusterView))), [])
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import PortainerError from 'Portainer/error';
|
|
||||||
|
|
||||||
export default class KubernetesHelmApplicationController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $state, Authentication, Notifications, HelmService) {
|
|
||||||
this.$async = $async;
|
|
||||||
this.$state = $state;
|
|
||||||
this.Authentication = Authentication;
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.HelmService = HelmService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APPLICATION
|
|
||||||
*/
|
|
||||||
async getHelmApplication() {
|
|
||||||
try {
|
|
||||||
this.state.dataLoading = true;
|
|
||||||
const releases = await this.HelmService.listReleases(this.endpoint.Id, { filter: `^${this.state.params.name}$`, namespace: this.state.params.namespace });
|
|
||||||
if (releases.length > 0) {
|
|
||||||
this.state.release = releases[0];
|
|
||||||
} else {
|
|
||||||
throw new PortainerError(`Release ${this.state.params.name} not found`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm application details');
|
|
||||||
} finally {
|
|
||||||
this.state.dataLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
return this.$async(async () => {
|
|
||||||
this.state = {
|
|
||||||
dataLoading: true,
|
|
||||||
viewReady: false,
|
|
||||||
params: {
|
|
||||||
name: this.$state.params.name,
|
|
||||||
namespace: this.$state.params.namespace,
|
|
||||||
},
|
|
||||||
release: {
|
|
||||||
name: undefined,
|
|
||||||
chart: undefined,
|
|
||||||
app_version: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.getHelmApplication();
|
|
||||||
this.state.viewReady = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
.release-table tr {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-template-columns: 1fr 4fr;
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
<page-header
|
|
||||||
ng-if="$ctrl.state.viewReady"
|
|
||||||
title="'Helm details'"
|
|
||||||
breadcrumbs="[{label:'Applications', link:'kubernetes.applications'}, $ctrl.state.params.name]"
|
|
||||||
reload="true"
|
|
||||||
></page-header>
|
|
||||||
|
|
||||||
<kubernetes-view-loading view-ready="$ctrl.state.viewReady"></kubernetes-view-loading>
|
|
||||||
|
|
||||||
<div ng-if="$ctrl.state.viewReady">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget>
|
|
||||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 p-5">
|
|
||||||
<div class="toolBarTitle vertical-center">
|
|
||||||
<div class="widget-icon space-right">
|
|
||||||
<pr-icon icon="'svg-helm'"></pr-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Release
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<rd-widget-body>
|
|
||||||
<table class="table">
|
|
||||||
<tbody class="release-table">
|
|
||||||
<tr>
|
|
||||||
<td class="vertical-center">Name</td>
|
|
||||||
<td class="vertical-center !p-2" data-cy="k8sAppDetail-appName">
|
|
||||||
{{ $ctrl.state.release.name }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="vertical-center">Chart</td>
|
|
||||||
<td class="vertical-center !p-2">
|
|
||||||
{{ $ctrl.state.release.chart }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="vertical-center">App version</td>
|
|
||||||
<td class="vertical-center !p-2">
|
|
||||||
{{ $ctrl.state.release.app_version }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,11 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import controller from './helm.controller';
|
|
||||||
import './helm.css';
|
|
||||||
|
|
||||||
angular.module('portainer.kubernetes').component('kubernetesHelmApplicationView', {
|
|
||||||
templateUrl: './helm.html',
|
|
||||||
controller,
|
|
||||||
bindings: {
|
|
||||||
endpoint: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { HttpResponse } from 'msw';
|
||||||
|
|
||||||
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||||
|
import { server, http } from '@/setup-tests/server';
|
||||||
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||||
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||||
|
|
||||||
|
import { HelmApplicationView } from './HelmApplicationView';
|
||||||
|
|
||||||
|
// Mock the necessary hooks and dependencies
|
||||||
|
const mockUseCurrentStateAndParams = vi.fn();
|
||||||
|
const mockUseEnvironmentId = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
useCurrentStateAndParams: () => mockUseCurrentStateAndParams(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/react/hooks/useEnvironmentId', () => ({
|
||||||
|
useEnvironmentId: () => mockUseEnvironmentId(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
const user = new UserViewModel({ Username: 'user' });
|
||||||
|
const Wrapped = withTestQueryProvider(
|
||||||
|
withUserProvider(withTestRouter(HelmApplicationView), user)
|
||||||
|
);
|
||||||
|
return render(<Wrapped />);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HelmApplicationView', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Set up default mock values
|
||||||
|
mockUseEnvironmentId.mockReturnValue(3);
|
||||||
|
mockUseCurrentStateAndParams.mockReturnValue({
|
||||||
|
params: {
|
||||||
|
name: 'test-release',
|
||||||
|
namespace: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up default mock API responses
|
||||||
|
server.use(
|
||||||
|
http.get('/api/endpoints/3/kubernetes/helm', () =>
|
||||||
|
HttpResponse.json([
|
||||||
|
{
|
||||||
|
name: 'test-release',
|
||||||
|
chart: 'test-chart-1.0.0',
|
||||||
|
app_version: '1.0.0',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display helm release details when data is loaded', async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Check for the page header
|
||||||
|
expect(await screen.findByText('Helm details')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check for the release details
|
||||||
|
expect(screen.getByText('Release')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check for the table content
|
||||||
|
expect(screen.getByText('Name')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Chart')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('App version')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check for the actual values
|
||||||
|
expect(screen.getByTestId('k8sAppDetail-appName')).toHaveTextContent(
|
||||||
|
'test-release'
|
||||||
|
);
|
||||||
|
expect(screen.getByText('test-chart-1.0.0')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('1.0.0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message when API request fails', async () => {
|
||||||
|
// Mock API failure
|
||||||
|
server.use(
|
||||||
|
http.get('/api/endpoints/3/kubernetes/helm', () => HttpResponse.error())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock console.error to prevent test output pollution
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Wait for the error message to appear
|
||||||
|
expect(
|
||||||
|
await screen.findByText('Failed to load Helm application details')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Restore console.error
|
||||||
|
vi.spyOn(console, 'error').mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message when release is not found', async () => {
|
||||||
|
// Mock empty response (no releases found)
|
||||||
|
server.use(
|
||||||
|
http.get('/api/endpoints/3/kubernetes/helm', () => HttpResponse.json([]))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock console.error to prevent test output pollution
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Wait for the error message to appear
|
||||||
|
expect(
|
||||||
|
await screen.findByText('Failed to load Helm application details')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Restore console.error
|
||||||
|
vi.spyOn(console, 'error').mockRestore();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { PageHeader } from '@/react/components/PageHeader';
|
||||||
|
import { Widget, WidgetBody, WidgetTitle } from '@/react/components/Widget';
|
||||||
|
import helm from '@/assets/ico/vendor/helm.svg?c';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { ViewLoading } from '@@/ViewLoading';
|
||||||
|
import { Alert } from '@@/Alert';
|
||||||
|
|
||||||
|
import { useHelmRelease } from './queries/useHelmRelease';
|
||||||
|
|
||||||
|
export function HelmApplicationView() {
|
||||||
|
const { params } = useCurrentStateAndParams();
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
|
||||||
|
const name = params.name as string;
|
||||||
|
const namespace = params.namespace as string;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: release,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useHelmRelease(environmentId, name, namespace);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <ViewLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !release) {
|
||||||
|
return (
|
||||||
|
<Alert color="error" title="Failed to load Helm application details" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Helm details"
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: 'Applications', link: 'kubernetes.applications' },
|
||||||
|
name,
|
||||||
|
]}
|
||||||
|
reload
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<WidgetTitle icon={helm} title="Release" />
|
||||||
|
<WidgetBody>
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="!border-none w-40">Name</td>
|
||||||
|
<td
|
||||||
|
className="!border-none min-w-[140px]"
|
||||||
|
data-cy="k8sAppDetail-appName"
|
||||||
|
>
|
||||||
|
{release.name}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="!border-t">Chart</td>
|
||||||
|
<td className="!border-t">{release.chart}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>App version</td>
|
||||||
|
<td>{release.app_version}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
1
app/react/kubernetes/helm/HelmApplicationView/index.ts
Normal file
1
app/react/kubernetes/helm/HelmApplicationView/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { HelmApplicationView } from './HelmApplicationView';
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withGlobalError } from '@/react-tools/react-query';
|
||||||
|
import PortainerError from 'Portainer/error';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
interface HelmRelease {
|
||||||
|
name: string;
|
||||||
|
chart: string;
|
||||||
|
app_version: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* List all helm releases based on passed in options
|
||||||
|
* @param environmentId - Environment ID
|
||||||
|
* @param options - Options for filtering releases
|
||||||
|
* @returns List of helm releases
|
||||||
|
*/
|
||||||
|
export async function listReleases(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
options: {
|
||||||
|
namespace?: string;
|
||||||
|
filter?: string;
|
||||||
|
selector?: string;
|
||||||
|
output?: string;
|
||||||
|
} = {}
|
||||||
|
): Promise<HelmRelease[]> {
|
||||||
|
try {
|
||||||
|
const { namespace, filter, selector, output } = options;
|
||||||
|
const url = `endpoints/${environmentId}/kubernetes/helm`;
|
||||||
|
const { data } = await axios.get<HelmRelease[]>(url, {
|
||||||
|
params: { namespace, filter, selector, output },
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve release list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook to fetch a specific Helm release
|
||||||
|
*/
|
||||||
|
export function useHelmRelease(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
name: string,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
[environmentId, 'helm', namespace, name],
|
||||||
|
() => getHelmRelease(environmentId, name, namespace),
|
||||||
|
{
|
||||||
|
enabled: !!environmentId,
|
||||||
|
...withGlobalError('Unable to retrieve helm application details'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific Helm release
|
||||||
|
*/
|
||||||
|
async function getHelmRelease(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
name: string,
|
||||||
|
namespace: string
|
||||||
|
): Promise<HelmRelease> {
|
||||||
|
try {
|
||||||
|
const releases = await listReleases(environmentId, {
|
||||||
|
filter: `^${name}$`,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (releases.length > 0) {
|
||||||
|
return releases[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PortainerError(`Release ${name} not found`);
|
||||||
|
} catch (err) {
|
||||||
|
throw new PortainerError(
|
||||||
|
'Unable to retrieve helm application details',
|
||||||
|
err as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue