diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 3e5d353c96..6acd2a2b05 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -66,6 +66,8 @@ export { default as ExpandableSection } from '@cloudscape-design/components/expa export { default as KeyValuePairs } from '@cloudscape-design/components/key-value-pairs'; export { I18nProvider } from '@cloudscape-design/components/i18n'; export { default as Wizard } from '@cloudscape-design/components/wizard'; +export { default as SegmentedControl } from '@cloudscape-design/components/segmented-control'; +export type { SegmentedControlProps } from '@cloudscape-design/components/segmented-control'; // custom components export { NavigateLink } from './NavigateLink'; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index e11541edba..60fb1c486f 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -182,6 +182,7 @@ "repositories": "Repositories", "runs": "Runs", "tags": "Tags", + "events": "Events", "settings": "Settings", "join": "Join", "leave_confirm_title": "Leave project", @@ -695,6 +696,7 @@ "account_settings": "User settings", "settings": "Settings", "projects": "Projects", + "events": "Events", "create": { "page_title": "Create user", "error_notification": "Create user error", diff --git a/frontend/src/pages/Events/List/ListPage.tsx b/frontend/src/pages/Events/List/ListPage.tsx new file mode 100644 index 0000000000..9117b37bf0 --- /dev/null +++ b/frontend/src/pages/Events/List/ListPage.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button, Header, SpaceBetween } from 'components'; + +import { useBreadcrumbs } from 'hooks'; +import { ROUTES } from 'routes'; + +import { EventList } from './index'; + +export const ListPage: React.FC = () => { + const { t } = useTranslation(); + + useBreadcrumbs([ + { + text: t('navigation.events'), + href: ROUTES.EVENTS.LIST, + }, + ]); + + return ( + { + return ( +
+
+ ); + }} + /> + ); +}; diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index 6a82b3a654..6413c40392 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'; @@ -9,23 +10,7 @@ import { useGetUserListQuery } from 'services/user'; import { filterLastElementByPrefix } from '../helpers'; -type RequestParamsKeys = keyof Pick< - TEventListRequestParams, - | 'target_projects' - | 'target_users' - | 'target_fleets' - | 'target_instances' - | 'target_runs' - | 'target_jobs' - | 'target_volumes' - | 'target_gateways' - | 'target_secrets' - | 'within_projects' - | 'within_fleets' - | 'within_runs' - | 'include_target_types' - | 'actors' ->; +type RequestParamsKeys = keyof TEventListFilters; const filterKeys: Record = { TARGET_PROJECTS: 'target_projects', @@ -75,17 +60,107 @@ const targetTypes = [ { label: 'Secret', value: 'secret' }, ]; -export const useFilters = () => { +const baseFilteringProperties = [ + { + key: filterKeys.TARGET_PROJECTS, + operators: ['='], + propertyLabel: 'Target projects', + groupValuesLabel: 'Project ids', + }, + { + key: filterKeys.TARGET_USERS, + operators: ['='], + propertyLabel: 'Target users', + groupValuesLabel: 'Project ids', + }, + { + key: filterKeys.TARGET_FLEETS, + operators: ['='], + propertyLabel: 'Target fleet IDs', + }, + { + key: filterKeys.TARGET_INSTANCES, + operators: ['='], + propertyLabel: 'Target instance IDs', + }, + { + key: filterKeys.TARGET_RUNS, + operators: ['='], + propertyLabel: 'Target run IDs', + }, + { + key: filterKeys.TARGET_JOBS, + operators: ['='], + propertyLabel: 'Target job IDs', + }, + { + key: filterKeys.TARGET_VOLUMES, + operators: ['='], + propertyLabel: 'Target volume IDs', + }, + { + key: filterKeys.TARGET_GATEWAYS, + operators: ['='], + propertyLabel: 'Target gateway IDs', + }, + { + key: filterKeys.TARGET_SECRETS, + operators: ['='], + propertyLabel: 'Target secret IDs', + }, + + { + key: filterKeys.WITHIN_PROJECTS, + operators: ['='], + propertyLabel: 'Within projects', + groupValuesLabel: 'Project ids', + }, + + { + key: filterKeys.WITHIN_FLEETS, + operators: ['='], + propertyLabel: 'Within fleet IDs', + }, + + { + key: filterKeys.WITHIN_RUNS, + operators: ['='], + propertyLabel: 'Within run IDs', + }, + + { + key: filterKeys.INCLUDE_TARGET_TYPES, + operators: ['='], + propertyLabel: 'Target types', + groupValuesLabel: 'Target type values', + }, + + { + key: filterKeys.ACTORS, + operators: ['='], + propertyLabel: 'Actors', + }, +]; + +export const useFilters = ({ + permanentFilters, + withSearchParams, +}: { + permanentFilters?: Partial; + withSearchParams?: boolean; +}) => { 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 }), ); const clearFilter = () => { - setSearchParams({}); + if (withSearchParams) { + setSearchParams({}); + } setPropertyFilterQuery(EMPTY_QUERY); }; @@ -132,88 +207,6 @@ export const useFilters = () => { setSearchParams(searchParams); }; - const filteringProperties = [ - { - key: filterKeys.TARGET_PROJECTS, - operators: ['='], - propertyLabel: 'Target projects', - groupValuesLabel: 'Project ids', - }, - { - key: filterKeys.TARGET_USERS, - operators: ['='], - propertyLabel: 'Target users', - groupValuesLabel: 'Project ids', - }, - { - key: filterKeys.TARGET_FLEETS, - operators: ['='], - propertyLabel: 'Target fleet IDs', - }, - { - key: filterKeys.TARGET_INSTANCES, - operators: ['='], - propertyLabel: 'Target instance IDs', - }, - { - key: filterKeys.TARGET_RUNS, - operators: ['='], - propertyLabel: 'Target run IDs', - }, - { - key: filterKeys.TARGET_JOBS, - operators: ['='], - propertyLabel: 'Target job IDs', - }, - { - key: filterKeys.TARGET_VOLUMES, - operators: ['='], - propertyLabel: 'Target volume IDs', - }, - { - key: filterKeys.TARGET_GATEWAYS, - operators: ['='], - propertyLabel: 'Target gateway IDs', - }, - { - key: filterKeys.TARGET_SECRETS, - operators: ['='], - propertyLabel: 'Target secret IDs', - }, - - { - key: filterKeys.WITHIN_PROJECTS, - operators: ['='], - propertyLabel: 'Within projects', - groupValuesLabel: 'Project ids', - }, - - { - key: filterKeys.WITHIN_FLEETS, - operators: ['='], - propertyLabel: 'Within fleet IDs', - }, - - { - key: filterKeys.WITHIN_RUNS, - operators: ['='], - propertyLabel: 'Within run IDs', - }, - - { - key: filterKeys.INCLUDE_TARGET_TYPES, - operators: ['='], - propertyLabel: 'Target types', - groupValuesLabel: 'Target type values', - }, - - { - key: filterKeys.ACTORS, - operators: ['='], - propertyLabel: 'Actors', - }, - ]; - const onChangePropertyFilterHandle = ({ tokens, operation }: PropertyFilterProps.Query) => { let filteredTokens = [...tokens]; @@ -225,7 +218,9 @@ export const useFilters = () => { } }); - setSearchParamsHandle({ tokens: filteredTokens }); + if (withSearchParams) { + setSearchParamsHandle({ tokens: filteredTokens }); + } setPropertyFilterQuery({ operation, @@ -237,60 +232,130 @@ export const useFilters = () => { onChangePropertyFilterHandle(detail); }; + const filteringProperties = useMemo(() => { + const permanentFiltersKeysMap = new Map(); + + for (const prefix of onlyOneFilterGroupPrefixes) { + const permanentFilterKey = Object.keys(permanentFilters ?? {}).find((filterKey) => filterKey.startsWith(prefix)); + + if (permanentFilterKey) { + permanentFiltersKeysMap.set(prefix, permanentFilterKey); + } + } + + if (permanentFiltersKeysMap.size === 0) { + return baseFilteringProperties; + } + + return baseFilteringProperties.filter(({ key }) => { + const propertyPrefix = onlyOneFilterGroupPrefixes.find((prefix) => key.startsWith(prefix)); + + if (!propertyPrefix) { + return true; + } + + if (permanentFiltersKeysMap.has(propertyPrefix)) { + return key === permanentFiltersKeysMap.get(propertyPrefix); + } + + return true; + }); + }, [permanentFilters]); + const filteringRequestParams = useMemo(() => { const params = tokensToRequestParams({ tokens: propertyFilterQuery.tokens, arrayFieldKeys: multipleChoiseKeys, }); + const filterParamsWithPermanentFitters = (filterKey: RequestParamsKeys): string[] => { + let paramsFilter = params[filterKey] ?? ''; + const permanentFilter = permanentFilters?.[filterKey] ?? ''; + + if (!Array.isArray(paramsFilter) && typeof paramsFilter === 'object') { + paramsFilter = ''; + } + + if (Array.isArray(paramsFilter) && Array.isArray(permanentFilter)) { + return [...paramsFilter, ...permanentFilter]; + } + + if (Array.isArray(paramsFilter) && !Array.isArray(permanentFilter)) { + return [...paramsFilter, permanentFilter]; + } + + if (!Array.isArray(paramsFilter) && Array.isArray(permanentFilter)) { + return [paramsFilter, ...permanentFilter]; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return [paramsFilter, permanentFilter]; + }; + + const targetProjects = filterParamsWithPermanentFitters(filterKeys.TARGET_PROJECTS) + .map((name: string) => projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id']) + .filter(Boolean); + + const withInProjects = filterParamsWithPermanentFitters(filterKeys.WITHIN_PROJECTS) + .map((name: string) => projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id']) + .filter(Boolean); + + const targetUsers = filterParamsWithPermanentFitters(filterKeys.TARGET_USERS) + .map((name: string) => usersData?.data?.find(({ username }) => username === name)?.['id']) + .filter(Boolean); + + const actors = filterParamsWithPermanentFitters(filterKeys.ACTORS) + .map((name: string) => usersData?.data?.find(({ username }) => username === name)?.['id']) + .filter(Boolean); + + const includeTargetTypes = filterParamsWithPermanentFitters(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, + ]), + ...permanentFilters, ...mappedFields, - } as Partial; - }, [propertyFilterQuery, usersData, projectsData]); + } as TEventListFilters; + }, [propertyFilterQuery, usersData, projectsData, permanentFilters]); return { filteringRequestParams, @@ -299,5 +364,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..bfed27f558 100644 --- a/frontend/src/pages/Events/List/index.tsx +++ b/frontend/src/pages/Events/List/index.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Header, Loader, PropertyFilter, SpaceBetween, Table } from 'components'; +import { Loader, PropertyFilter, Table } from 'components'; +import { TableProps } from 'components'; import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; import { useBreadcrumbs, useInfiniteScroll } from 'hooks'; @@ -14,7 +15,25 @@ import { useFilters } from './hooks/useFilters'; import styles from '../../Runs/List/styles.module.scss'; -export const EventList = () => { +type RenderHeaderArgs = { + refreshAction?: () => void; + disabledRefresh?: boolean; +}; + +type EventListProps = Pick & { + withSearchParams?: boolean; + renderHeader?: (args: RenderHeaderArgs) => React.ReactNode; + permanentFilters?: Partial; + showFilters?: boolean; +}; + +export const EventList: React.FC = ({ + withSearchParams, + permanentFilters, + renderHeader, + showFilters = true, + ...props +}) => { const { t } = useTranslation(); useBreadcrumbs([ @@ -24,12 +43,19 @@ export const EventList = () => { }, ]); - const { filteringRequestParams, propertyFilterQuery, onChangePropertyFilter, filteringOptions, filteringProperties } = - useFilters(); + const { + filteringRequestParams, + propertyFilterQuery, + onChangePropertyFilter, + filteringOptions, + filteringProperties, + isLoadingFilters, + } = useFilters({ permanentFilters, withSearchParams }); 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,52 +73,39 @@ export const EventList = () => { const { columns } = useColumnsDefinitions(); + const loading = isLoadingFilters || isLoading; + 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 b26c921a05..f667319eb2 100644 --- a/frontend/src/pages/Project/Details/index.tsx +++ b/frontend/src/pages/Project/Details/index.tsx @@ -1,15 +1,49 @@ -import React from 'react'; -import { Outlet, useParams } from 'react-router-dom'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet, useMatch, useParams } from 'react-router-dom'; -import { ContentLayout, DetailsHeader } 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 ( - }> - - +
+ }> + {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 new file mode 100644 index 0000000000..3141d6f33a --- /dev/null +++ b/frontend/src/pages/User/Details/Events/index.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { Button, Header, SegmentedControl, 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 paramUserName = params.userName ?? ''; + const navigate = useNavigate(); + const [filterParamName, setFilterParamName] = useState('actors'); + + useBreadcrumbs([ + { + text: t('navigation.account'), + href: ROUTES.USER.LIST, + }, + { + text: paramUserName, + href: ROUTES.USER.DETAILS.FORMAT(paramUserName), + }, + { + text: t('users.events'), + href: ROUTES.USER.EVENTS.FORMAT(paramUserName), + }, + ]); + + const goToEventsPage = () => { + navigate(ROUTES.EVENTS.LIST + `?${filterParamName}=${paramUserName}`); + }; + + return ( + { + return ( +
+ setFilterParamName(detail.selectedId as keyof TEventListFilters)} + options={[ + { text: 'Actor', id: 'actors' }, + { text: 'Target user', id: 'target_users' }, + ]} + /> + + + } + /> + ); + }} + 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 2cb9885ab3..3ce243d978 100644 --- a/frontend/src/pages/User/Details/Projects/index.tsx +++ b/frontend/src/pages/User/Details/Projects/index.tsx @@ -29,15 +29,14 @@ export const UserProjectList: React.FC = () => { text: paramUserName, href: ROUTES.USER.DETAILS.FORMAT(paramUserName), }, + { + text: t('users.projects'), + href: ROUTES.USER.PROJECTS.FORMAT(paramUserName), + }, ]); const renderEmptyMessage = (): React.ReactNode => { - return ( - - ); + return ; }; const filteredData = useMemo(() => { @@ -74,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 8f1b2d393d..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,8 +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 { UserProjectList as UserProjects } from './Projects'; export const UserDetails: React.FC = () => { @@ -63,24 +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), }, 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/pages/User/Details/types.ts b/frontend/src/pages/User/Details/types.ts index 8ced1c3c29..9f2a0680ac 100644 --- a/frontend/src/pages/User/Details/types.ts +++ b/frontend/src/pages/User/Details/types.ts @@ -1,5 +1,7 @@ export enum UserDetailsTabTypeEnum { SETTINGS = 'settings', PROJECTS = 'projects', + EVENTS = 'events', + ACTIVITY = 'activity', BILLING = 'billing', } diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 34a8abaaf0..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 { @@ -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 { UserBilling, 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'; @@ -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: , @@ -258,6 +262,10 @@ export const router = createBrowserRouter([ path: ROUTES.USER.PROJECTS.TEMPLATE, element: , }, + { + path: ROUTES.USER.EVENTS.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 288cef72fc..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`, @@ -181,6 +186,10 @@ export const ROUTES = { TEMPLATE: `/users/:userName/projects`, FORMAT: (userName: string) => buildRoute(ROUTES.USER.PROJECTS.TEMPLATE, { userName }), }, + EVENTS: { + TEMPLATE: `/users/:userName/events`, + FORMAT: (userName: string) => buildRoute(ROUTES.USER.EVENTS.TEMPLATE, { userName }), + }, BILLING: { LIST: { TEMPLATE: `/users/:userName/billing`, diff --git a/frontend/src/types/event.d.ts b/frontend/src/types/event.d.ts index dd0147fe15..0afdb7436f 100644 --- a/frontend/src/types/event.d.ts +++ b/frontend/src/types/event.d.ts @@ -1,6 +1,6 @@ declare type TEventTargetType = 'project' | 'user' | 'fleet' | 'instance' | 'run' | 'job' | 'volume' | 'gateway' | 'secret'; -declare type TEventListRequestParams = Omit & { +declare type TEventListFilters = { prev_recorded_at?: string; target_projects?: string[]; target_users?: string[]; @@ -17,6 +17,7 @@ declare type TEventListRequestParams = Omit & TEventListFilters; declare interface IEventTarget { type: 'project' | 'user' | 'fleet' | 'instance' | 'run' | 'job' | 'volume' | 'gateway' | 'secret';