1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 05:19:39 +02:00

fix(kubernetes): events api to call the backend [R8S-243] (#563)

This commit is contained in:
Cara Ryan 2025-05-27 13:55:31 +12:00 committed by GitHub
parent 32ef208278
commit 07dfd981a2
26 changed files with 750 additions and 217 deletions

View file

@ -58,7 +58,7 @@
<resource-events-datatable
resource-id="ctrl.configuration.Id"
storage-key="'kubernetes.configmap.events'"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
namespace="ctrl.configuration.Namespace"
></resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">

View file

@ -65,7 +65,7 @@
<resource-events-datatable
resource-id="ctrl.configuration.Id"
storage-key="'kubernetes.secret.events'"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
namespace="ctrl.configuration.Namespace"
></resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">

View file

@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter';
import { UserViewModel } from '@/portainer/models/user';
import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { TableState } from '@@/datatables/useTableState';
import { Event } from '../../queries/types';
import { EventsDatatable } from './EventsDatatable';
// Mock the necessary hooks and dependencies
const mockTableState: TableState<TableSettings> = {
sortBy: { id: 'Date', desc: true },
pageSize: 10,
search: '',
autoRefreshRate: 0,
showSystemResources: false,
setSortBy: vi.fn(),
setPageSize: vi.fn(),
setSearch: vi.fn(),
setAutoRefreshRate: vi.fn(),
setShowSystemResources: vi.fn(),
};
vi.mock('../../datatables/default-kube-datatable-store', () => ({
useKubeStore: () => mockTableState,
}));
function renderComponent() {
const user = new UserViewModel({ Username: 'user' });
const events: Event[] = [
{
type: 'Warning',
name: 'name',
message: 'not sure if this what you want to do',
namespace: 'default',
reason: 'unknown',
count: 1,
eventTime: new Date('2025-01-02T15:04:05Z'),
uid: '4500fc9c-0cc8-4695-b4c4-989ac021d1d6',
involvedObject: {
kind: 'configMap',
uid: '35',
name: 'name',
namespace: 'default',
},
},
];
const Wrapped = withTestQueryProvider(
withUserProvider(
withTestRouter(() => (
<EventsDatatable
dataset={events}
tableState={mockTableState}
isLoading={false}
data-cy="k8sNodeDetail-eventsTable"
noWidget
/>
)),
user
)
);
return { ...render(<Wrapped />), events };
}
describe('EventsDatatable', () => {
it('should display events when data is loaded', async () => {
const { events } = renderComponent();
const event = events[0];
expect(screen.getByText(event.message || '')).toBeInTheDocument();
expect(screen.getAllByText(event.type || '')).toHaveLength(2);
expect(screen.getAllByText(event.involvedObject.kind || '')).toHaveLength(
2
);
});
});

View file

@ -1,7 +1,7 @@
import { Event } from 'kubernetes-types/core/v1';
import { History } from 'lucide-react';
import { ReactNode } from 'react';
import { Event } from '@/react/kubernetes/queries/types';
import { IndexOptional } from '@/react/kubernetes/configs/types';
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
@ -38,7 +38,7 @@ export function EventsDatatable({
isLoading={isLoading}
title={title}
titleIcon={titleIcon}
getRowId={(row) => row.metadata?.uid || ''}
getRowId={(row) => row.uid || ''}
disableSelect
renderTableSettings={() => (
<TableSettingsMenu>

View file

@ -29,9 +29,7 @@ export function ResourceEventsDatatable({
params: { endpointId },
} = useCurrentStateAndParams();
const params = resourceId
? { fieldSelector: `involvedObject.uid=${resourceId}` }
: {};
const params = resourceId ? { resourceId: `${resourceId}` } : {};
const resourceEventsQuery = useEvents(endpointId, {
namespace,
params,

View file

@ -1,5 +1,6 @@
import { Row } from '@tanstack/react-table';
import { Event } from 'kubernetes-types/core/v1';
import { Event } from '@/react/kubernetes/queries/types';
import { Badge, BadgeType } from '@@/Badge';
import { filterHOC } from '@@/datatables/Filter';

View file

@ -1,4 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Event } from 'kubernetes-types/core/v1';
import { Event } from '@/react/kubernetes/queries/types';
export const columnHelper = createColumnHelper<Event>();

View file

@ -1,5 +1,6 @@
import { Row } from '@tanstack/react-table';
import { Event } from 'kubernetes-types/core/v1';
import { Event } from '@/react/kubernetes/queries/types';
import { filterHOC } from '@@/datatables/Filter';

View file

@ -184,15 +184,8 @@ describe(
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
HttpResponse.json(helmReleaseHistory)
),
http.get(
'/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
() =>
HttpResponse.json({
kind: 'EventList',
apiVersion: 'v1',
metadata: { resourceVersion: '12345' },
items: [],
})
http.get('/api/kubernetes/3/namespaces/default/events', () =>
HttpResponse.json([])
)
);
@ -236,15 +229,8 @@ describe(
HttpResponse.error()
),
// Add mock for events endpoint
http.get(
'/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
() =>
HttpResponse.json({
kind: 'EventList',
apiVersion: 'v1',
metadata: { resourceVersion: '12345' },
items: [],
})
http.get('/api/kubernetes/3/namespaces/default/events', () =>
HttpResponse.json([])
)
);
@ -274,15 +260,8 @@ describe(
http.get('/api/endpoints/3/kubernetes/helm/test-release/history', () =>
HttpResponse.json(helmReleaseHistory)
),
http.get(
'/api/endpoints/3/kubernetes/api/v1/namespaces/default/events',
() =>
HttpResponse.json({
kind: 'EventList',
apiVersion: 'v1',
metadata: { resourceVersion: '12345' },
items: [],
})
http.get('/api/kubernetes/3/namespaces/default/events', () =>
HttpResponse.json([])
)
);

View file

@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react';
import { HttpResponse } from 'msw';
import { Event, EventList } from 'kubernetes-types/core/v1';
import { Event } from '@/react/kubernetes/queries/types';
import { server, http } from '@/setup-tests/server';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter';
@ -56,136 +56,84 @@ const testResources: GenericResource[] = [
},
];
const mockEventsResponse: EventList = {
kind: 'EventList',
apiVersion: 'v1',
metadata: {
resourceVersion: '12345',
const mockEventsResponse: Event[] = [
{
name: 'test-deployment-123456',
namespace: 'default',
reason: 'CreatedLoadBalancer',
eventTime: new Date('2023-01-01T00:00:00Z'),
uid: 'event-uid-1',
involvedObject: {
kind: 'Deployment',
name: 'test-deployment',
uid: 'test-deployment-uid',
namespace: 'default',
},
message: 'Scaled up replica set test-deployment-abc123 to 1',
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
count: 1,
type: 'Normal',
},
items: [
{
metadata: {
name: 'test-deployment-123456',
namespace: 'default',
uid: 'event-uid-1',
resourceVersion: '1000',
creationTimestamp: '2023-01-01T00:00:00Z',
},
involvedObject: {
kind: 'Deployment',
namespace: 'default',
name: 'test-deployment',
uid: 'test-deployment-uid',
apiVersion: 'apps/v1',
resourceVersion: '2000',
},
reason: 'ScalingReplicaSet',
message: 'Scaled up replica set test-deployment-abc123 to 1',
source: {
component: 'deployment-controller',
},
firstTimestamp: '2023-01-01T00:00:00Z',
lastTimestamp: '2023-01-01T00:00:00Z',
count: 1,
type: 'Normal',
reportingComponent: 'deployment-controller',
reportingInstance: '',
{
name: 'test-service-123456',
namespace: 'default',
uid: 'event-uid-2',
eventTime: new Date('2023-01-01T00:00:00Z'),
involvedObject: {
kind: 'Service',
namespace: 'default',
name: 'test-service',
uid: 'test-service-uid',
},
{
metadata: {
name: 'test-service-123456',
namespace: 'default',
uid: 'event-uid-2',
resourceVersion: '1001',
creationTimestamp: '2023-01-01T00:00:00Z',
},
involvedObject: {
kind: 'Service',
namespace: 'default',
name: 'test-service',
uid: 'test-service-uid',
apiVersion: 'v1',
resourceVersion: '2001',
},
reason: 'CreatedLoadBalancer',
message: 'Created load balancer',
source: {
component: 'service-controller',
},
firstTimestamp: '2023-01-01T00:00:00Z',
lastTimestamp: '2023-01-01T00:00:00Z',
count: 1,
type: 'Normal',
reportingComponent: 'service-controller',
reportingInstance: '',
},
],
};
reason: 'CreatedLoadBalancer',
message: 'Created load balancer',
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
count: 1,
type: 'Normal',
},
];
const mixedEventsResponse: EventList = {
kind: 'EventList',
apiVersion: 'v1',
metadata: {
resourceVersion: '12345',
const mixedEventsResponse: Event[] = [
{
name: 'test-deployment-123456',
namespace: 'default',
uid: 'event-uid-1',
eventTime: new Date('2023-01-01T00:00:00Z'),
involvedObject: {
kind: 'Deployment',
namespace: 'default',
name: 'test-deployment',
uid: 'test-deployment-uid', // This matches a resource UID
},
reason: 'ScalingReplicaSet',
message: 'Scaled up replica set test-deployment-abc123 to 1',
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
count: 1,
type: 'Normal',
},
items: [
{
metadata: {
name: 'test-deployment-123456',
namespace: 'default',
uid: 'event-uid-1',
resourceVersion: '1000',
creationTimestamp: '2023-01-01T00:00:00Z',
},
involvedObject: {
kind: 'Deployment',
namespace: 'default',
name: 'test-deployment',
uid: 'test-deployment-uid', // This matches a resource UID
apiVersion: 'apps/v1',
resourceVersion: '2000',
},
reason: 'ScalingReplicaSet',
message: 'Scaled up replica set test-deployment-abc123 to 1',
source: {
component: 'deployment-controller',
},
firstTimestamp: '2023-01-01T00:00:00Z',
lastTimestamp: '2023-01-01T00:00:00Z',
count: 1,
type: 'Normal',
reportingComponent: 'deployment-controller',
reportingInstance: '',
{
name: 'unrelated-pod-123456',
namespace: 'default',
uid: 'event-uid-3',
eventTime: new Date('2023-01-01T00:00:00Z'),
involvedObject: {
kind: 'Pod',
namespace: 'default',
name: 'unrelated-pod',
uid: 'unrelated-pod-uid', // This does NOT match any resource UIDs
},
{
metadata: {
name: 'unrelated-pod-123456',
namespace: 'default',
uid: 'event-uid-3',
resourceVersion: '1002',
creationTimestamp: '2023-01-01T00:00:00Z',
},
involvedObject: {
kind: 'Pod',
namespace: 'default',
name: 'unrelated-pod',
uid: 'unrelated-pod-uid', // This does NOT match any resource UIDs
apiVersion: 'v1',
resourceVersion: '2002',
},
reason: 'Scheduled',
message: 'Successfully assigned unrelated-pod to node',
source: {
component: 'default-scheduler',
},
firstTimestamp: '2023-01-01T00:00:00Z',
lastTimestamp: '2023-01-01T00:00:00Z',
count: 1,
reportingComponent: 'scheduler',
reportingInstance: '',
},
],
};
reason: 'Scheduled',
message: 'Successfully assigned unrelated-pod to node',
type: 'Normal',
firstTimestamp: new Date('2023-01-01T00:00:00Z'),
lastTimestamp: new Date('2023-01-01T00:00:00Z'),
count: 1,
},
];
function renderComponent() {
const user = new UserViewModel({ Username: 'user' });
@ -229,7 +177,7 @@ describe('HelmEventsDatatable', () => {
it('should correctly filter related events using the filterRelatedEvents function', () => {
const filteredEvents = filterRelatedEvents(
mixedEventsResponse.items as Event[],
mixedEventsResponse as Event[],
testResources
);

View file

@ -1,6 +1,6 @@
import { compact } from 'lodash';
import { Event } from 'kubernetes-types/core/v1';
import { Event } from '@/react/kubernetes/queries/types';
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { EventsDatatable } from '@/react/kubernetes/components/EventsDatatable';
import { useEvents } from '@/react/kubernetes/queries/useEvents';

View file

@ -0,0 +1,19 @@
export type Event = {
type: string;
name: string;
reason: string;
message: string;
namespace: string;
eventTime: Date;
kind?: string;
count: number;
lastTimestamp?: Date;
firstTimestamp?: Date;
uid: string;
involvedObject: {
uid: string;
kind?: string;
name: string;
namespace: string;
};
};

View file

@ -1,6 +1,6 @@
import { EventList, Event } from 'kubernetes-types/core/v1';
import { useQuery } from '@tanstack/react-query';
import { Event } from '@/react/kubernetes/queries/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
@ -13,10 +13,7 @@ type RequestOptions = {
/** if undefined, events are fetched at the cluster scope */
namespace?: string;
params?: {
/** https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors */
labelSelector?: string;
/** https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors */
fieldSelector?: string;
resourceId?: string;
};
};
@ -44,13 +41,13 @@ async function getEvents(
): Promise<Event[]> {
const { namespace, params } = options ?? {};
try {
const { data } = await axios.get<EventList>(
const { data } = await axios.get<Event[]>(
buildUrl(environmentId, namespace),
{
params,
}
);
return data.items;
return data;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve events');
}
@ -96,6 +93,6 @@ export function useEventWarningsCount(
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
return namespace
? `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/events`
: `/endpoints/${environmentId}/kubernetes/api/v1/events`;
? `/kubernetes/${environmentId}/namespaces/${namespace}/events`
: `/kubernetes/${environmentId}/events`;
}