{!disableSelect && | }
diff --git a/app/react/components/datatables/expand-column.tsx b/app/react/components/datatables/expand-column.tsx
index b293b4dae..33a86d681 100644
--- a/app/react/components/datatables/expand-column.tsx
+++ b/app/react/components/datatables/expand-column.tsx
@@ -9,7 +9,7 @@ export function buildExpandColumn<
return {
id: 'expand',
header: ({ table }) => {
- const hasExpandableItems = table.getExpandedRowModel().rows.length > 0;
+ const hasExpandableItems = table.getCanSomeRowsExpand();
return (
hasExpandableItems && (
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx
new file mode 100644
index 000000000..2f36d7934
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx
@@ -0,0 +1,85 @@
+import { useCurrentStateAndParams } from '@uirouter/react';
+import { useMemo } from 'react';
+
+import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
+
+import { useTableState } from '@@/datatables/useTableState';
+
+import {
+ useApplication,
+ useApplicationPods,
+ useApplicationServices,
+} from '../application.queries';
+import { EventsDatatable } from '../../components/KubernetesEventsDatatable';
+
+import { useNamespaceEventsQuery } from './useNamespaceEventsQuery';
+
+const storageKey = 'k8sAppEventsDatatable';
+const settingsStore = createStore(storageKey, { id: 'Date', desc: true });
+
+export function ApplicationEventsDatatable() {
+ const tableState = useTableState(settingsStore, storageKey);
+ const {
+ params: {
+ namespace,
+ name,
+ 'resource-type': resourceType,
+ endpointId: environmentId,
+ },
+ } = useCurrentStateAndParams();
+
+ const { data: application, ...applicationQuery } = useApplication(
+ environmentId,
+ namespace,
+ name,
+ resourceType
+ );
+ const { data: services, ...servicesQuery } = useApplicationServices(
+ environmentId,
+ namespace,
+ name,
+ application
+ );
+ const { data: pods, ...podsQuery } = useApplicationPods(
+ environmentId,
+ namespace,
+ name,
+ application
+ );
+ const { data: events, ...eventsQuery } = useNamespaceEventsQuery(
+ environmentId,
+ namespace,
+ {
+ autoRefreshRate: tableState.autoRefreshRate * 1000,
+ }
+ );
+
+ // related events are events that have the application id, or the id of a service or pod from the application
+ const relatedEvents = useMemo(() => {
+ const serviceIds = services?.map((service) => service?.metadata?.uid);
+ const podIds = pods?.map((pod) => pod?.metadata?.uid);
+ return (
+ events?.filter(
+ (event) =>
+ event.involvedObject.uid === application?.metadata?.uid ||
+ serviceIds?.includes(event.involvedObject.uid) ||
+ podIds?.includes(event.involvedObject.uid)
+ ) || []
+ );
+ }, [application?.metadata?.uid, events, pods, services]);
+
+ return (
+
+ );
+}
diff --git a/app/react/kubernetes/applications/DetailsView/index.ts b/app/react/kubernetes/applications/DetailsView/index.ts
index 1ee252868..19efd9127 100644
--- a/app/react/kubernetes/applications/DetailsView/index.ts
+++ b/app/react/kubernetes/applications/DetailsView/index.ts
@@ -1,2 +1,3 @@
export { ApplicationSummaryWidget } from './ApplicationSummaryWidget';
export { ApplicationDetailsWidget } from './ApplicationDetailsWidget/ApplicationDetailsWidget';
+export { ApplicationEventsDatatable } from './ApplicationEventsDatatable';
diff --git a/app/react/kubernetes/applications/DetailsView/useNamespaceEventsQuery.ts b/app/react/kubernetes/applications/DetailsView/useNamespaceEventsQuery.ts
new file mode 100644
index 000000000..68c1f30f6
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/useNamespaceEventsQuery.ts
@@ -0,0 +1,51 @@
+import { EventList } from 'kubernetes-types/core/v1';
+import { useQuery } from 'react-query';
+
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { withError } from '@/react-tools/react-query';
+
+async function getNamespaceEvents(
+ environmentId: EnvironmentId,
+ namespace: string,
+ labelSelector?: string
+) {
+ try {
+ const { data } = await axios.get(
+ `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/events`,
+ {
+ params: {
+ labelSelector,
+ },
+ }
+ );
+ return data.items;
+ } catch (e) {
+ throw parseAxiosError(e as Error, 'Unable to retrieve events');
+ }
+}
+
+export function useNamespaceEventsQuery(
+ environmentId: EnvironmentId,
+ namespace: string,
+ options?: { autoRefreshRate?: number },
+ labelSelector?: string
+) {
+ return useQuery(
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'events',
+ namespace,
+ labelSelector,
+ ],
+ () => getNamespaceEvents(environmentId, namespace, labelSelector),
+ {
+ ...withError('Unable to retrieve events'),
+ refetchInterval() {
+ return options?.autoRefreshRate ?? false;
+ },
+ }
+ );
+}
diff --git a/app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/status.tsx b/app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/status.tsx
index e819def02..0d1569cb2 100644
--- a/app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/status.tsx
+++ b/app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/status.tsx
@@ -5,7 +5,9 @@ import { Icon } from '@@/Icon';
import { columnHelper } from './helper';
export const status = columnHelper.accessor('AcceptsApplication', {
+ header: '',
id: 'status',
+ enableSorting: false,
cell: ({ getValue }) => {
const acceptsApplication = getValue();
return (
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/EventsDatatable.tsx b/app/react/kubernetes/components/KubernetesEventsDatatable/EventsDatatable.tsx
new file mode 100644
index 000000000..d59dca043
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/EventsDatatable.tsx
@@ -0,0 +1,51 @@
+import { Event } from 'kubernetes-types/core/v1';
+import { History } from 'lucide-react';
+
+import { IndexOptional } from '@/react/kubernetes/configs/types';
+import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
+
+import { Datatable, TableSettingsMenu } from '@@/datatables';
+import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
+import { TableState } from '@@/datatables/useTableState';
+
+import { columns } from './columns';
+
+type Props = {
+ dataset: Event[];
+ tableState: TableState;
+ isLoading: boolean;
+ 'data-cy': string;
+ noWidget: boolean;
+};
+
+export function EventsDatatable({
+ dataset,
+ tableState,
+ isLoading,
+ 'data-cy': dataCy,
+ noWidget,
+}: Props) {
+ return (
+ >
+ dataset={dataset}
+ columns={columns}
+ settingsManager={tableState}
+ isLoading={isLoading}
+ emptyContentLabel="No event available."
+ title="Events"
+ titleIcon={History}
+ getRowId={(row) => row.metadata?.uid || ''}
+ disableSelect
+ renderTableSettings={() => (
+
+ tableState.setAutoRefreshRate(value)}
+ />
+
+ )}
+ data-cy={dataCy}
+ noWidget={noWidget}
+ />
+ );
+}
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/columns/date.tsx b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/date.tsx
new file mode 100644
index 000000000..d64132e06
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/date.tsx
@@ -0,0 +1,11 @@
+import { formatDate } from '@/portainer/filters/filters';
+
+import { columnHelper } from './helper';
+
+export const date = columnHelper.accessor(
+ (event) => formatDate(event.lastTimestamp || event.eventTime),
+ {
+ header: 'Date',
+ id: 'Date',
+ }
+);
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/columns/eventType.tsx b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/eventType.tsx
new file mode 100644
index 000000000..47b15a544
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/eventType.tsx
@@ -0,0 +1,21 @@
+import { Badge, BadgeType } from '@@/Badge';
+
+import { columnHelper } from './helper';
+
+export const eventType = columnHelper.accessor('type', {
+ header: 'Type',
+ cell: ({ getValue }) => (
+ {getValue()}
+ ),
+});
+
+function getBadgeColor(status?: string): BadgeType {
+ switch (status?.toLowerCase()) {
+ case 'normal':
+ return 'info';
+ case 'warning':
+ return 'warn';
+ default:
+ return 'danger';
+ }
+}
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/columns/helper.ts b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/helper.ts
new file mode 100644
index 000000000..23a453238
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/helper.ts
@@ -0,0 +1,4 @@
+import { createColumnHelper } from '@tanstack/react-table';
+import { Event } from 'kubernetes-types/core/v1';
+
+export const columnHelper = createColumnHelper();
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/columns/index.ts b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/index.ts
new file mode 100644
index 000000000..05ae483d5
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/index.ts
@@ -0,0 +1,6 @@
+import { date } from './date';
+import { kind } from './kind';
+import { eventType } from './eventType';
+import { message } from './message';
+
+export const columns = [date, kind, eventType, message];
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/columns/kind.tsx b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/kind.tsx
new file mode 100644
index 000000000..856d6bf8b
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/kind.tsx
@@ -0,0 +1,8 @@
+import { columnHelper } from './helper';
+
+export const kind = columnHelper.accessor(
+ (event) => event.involvedObject.kind,
+ {
+ header: 'Kind',
+ }
+);
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/columns/message.tsx b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/message.tsx
new file mode 100644
index 000000000..ec3b770cb
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/columns/message.tsx
@@ -0,0 +1,5 @@
+import { columnHelper } from './helper';
+
+export const message = columnHelper.accessor('message', {
+ header: 'Message',
+});
diff --git a/app/react/kubernetes/components/KubernetesEventsDatatable/index.ts b/app/react/kubernetes/components/KubernetesEventsDatatable/index.ts
new file mode 100644
index 000000000..79b6c371a
--- /dev/null
+++ b/app/react/kubernetes/components/KubernetesEventsDatatable/index.ts
@@ -0,0 +1 @@
+export { EventsDatatable } from './EventsDatatable';
diff --git a/app/react/kubernetes/datatables/default-kube-datatable-store.ts b/app/react/kubernetes/datatables/default-kube-datatable-store.ts
index 065d3a6d9..96a8174fe 100644
--- a/app/react/kubernetes/datatables/default-kube-datatable-store.ts
+++ b/app/react/kubernetes/datatables/default-kube-datatable-store.ts
@@ -5,9 +5,16 @@ import {
TableSettings,
} from './DefaultDatatableSettings';
-export function createStore(storageKey: string) {
- return createPersistedStore(storageKey, 'name', (set) => ({
- ...refreshableSettings(set),
- ...systemResourcesSettings(set),
- }));
+export function createStore(
+ storageKey: string,
+ initialSortBy: string | { id: string; desc: boolean } = 'name'
+) {
+ return createPersistedStore(
+ storageKey,
+ initialSortBy,
+ (set) => ({
+ ...refreshableSettings(set),
+ ...systemResourcesSettings(set),
+ })
+ );
}
|