From c472fe9c1824b9f3d2a440f68ba6290677c9c685 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Mon, 14 Aug 2023 05:09:40 +1200 Subject: [PATCH] refactor(app): app events datatable [EE-5355] (#10024) --- app/kubernetes/react/components/index.ts | 15 +++- .../views/applications/edit/application.html | 11 +-- app/react/components/Badge/Badge.tsx | 2 +- app/react/components/Badge/index.ts | 1 + .../datatables/ExpandableDatatableRow.tsx | 2 +- .../components/datatables/expand-column.tsx | 2 +- .../ApplicationEventsDatatable.tsx | 85 +++++++++++++++++++ .../applications/DetailsView/index.ts | 1 + .../DetailsView/useNamespaceEventsQuery.ts | 51 +++++++++++ .../PlacementsDatatable/columns/status.tsx | 2 + .../EventsDatatable.tsx | 51 +++++++++++ .../columns/date.tsx | 11 +++ .../columns/eventType.tsx | 21 +++++ .../columns/helper.ts | 4 + .../columns/index.ts | 6 ++ .../columns/kind.tsx | 8 ++ .../columns/message.tsx | 5 ++ .../KubernetesEventsDatatable/index.ts | 1 + .../default-kube-datatable-store.ts | 17 ++-- 19 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx create mode 100644 app/react/kubernetes/applications/DetailsView/useNamespaceEventsQuery.ts create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/EventsDatatable.tsx create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/columns/date.tsx create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/columns/eventType.tsx create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/columns/helper.ts create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/columns/index.ts create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/columns/kind.tsx create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/columns/message.tsx create mode 100644 app/react/kubernetes/components/KubernetesEventsDatatable/index.ts diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index 212a9b676..05a4cf07f 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -15,6 +15,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter'; import { ApplicationSummaryWidget, ApplicationDetailsWidget, + ApplicationEventsDatatable, } from '@/react/kubernetes/applications/DetailsView'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withFormValidation } from '@/react-tools/withFormValidation'; @@ -109,9 +110,21 @@ export const ngModule = angular [] ) ) + .component( + 'applicationEventsDatatable', + r2a( + withUIRouter( + withReactQuery(withUserProvider(ApplicationEventsDatatable)) + ), + [] + ) + ) .component( 'kubernetesApplicationPlacementsDatatable', - r2a(withCurrentUser(PlacementsDatatable), ['dataset', 'onRefresh']) + r2a(withUIRouter(withCurrentUser(PlacementsDatatable)), [ + 'dataset', + 'onRefresh', + ]) ); export const componentsModule = ngModule.name; diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index c6996653d..13057f9aa 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -56,16 +56,7 @@ {{ ctrl.state.eventWarningCount }} warning(s) - + diff --git a/app/react/components/Badge/Badge.tsx b/app/react/components/Badge/Badge.tsx index 7fd36e435..f8a851232 100644 --- a/app/react/components/Badge/Badge.tsx +++ b/app/react/components/Badge/Badge.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import { PropsWithChildren } from 'react'; -type BadgeType = 'success' | 'danger' | 'warn' | 'info'; +export type BadgeType = 'success' | 'danger' | 'warn' | 'info'; export interface Props { type?: BadgeType; diff --git a/app/react/components/Badge/index.ts b/app/react/components/Badge/index.ts index 26a9e305c..1deb737d3 100644 --- a/app/react/components/Badge/index.ts +++ b/app/react/components/Badge/index.ts @@ -1 +1,2 @@ export { Badge } from './Badge'; +export type { BadgeType } from './Badge'; diff --git a/app/react/components/datatables/ExpandableDatatableRow.tsx b/app/react/components/datatables/ExpandableDatatableRow.tsx index a624e0d4b..6edc3e821 100644 --- a/app/react/components/datatables/ExpandableDatatableRow.tsx +++ b/app/react/components/datatables/ExpandableDatatableRow.tsx @@ -24,7 +24,7 @@ export function ExpandableDatatableTableRow>({ cells={cells} onClick={expandOnClick ? () => row.toggleExpanded() : undefined} /> - {row.getIsExpanded() && ( + {row.getIsExpanded() && row.getCanExpand() && ( {!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), + }) + ); }