From 9c82e48dcf0b54d9322b8d704ae517166c123174 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Mon, 2 Feb 2026 20:21:12 +0300 Subject: [PATCH 1/5] Events UI #3309 Added links to event list from project and user details pages --- frontend/src/locale/en.json | 3 + .../src/pages/Events/List/hooks/useFilters.ts | 66 ++++++++++++------- frontend/src/pages/Events/List/index.tsx | 17 +++-- frontend/src/pages/Project/Details/index.tsx | 19 +++++- frontend/src/pages/User/Details/index.tsx | 13 +++- 5 files changed, 86 insertions(+), 32 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index e11541edba..b8b2d0c35b 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -182,6 +182,7 @@ "repositories": "Repositories", "runs": "Runs", "tags": "Tags", + "events": "Project events", "settings": "Settings", "join": "Join", "leave_confirm_title": "Leave project", @@ -695,6 +696,8 @@ "account_settings": "User settings", "settings": "Settings", "projects": "Projects", + "events": "User events", + "activity": "User activity", "create": { "page_title": "Create user", "error_notification": "Create user error", diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index 6a82b3a654..14c245f374 100644 --- a/frontend/src/pages/Events/List/hooks/useFilters.ts +++ b/frontend/src/pages/Events/List/hooks/useFilters.ts @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { omit } from 'lodash'; import type { PropertyFilterProps } from 'components'; @@ -77,8 +78,8 @@ const targetTypes = [ export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const { data: projectsData } = useGetProjectsQuery({}); - const { data: usersData } = useGetUserListQuery({}); + const { data: projectsData, isLoading: isLoadingProjects } = useGetProjectsQuery({}); + const { data: usersData, isLoading: isLoadingUsers } = useGetUserListQuery({}); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -243,51 +244,65 @@ export const useFilters = () => { arrayFieldKeys: multipleChoiseKeys, }); + const targetProjects = params[filterKeys.TARGET_PROJECTS] + ?.map((name: string) => projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id']) + .filter(Boolean); + + const withInProjects = params[filterKeys.WITHIN_PROJECTS] + ?.map((name: string) => projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id']) + .filter(Boolean); + + const targetUsers = params[filterKeys.TARGET_USERS] + ?.map((name: string) => usersData?.data?.find(({ username }) => username === name)?.['id']) + .filter(Boolean); + + const actors = params[filterKeys.ACTORS] + ?.map((name: string) => usersData?.data?.find(({ username }) => username === name)?.['id']) + .filter(Boolean); + + const includeTargetTypes = params[filterKeys.INCLUDE_TARGET_TYPES] + ?.map((selectedLabel: string) => targetTypes?.find(({ label }) => label === selectedLabel)?.['value']) + .filter(Boolean); + const mappedFields = { - ...(params[filterKeys.TARGET_PROJECTS] && Array.isArray(params[filterKeys.TARGET_PROJECTS]) + ...(targetProjects?.length ? { - [filterKeys.TARGET_PROJECTS]: params[filterKeys.TARGET_PROJECTS]?.map( - (name: string) => - projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id'], - ), + [filterKeys.TARGET_PROJECTS]: targetProjects, } : {}), - ...(params[filterKeys.WITHIN_PROJECTS] && Array.isArray(params[filterKeys.WITHIN_PROJECTS]) + ...(withInProjects?.length ? { - [filterKeys.WITHIN_PROJECTS]: params[filterKeys.WITHIN_PROJECTS]?.map( - (name: string) => - projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id'], - ), + [filterKeys.WITHIN_PROJECTS]: withInProjects, } : {}), - ...(params[filterKeys.TARGET_USERS] && Array.isArray(params[filterKeys.TARGET_USERS]) + ...(targetUsers?.length ? { - [filterKeys.TARGET_USERS]: params[filterKeys.TARGET_USERS]?.map( - (name: string) => usersData?.data?.find(({ username }) => username === name)?.['id'], - ), + [filterKeys.TARGET_USERS]: targetUsers, } : {}), - ...(params[filterKeys.ACTORS] && Array.isArray(params[filterKeys.ACTORS]) + ...(actors?.length ? { - [filterKeys.ACTORS]: params[filterKeys.ACTORS]?.map( - (name: string) => usersData?.data?.find(({ username }) => username === name)?.['id'], - ), + [filterKeys.ACTORS]: actors, } : {}), - ...(params[filterKeys.INCLUDE_TARGET_TYPES] && Array.isArray(params[filterKeys.INCLUDE_TARGET_TYPES]) + ...(includeTargetTypes?.length ? { - [filterKeys.INCLUDE_TARGET_TYPES]: params[filterKeys.INCLUDE_TARGET_TYPES]?.map( - (selectedLabel: string) => targetTypes?.find(({ label }) => label === selectedLabel)?.['value'], - ), + [filterKeys.INCLUDE_TARGET_TYPES]: includeTargetTypes, } : {}), }; return { - ...params, + ...omit(params, [ + filterKeys.TARGET_PROJECTS, + filterKeys.WITHIN_PROJECTS, + filterKeys.TARGET_USERS, + filterKeys.ACTORS, + filterKeys.INCLUDE_TARGET_TYPES, + ]), ...mappedFields, } as Partial; }, [propertyFilterQuery, usersData, projectsData]); @@ -299,5 +314,6 @@ export const useFilters = () => { onChangePropertyFilter, filteringOptions, filteringProperties, + isLoadingFilters: isLoadingProjects || isLoadingUsers, } as const; }; diff --git a/frontend/src/pages/Events/List/index.tsx b/frontend/src/pages/Events/List/index.tsx index fcf979d554..cfb681b3e5 100644 --- a/frontend/src/pages/Events/List/index.tsx +++ b/frontend/src/pages/Events/List/index.tsx @@ -24,12 +24,19 @@ export const EventList = () => { }, ]); - const { filteringRequestParams, propertyFilterQuery, onChangePropertyFilter, filteringOptions, filteringProperties } = - useFilters(); + const { + filteringRequestParams, + propertyFilterQuery, + onChangePropertyFilter, + filteringOptions, + filteringProperties, + isLoadingFilters, + } = useFilters(); const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ useLazyQuery: useLazyGetAllEventsQuery, args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE }, + skip: isLoadingFilters, getPaginationParams: (lastEvent) => ({ prev_recorded_at: lastEvent.recorded_at, @@ -47,13 +54,15 @@ export const EventList = () => { const { columns } = useColumnsDefinitions(); + const loading = isLoadingFilters || isLoading; + return ( {
- + + } + > + {/*{t('navigation.events')}*/} + + ); + }} + permanentFilters={{ [filterParamName]: [paramUserName] }} + showFilters={false} + /> + ); }; diff --git a/frontend/src/pages/User/Details/Projects/index.tsx b/frontend/src/pages/User/Details/Projects/index.tsx index 2663538cd7..3ce243d978 100644 --- a/frontend/src/pages/User/Details/Projects/index.tsx +++ b/frontend/src/pages/User/Details/Projects/index.tsx @@ -36,12 +36,7 @@ export const UserProjectList: React.FC = () => { ]); const renderEmptyMessage = (): React.ReactNode => { - return ( - - ); + return ; }; const filteredData = useMemo(() => { @@ -78,7 +73,6 @@ export const UserProjectList: React.FC = () => { return (
{ }; return ( -
-
- {t('common.edit')} - - } - > - {t('users.account_settings')} -
- + + {t('common.edit')} + + } + > + {t('users.account_settings')} + + } + > {isLoading && } {data && ( @@ -105,6 +107,6 @@ export const Settings: React.FC = () => { )} -
+ ); }; diff --git a/frontend/src/pages/User/Details/index.tsx b/frontend/src/pages/User/Details/index.tsx index 604da97a9a..3236d9acad 100644 --- a/frontend/src/pages/User/Details/index.tsx +++ b/frontend/src/pages/User/Details/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet, useNavigate, useParams } from 'react-router-dom'; -import { Box, ConfirmationDialog, ContentLayout, SpaceBetween, Tabs } from 'components'; +import { Box, ConfirmationDialog, ContentLayout, Tabs } from 'components'; import { DetailsHeader } from 'components'; import { useNotifications /* usePermissionGuard*/ } from 'hooks'; @@ -13,10 +13,11 @@ import { useDeleteUsersMutation, useGetUserQuery } from 'services/user'; // import { GlobalUserRole } from '../../../types'; import { UserDetailsTabTypeEnum } from './types'; +import styles from './styles.module.scss'; + export { Settings as UserSettings } from './Settings'; export { Billing as UserBilling } from './Billing'; export { Events as UserEvents } from './Events'; -export { Activity as UserActivity } from './Activity'; export { UserProjectList as UserProjects } from './Projects'; export const UserDetails: React.FC = () => { @@ -65,36 +66,26 @@ export const UserDetails: React.FC = () => { label: t('users.settings'), id: UserDetailsTabTypeEnum.SETTINGS, href: ROUTES.USER.DETAILS.FORMAT(paramUserName), - content: , }, { label: t('users.projects'), id: UserDetailsTabTypeEnum.PROJECTS, href: ROUTES.USER.PROJECTS.FORMAT(paramUserName), - content: , }, { label: t('users.events'), id: UserDetailsTabTypeEnum.EVENTS, href: ROUTES.USER.EVENTS.FORMAT(paramUserName), - content: , - }, - { - label: t('users.activity'), - id: UserDetailsTabTypeEnum.ACTIVITY, - href: ROUTES.USER.ACTIVITY.FORMAT(paramUserName), - content: , }, process.env.UI_VERSION === 'sky' && { label: t('billing.title'), id: UserDetailsTabTypeEnum.BILLING, href: ROUTES.USER.BILLING.LIST.FORMAT(paramUserName), - content: , }, ].filter(Boolean); return ( - <> +
{ /> } > - - - + + + { onConfirm={deleteUserHandler} confirmButtonLabel={t('common.delete')} /> - +
); }; diff --git a/frontend/src/pages/User/Details/styles.module.scss b/frontend/src/pages/User/Details/styles.module.scss new file mode 100644 index 0000000000..1a7d41a9c5 --- /dev/null +++ b/frontend/src/pages/User/Details/styles.module.scss @@ -0,0 +1,18 @@ +.page { + height: 100%; + + & [class^="awsui_tabs-content"] { + display: none; + } + + & > [class^="awsui_layout"] { + height: 100%; + + & > [class^="awsui_content"] { + display: flex; + flex-direction: column; + gap: 20px; + height: 100%; + } + } +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 6bc91b23e8..95398bdd1c 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -33,7 +33,7 @@ import { RunInspect } from 'pages/Runs/Details/Inspect'; import { JobDetailsPage } from 'pages/Runs/Details/Jobs/Details'; import { EventsList as JobEvents } from 'pages/Runs/Details/Jobs/Events'; import { CreditsHistoryAdd, UserAdd, UserDetails, UserEdit, UserList } from 'pages/User'; -import { UserActivity, UserBilling, UserEvents, UserProjects, UserSettings } from 'pages/User/Details'; +import { UserBilling, UserEvents, UserProjects, UserSettings } from 'pages/User/Details'; import { AuthErrorMessage } from './App/AuthErrorMessage'; import { EventList } from './pages/Events'; @@ -262,10 +262,6 @@ export const router = createBrowserRouter([ path: ROUTES.USER.EVENTS.TEMPLATE, element: , }, - { - path: ROUTES.USER.ACTIVITY.TEMPLATE, - element: , - }, process.env.UI_VERSION === 'sky' && { path: ROUTES.USER.BILLING.LIST.TEMPLATE, element: , diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 765a158f69..76bf1ff11b 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -185,10 +185,6 @@ export const ROUTES = { TEMPLATE: `/users/:userName/events`, FORMAT: (userName: string) => buildRoute(ROUTES.USER.EVENTS.TEMPLATE, { userName }), }, - ACTIVITY: { - TEMPLATE: `/users/:userName/activity`, - FORMAT: (userName: string) => buildRoute(ROUTES.USER.ACTIVITY.TEMPLATE, { userName }), - }, BILLING: { LIST: { TEMPLATE: `/users/:userName/billing`, From 285248dc0f6c8b1a816390fce1abde8ddf0e4876 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Thu, 5 Feb 2026 01:48:36 +0300 Subject: [PATCH 4/5] Events UI #3309 User events refactoring after review --- frontend/src/locale/en.json | 2 +- .../pages/Project/Details/Events/index.tsx | 55 +++++++++++++++++++ frontend/src/pages/Project/Details/index.tsx | 53 ++++++++++++------ .../pages/Project/Details/styles.module.scss | 18 ++++++ frontend/src/pages/Project/index.tsx | 1 + .../src/pages/User/Details/Events/index.tsx | 4 +- frontend/src/router.tsx | 6 +- frontend/src/routes.ts | 5 ++ 8 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 frontend/src/pages/Project/Details/Events/index.tsx create mode 100644 frontend/src/pages/Project/Details/styles.module.scss diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 2af3e4d910..60fb1c486f 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -182,7 +182,7 @@ "repositories": "Repositories", "runs": "Runs", "tags": "Tags", - "events": "Project events", + "events": "Events", "settings": "Settings", "join": "Join", "leave_confirm_title": "Leave project", diff --git a/frontend/src/pages/Project/Details/Events/index.tsx b/frontend/src/pages/Project/Details/Events/index.tsx new file mode 100644 index 0000000000..df18300c52 --- /dev/null +++ b/frontend/src/pages/Project/Details/Events/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { Button, Header, SpaceBetween } from 'components'; + +import { useBreadcrumbs } from 'hooks'; +import { ROUTES } from 'routes'; + +import { EventList } from 'pages/Events/List'; + +export const Events: React.FC = () => { + const { t } = useTranslation(); + const params = useParams(); + const paramProjectName = params.projectName ?? ''; + const navigate = useNavigate(); + + useBreadcrumbs([ + { + text: t('navigation.project_other'), + href: ROUTES.PROJECT.LIST, + }, + { + text: paramProjectName, + href: ROUTES.PROJECT.DETAILS.FORMAT(paramProjectName), + }, + { + text: t('projects.events'), + href: ROUTES.PROJECT.DETAILS.EVENTS.FORMAT(paramProjectName), + }, + ]); + + const goToEventsPage = () => { + navigate(ROUTES.EVENTS.LIST + `?within_projects=${paramProjectName}`); + }; + + return ( + { + return ( +
+ + + } + /> + ); + }} + permanentFilters={{ within_projects: [paramProjectName] }} + showFilters={false} + /> + ); +}; diff --git a/frontend/src/pages/Project/Details/index.tsx b/frontend/src/pages/Project/Details/index.tsx index c59e94685d..f667319eb2 100644 --- a/frontend/src/pages/Project/Details/index.tsx +++ b/frontend/src/pages/Project/Details/index.tsx @@ -1,30 +1,49 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useParams } from 'react-router-dom'; +import { Outlet, useMatch, useParams } from 'react-router-dom'; -import { ContentLayout, DetailsHeader, NavigateLink } from 'components'; +import { ContentLayout, DetailsHeader, Tabs } from 'components'; import { ROUTES } from 'routes'; +import styles from './styles.module.scss'; + export const ProjectDetails: React.FC = () => { const params = useParams(); const paramProjectName = params.projectName ?? ''; const { t } = useTranslation(); + const matchSettings = useMatch(ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(paramProjectName)); + const matchEvents = useMatch(ROUTES.PROJECT.DETAILS.EVENTS.FORMAT(paramProjectName)); + + const tabs: { + label: string; + id: string; + href: string; + }[] = [ + { + label: t('projects.settings'), + id: 'settings', + href: ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(paramProjectName), + }, + { + label: t('projects.events'), + id: 'events', + href: ROUTES.PROJECT.DETAILS.EVENTS.FORMAT(paramProjectName), + }, + ].filter(Boolean); + + const showTabs = useMemo(() => { + return Boolean(matchSettings) || Boolean(matchEvents); + }, [matchSettings, matchEvents]); + return ( - - {t('projects.events')} - - } - /> - } - > - - +
+ }> + {showTabs && } + + + +
); }; diff --git a/frontend/src/pages/Project/Details/styles.module.scss b/frontend/src/pages/Project/Details/styles.module.scss new file mode 100644 index 0000000000..1a7d41a9c5 --- /dev/null +++ b/frontend/src/pages/Project/Details/styles.module.scss @@ -0,0 +1,18 @@ +.page { + height: 100%; + + & [class^="awsui_tabs-content"] { + display: none; + } + + & > [class^="awsui_layout"] { + height: 100%; + + & > [class^="awsui_content"] { + display: flex; + flex-direction: column; + gap: 20px; + height: 100%; + } + } +} diff --git a/frontend/src/pages/Project/index.tsx b/frontend/src/pages/Project/index.tsx index a7bdc1617e..503d4c2228 100644 --- a/frontend/src/pages/Project/index.tsx +++ b/frontend/src/pages/Project/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; export { ProjectList } from './List'; export { ProjectDetails } from './Details'; export { ProjectSettings } from './Details/Settings'; +export { Events as ProjectEvents } from './Details/Events'; export { ProjectAdd } from './Add'; export { CreateProjectWizard } from './CreateWizard'; diff --git a/frontend/src/pages/User/Details/Events/index.tsx b/frontend/src/pages/User/Details/Events/index.tsx index afa1fba6de..b53ec4931a 100644 --- a/frontend/src/pages/User/Details/Events/index.tsx +++ b/frontend/src/pages/User/Details/Events/index.tsx @@ -54,9 +54,7 @@ export const Events: React.FC = () => { } - > - {/*{t('navigation.events')}*/} -
+ /> ); }} permanentFilters={{ [filterParamName]: [paramUserName] }} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 95398bdd1c..a5f2b50bd4 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -17,7 +17,7 @@ import { FleetInspect } from 'pages/Fleets/Details/Inspect'; import { InstanceList } from 'pages/Instances'; import { ModelsList } from 'pages/Models'; import { ModelDetails } from 'pages/Models/Details'; -import { CreateProjectWizard, ProjectAdd, ProjectDetails, ProjectList, ProjectSettings } from 'pages/Project'; +import { CreateProjectWizard, ProjectAdd, ProjectDetails, ProjectEvents, ProjectList, ProjectSettings } from 'pages/Project'; import { BackendAdd, BackendEdit } from 'pages/Project/Backends'; import { AddGateway, EditGateway } from 'pages/Project/Gateways'; import { @@ -86,6 +86,10 @@ export const router = createBrowserRouter([ index: true, element: , }, + { + path: ROUTES.PROJECT.DETAILS.EVENTS.TEMPLATE, + element: , + }, { path: ROUTES.PROJECT.BACKEND.ADD.TEMPLATE, element: , diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 76bf1ff11b..7922354e19 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -23,6 +23,11 @@ export const ROUTES = { FORMAT: (projectName: string) => buildRoute(ROUTES.PROJECT.DETAILS.SETTINGS.TEMPLATE, { projectName }), }, + EVENTS: { + TEMPLATE: `/projects/:projectName/events`, + FORMAT: (projectName: string) => buildRoute(ROUTES.PROJECT.DETAILS.EVENTS.TEMPLATE, { projectName }), + }, + RUNS: { DETAILS: { TEMPLATE: `/projects/:projectName/runs/:runId`, From 2fe47dcfc56d53ef9db41f8dafb47e4b23c68809 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Thu, 5 Feb 2026 12:24:32 +0300 Subject: [PATCH 5/5] Events UI #3309 User events refactoring after review --- frontend/src/pages/User/Details/Events/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/User/Details/Events/index.tsx b/frontend/src/pages/User/Details/Events/index.tsx index b53ec4931a..3141d6f33a 100644 --- a/frontend/src/pages/User/Details/Events/index.tsx +++ b/frontend/src/pages/User/Details/Events/index.tsx @@ -14,7 +14,7 @@ export const Events: React.FC = () => { const params = useParams(); const paramUserName = params.userName ?? ''; const navigate = useNavigate(); - const [filterParamName, setFilterParamName] = useState('target_users'); + const [filterParamName, setFilterParamName] = useState('actors'); useBreadcrumbs([ { @@ -47,8 +47,8 @@ export const Events: React.FC = () => { selectedId={filterParamName} onChange={({ detail }) => setFilterParamName(detail.selectedId as keyof TEventListFilters)} options={[ - { text: 'Target user', id: 'target_users' }, { text: 'Actor', id: 'actors' }, + { text: 'Target user', id: 'target_users' }, ]} />