From c86aeaba903bc48de8f72c6936290c08d1616c84 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Tue, 9 Dec 2025 23:31:29 +0300 Subject: [PATCH 1/4] #3309 Events list --- frontend/src/api.ts | 5 + frontend/src/layouts/AppLayout/hooks.ts | 1 + frontend/src/locale/en.json | 10 +- .../List/hooks/useColumnDefinitions.tsx | 129 ++++++++++++++++++ frontend/src/pages/Events/List/index.tsx | 75 ++++++++++ frontend/src/pages/Events/index.tsx | 1 + frontend/src/router.tsx | 7 + frontend/src/routes.ts | 4 + frontend/src/services/events.ts | 30 ++++ frontend/src/store.ts | 3 + frontend/src/types/event.d.ts | 33 +++++ frontend/staticServer.js | 6 + 12 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx create mode 100644 frontend/src/pages/Events/List/index.tsx create mode 100644 frontend/src/pages/Events/index.tsx create mode 100644 frontend/src/services/events.ts create mode 100644 frontend/src/types/event.d.ts diff --git a/frontend/src/api.ts b/frontend/src/api.ts index b0266e74ea..b57862d33a 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -54,6 +54,11 @@ export const API = { PORTAL_SESSION: (username: string) => `${API.USER_BILLING.BASE(username)}/portal_session`, }, + EVENTS: { + BASE: () => `${API.BASE()}/events`, + LIST: () => `${API.EVENTS.BASE()}/list`, + }, + PROJECTS: { BASE: () => `${API.BASE()}/projects`, LIST: () => `${API.PROJECTS.BASE()}/list`, diff --git a/frontend/src/layouts/AppLayout/hooks.ts b/frontend/src/layouts/AppLayout/hooks.ts index ef53a0082e..a305317d50 100644 --- a/frontend/src/layouts/AppLayout/hooks.ts +++ b/frontend/src/layouts/AppLayout/hooks.ts @@ -29,6 +29,7 @@ export const useSideNavigation = () => { { type: 'link', text: t('navigation.fleets'), href: ROUTES.FLEETS.LIST }, { type: 'link', text: t('navigation.instances'), href: ROUTES.INSTANCES.LIST }, { type: 'link', text: t('navigation.volumes'), href: ROUTES.VOLUMES.LIST }, + { type: 'link', text: t('navigation.events'), href: ROUTES.EVENTS.LIST }, { type: 'link', text: t('navigation.project_other'), href: ROUTES.PROJECT.LIST }, isGlobalAdmin && { diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 7d32a7545d..9b7f85b458 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -82,7 +82,8 @@ "resources": "Resources", "volumes": "Volumes", "instances": "Instances", - "offers": "Offers" + "offers": "Offers", + "events": "Events" }, "backend": { @@ -616,6 +617,13 @@ } }, + "events": { + "recorded_at": "Recorded At", + "actor": "Actor", + "targets": "Targets", + "message": "Message" + }, + "users": { "page_title": "Users", "search_placeholder": "Find members", diff --git a/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx b/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx new file mode 100644 index 0000000000..e5809f221c --- /dev/null +++ b/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { format } from 'date-fns'; + +import { NavigateLink, TableProps } from 'components'; + +import { DATE_TIME_FORMAT } from 'consts'; +import { ROUTES } from 'routes'; + +export const useColumnsDefinitions = () => { + const { t } = useTranslation(); + + const columns: TableProps.ColumnDefinition[] = [ + { + id: 'recorded_at', + header: t('events.recorded_at'), + cell: (item) => format(new Date(item.recorded_at), DATE_TIME_FORMAT), + }, + { + id: 'actor', + header: t('events.actor'), + cell: (item) => + item.actor_user ? ( + {item.actor_user} + ) : ( + '-' + ), + }, + { + id: 'target', + header: t('events.targets'), + cell: (item) => { + return item.targets.map((target) => { + switch (target.type) { + case 'project': + return ( +
+ project{' '} + {target.project_name && ( + + {target.project_name} + + )} +
+ ); + + case 'fleet': + return ( +
+ fleet{' '} + {target.project_name && ( + + {target.project_name} + + )} + / + + {target.name} + +
+ ); + + case 'user': + return ( +
+ user{' '} + {target.name} +
+ ); + + case 'instance': + return ( +
+ instance{' '} + {target.project_name && ( + + {target.project_name} + + )} + /{target.name} +
+ ); + + case 'run': + return ( +
+ run{' '} + {target.project_name && ( + + {target.project_name} + + )} + / + + {target.name} + +
+ ); + + case 'job': + return ( +
+ job{' '} + {target.project_name && ( + + {target.project_name} + + )} + /{target.name} +
+ ); + + default: + return '---'; + } + }); + }, + }, + { + id: 'message', + header: t('events.message'), + cell: ({ message }) => message, + }, + ]; + + return { columns } as const; +}; diff --git a/frontend/src/pages/Events/List/index.tsx b/frontend/src/pages/Events/List/index.tsx new file mode 100644 index 0000000000..86a2b2b4a5 --- /dev/null +++ b/frontend/src/pages/Events/List/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button, Header, Loader, SpaceBetween, Table } from 'components'; + +import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; +import { useBreadcrumbs, useInfiniteScroll } from 'hooks'; +import { useCollection } from 'hooks'; +import { ROUTES } from 'routes'; +import { useLazyGetAllEventsQuery } from 'services/events'; + +import { useColumnsDefinitions } from './hooks/useColumnDefinitions'; + +export const EventList = () => { + const { t } = useTranslation(); + + useBreadcrumbs([ + { + text: t('navigation.events'), + href: ROUTES.EVENTS.LIST, + }, + ]); + + const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ + useLazyQuery: useLazyGetAllEventsQuery, + args: { limit: DEFAULT_TABLE_PAGE_SIZE }, + + getPaginationParams: (lastEvent) => ({ + prev_recorded_at: lastEvent.recorded_at, + prev_id: lastEvent.id, + }), + }); + + const { items, collectionProps } = useCollection(data, { + filtering: { + // empty: renderEmptyMessage(), + // noMatch: renderNoMatchMessage(), + }, + selection: {}, + }); + + const { columns } = useColumnsDefinitions(); + + return ( + +