diff --git a/frontend/src/hooks/useInfiniteScroll.ts b/frontend/src/hooks/useInfiniteScroll.ts index 3a3813ff92..b29909f367 100644 --- a/frontend/src/hooks/useInfiniteScroll.ts +++ b/frontend/src/hooks/useInfiniteScroll.ts @@ -14,6 +14,7 @@ type UseInfinityParams = { useLazyQuery: UseLazyQuery, any>>; args: { limit?: number } & Args; getPaginationParams: (listItem: DataItem) => Partial; + skip?: boolean; // options?: UseQueryStateOptions, Record>; }; @@ -22,6 +23,7 @@ export const useInfiniteScroll = ({ getPaginationParams, // options, args, + skip, }: UseInfinityParams) => { const [data, setData] = useState>([]); const scrollElement = useRef(document.documentElement); @@ -55,14 +57,14 @@ export const useInfiniteScroll = ({ }; useEffect(() => { - if (!isEqual(argsProp, lastArgsProps.current)) { + if (!isEqual(argsProp, lastArgsProps.current) && !skip) { getEmptyList(); lastArgsProps.current = argsProp as Args; } - }, [argsProp, lastArgsProps]); + }, [argsProp, lastArgsProps, skip]); const getMore = async () => { - if (isLoadingRef.current || disabledMore) { + if (isLoadingRef.current || disabledMore || skip) { return; } @@ -83,7 +85,9 @@ export const useInfiniteScroll = ({ console.log(e); } - isLoadingRef.current = false; + setTimeout(() => { + isLoadingRef.current = false; + }, 10); }; useLayoutEffect(() => { @@ -106,6 +110,7 @@ export const useInfiniteScroll = ({ const scrollPositionFromBottom = element.scrollHeight - (element.clientHeight + element.scrollTop); if (scrollPositionFromBottom < SCROLL_POSITION_GAP) { + console.log('test', element.scrollHeight); getMore().catch(console.log); } }, [disabledMore, getMore]); diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 3281ba8f4c..7c07a5f938 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -52,7 +52,8 @@ "refresh": "Refresh", "quickstart": "Quickstart", "ask_ai": "Ask AI", - "new": "New" + "new": "New", + "full_view": "Full view" }, "auth": { diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index 5ef714c763..56aa1f67df 100644 --- a/frontend/src/pages/Events/List/hooks/useFilters.ts +++ b/frontend/src/pages/Events/List/hooks/useFilters.ts @@ -54,7 +54,14 @@ const multipleChoiseKeys: RequestParamsKeys[] = [ 'actors', ]; -const targetTypes = ['project', 'user', 'fleet', 'instance', 'run', 'job']; +const targetTypes = [ + { label: 'Project', value: 'project' }, + { label: 'User', value: 'user' }, + { label: 'Fleet', value: 'fleet' }, + { label: 'Instance', value: 'instance' }, + { label: 'Run', value: 'run' }, + { label: 'Job', value: 'job' }, +]; export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); @@ -100,7 +107,7 @@ export const useFilters = () => { targetTypes?.forEach((targetType) => { options.push({ propertyKey: filterKeys.INCLUDE_TARGET_TYPES, - value: targetType, + value: targetType.label, }); }); @@ -117,53 +124,53 @@ export const useFilters = () => { { key: filterKeys.TARGET_PROJECTS, operators: ['='], - propertyLabel: 'Target Projects', + propertyLabel: 'Target projects', groupValuesLabel: 'Project ids', }, { key: filterKeys.TARGET_USERS, operators: ['='], - propertyLabel: 'Target Users', + propertyLabel: 'Target users', groupValuesLabel: 'Project ids', }, { key: filterKeys.TARGET_FLEETS, operators: ['='], - propertyLabel: 'Target Fleets', + propertyLabel: 'Target fleets', }, { key: filterKeys.TARGET_INSTANCES, operators: ['='], - propertyLabel: 'Target Instances', + propertyLabel: 'Target instances', }, { key: filterKeys.TARGET_RUNS, operators: ['='], - propertyLabel: 'Target Runs', + propertyLabel: 'Target runs', }, { key: filterKeys.TARGET_JOBS, operators: ['='], - propertyLabel: 'Target Jobs', + propertyLabel: 'Target jobs', }, { key: filterKeys.WITHIN_PROJECTS, operators: ['='], - propertyLabel: 'Within Projects', + propertyLabel: 'Within projects', groupValuesLabel: 'Project ids', }, { key: filterKeys.WITHIN_FLEETS, operators: ['='], - propertyLabel: 'Within Fleets', + propertyLabel: 'Within fleets', }, { key: filterKeys.WITHIN_RUNS, operators: ['='], - propertyLabel: 'Within Runs', + propertyLabel: 'Within runs', }, { @@ -240,6 +247,14 @@ export const useFilters = () => { ), } : {}), + + ...(params[filterKeys.INCLUDE_TARGET_TYPES] && Array.isArray(params[filterKeys.INCLUDE_TARGET_TYPES]) + ? { + [filterKeys.INCLUDE_TARGET_TYPES]: params[filterKeys.INCLUDE_TARGET_TYPES]?.map( + (selectedLabel: string) => targetTypes?.find(({ label }) => label === selectedLabel)?.['value'], + ), + } + : {}), }; return { diff --git a/frontend/src/pages/Fleets/Details/Events/index.tsx b/frontend/src/pages/Fleets/Details/Events/index.tsx new file mode 100644 index 0000000000..9a81c7dec3 --- /dev/null +++ b/frontend/src/pages/Fleets/Details/Events/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import Button from '@cloudscape-design/components/button'; + +import { Header, Loader, Table } from 'components'; + +import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; +import { useCollection, useInfiniteScroll } from 'hooks'; +import { ROUTES } from 'routes'; +import { useLazyGetAllEventsQuery } from 'services/events'; + +import { useColumnsDefinitions } from 'pages/Events/List/hooks/useColumnDefinitions'; + +export const EventsList = () => { + const { t } = useTranslation(); + const params = useParams(); + const paramFleetId = params.fleetId ?? ''; + const navigate = useNavigate(); + + const { data, isLoading, isLoadingMore } = useInfiniteScroll({ + useLazyQuery: useLazyGetAllEventsQuery, + args: { limit: DEFAULT_TABLE_PAGE_SIZE, within_fleets: [paramFleetId] }, + + getPaginationParams: (lastEvent) => ({ + prev_recorded_at: lastEvent.recorded_at, + prev_id: lastEvent.id, + }), + }); + + const { items, collectionProps } = useCollection(data, { + selection: {}, + }); + + const goToFullView = () => { + navigate(ROUTES.EVENTS.LIST + `?within_fleets=${paramFleetId}`); + }; + + const { columns } = useColumnsDefinitions(); + + return ( + {t('common.full_view')}}> + {t('navigation.events')} + + } + footer={} + /> + ); +}; diff --git a/frontend/src/pages/Fleets/Details/FleetDetails/index.tsx b/frontend/src/pages/Fleets/Details/FleetDetails/index.tsx new file mode 100644 index 0000000000..19d818c236 --- /dev/null +++ b/frontend/src/pages/Fleets/Details/FleetDetails/index.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { format } from 'date-fns'; + +import { Box, ColumnLayout, Container, Header, Loader, NavigateLink, StatusIndicator } from 'components'; + +import { DATE_TIME_FORMAT } from 'consts'; +import { getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet'; +import { ROUTES } from 'routes'; +import { useGetFleetDetailsQuery } from 'services/fleet'; + +export const FleetDetails = () => { + const { t } = useTranslation(); + const params = useParams(); + const paramFleetId = params.fleetId ?? ''; + const paramProjectName = params.projectName ?? ''; + + const { data, isLoading } = useGetFleetDetailsQuery( + { + projectName: paramProjectName, + fleetId: paramFleetId, + }, + { + refetchOnMountOrArgChange: true, + }, + ); + + const renderPrice = (fleet: IFleet) => { + const price = getFleetPrice(fleet); + + if (typeof price === 'number') return `$${price}`; + + return '-'; + }; + + return ( + <> + {isLoading && ( + + + + )} + + {data && ( + {t('common.general')}}> + +
+ {t('fleets.fleet')} +
{data.name}
+
+ +
+ {t('fleets.instances.status')} + +
+ + {t(`fleets.statuses.${data.status}`)} + +
+
+ +
+ {t('fleets.instances.project')} + +
+ + {data.project_name} + +
+
+ +
+ {t('fleets.instances.title')} + +
+ + {getFleetInstancesLinkText(data)} + +
+
+ +
+ {t('fleets.instances.started')} +
{format(new Date(data.created_at), DATE_TIME_FORMAT)}
+
+ +
+ {t('fleets.instances.price')} +
{renderPrice(data)}
+
+
+
+ )} + + ); +}; diff --git a/frontend/src/pages/Fleets/Details/index.tsx b/frontend/src/pages/Fleets/Details/index.tsx index e487f7a2c9..d3690fcff2 100644 --- a/frontend/src/pages/Fleets/Details/index.tsx +++ b/frontend/src/pages/Fleets/Details/index.tsx @@ -1,29 +1,22 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; -import { format } from 'date-fns'; +import { Outlet, useNavigate, useParams } from 'react-router-dom'; -import { - Box, - Button, - ColumnLayout, - Container, - ContentLayout, - DetailsHeader, - Header, - Loader, - NavigateLink, - StatusIndicator, -} from 'components'; +import { Button, ContentLayout, DetailsHeader, Tabs } from 'components'; + +enum CodeTab { + Details = 'details', + Events = 'events', +} -import { DATE_TIME_FORMAT } from 'consts'; import { useBreadcrumbs } from 'hooks'; -import { getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet'; import { ROUTES } from 'routes'; import { useGetFleetDetailsQuery } from 'services/fleet'; import { useDeleteFleet } from '../List/useDeleteFleet'; +import styles from './styles.module.scss'; + export const FleetDetails: React.FC = () => { const { t } = useTranslation(); const params = useParams(); @@ -33,7 +26,7 @@ export const FleetDetails: React.FC = () => { const { deleteFleets, isDeleting } = useDeleteFleet(); - const { data, isLoading } = useGetFleetDetailsQuery( + const { data } = useGetFleetDetailsQuery( { projectName: paramProjectName, fleetId: paramFleetId, @@ -72,87 +65,42 @@ export const FleetDetails: React.FC = () => { .catch(console.log); }; - const renderPrice = (fleet: IFleet) => { - const price = getFleetPrice(fleet); - - if (typeof price === 'number') return `$${price}`; - - return '-'; - }; - const isDisabledDeleteButton = !data || isDeleting; return ( - - - - } +
+ + + + } + /> + } + > + - } - > - {isLoading && ( - - - - )} - - {data && ( - {t('common.general')}}> - -
- {t('fleets.fleet')} -
{data.name}
-
- -
- {t('fleets.instances.status')} - -
- - {t(`fleets.statuses.${data.status}`)} - -
-
- -
- {t('fleets.instances.project')} - -
- - {data.project_name} - -
-
- -
- {t('fleets.instances.title')} - -
- - {getFleetInstancesLinkText(data)} - -
-
- -
- {t('fleets.instances.started')} -
{format(new Date(data.created_at), DATE_TIME_FORMAT)}
-
-
- {t('fleets.instances.price')} -
{renderPrice(data)}
-
-
-
- )} -
+ + +
); }; diff --git a/frontend/src/pages/Fleets/Details/styles.module.scss b/frontend/src/pages/Fleets/Details/styles.module.scss new file mode 100644 index 0000000000..1a7d41a9c5 --- /dev/null +++ b/frontend/src/pages/Fleets/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/Runs/Details/Events/List/index.tsx b/frontend/src/pages/Runs/Details/Events/List/index.tsx new file mode 100644 index 0000000000..79ccb54436 --- /dev/null +++ b/frontend/src/pages/Runs/Details/Events/List/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import Button from '@cloudscape-design/components/button'; + +import { Header, Loader, Table } from 'components'; + +import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; +import { useCollection, useInfiniteScroll } from 'hooks'; +import { ROUTES } from 'routes'; +import { useLazyGetAllEventsQuery } from 'services/events'; + +import { useColumnsDefinitions } from 'pages/Events/List/hooks/useColumnDefinitions'; + +export const EventsList = () => { + const { t } = useTranslation(); + const params = useParams(); + const paramRunId = params.runId ?? ''; + const navigate = useNavigate(); + + const { data, isLoading, isLoadingMore } = useInfiniteScroll({ + useLazyQuery: useLazyGetAllEventsQuery, + args: { limit: DEFAULT_TABLE_PAGE_SIZE, within_runs: [paramRunId] }, + + getPaginationParams: (lastEvent) => ({ + prev_recorded_at: lastEvent.recorded_at, + prev_id: lastEvent.id, + }), + }); + + const { items, collectionProps } = useCollection(data, { + selection: {}, + }); + + const goToFullView = () => { + navigate(ROUTES.EVENTS.LIST + `?within_runs=${paramRunId}`); + }; + + const { columns } = useColumnsDefinitions(); + + return ( +
{t('common.full_view')}}> + {t('navigation.events')} + + } + footer={} + /> + ); +}; diff --git a/frontend/src/pages/Runs/Details/Jobs/Details/index.tsx b/frontend/src/pages/Runs/Details/Jobs/Details/index.tsx index da44e7ea2c..ffdc2d460c 100644 --- a/frontend/src/pages/Runs/Details/Jobs/Details/index.tsx +++ b/frontend/src/pages/Runs/Details/Jobs/Details/index.tsx @@ -15,6 +15,7 @@ enum CodeTab { Details = 'details', Metrics = 'metrics', Logs = 'logs', + Events = 'Events', } export const JobDetailsPage: React.FC = () => { @@ -97,6 +98,15 @@ export const JobDetailsPage: React.FC = () => { paramJobName, ), }, + { + label: 'Events', + id: CodeTab.Events, + href: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.JOBS.DETAILS.EVENTS.FORMAT( + paramProjectName, + paramRunId, + paramJobName, + ), + }, ]} /> diff --git a/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx b/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx new file mode 100644 index 0000000000..48adc56364 --- /dev/null +++ b/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx @@ -0,0 +1,78 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import Button from '@cloudscape-design/components/button'; + +import { Header, Loader, Table } from 'components'; + +import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; +import { useCollection, useInfiniteScroll } from 'hooks'; +import { useLazyGetAllEventsQuery } from 'services/events'; + +import { useColumnsDefinitions } from 'pages/Events/List/hooks/useColumnDefinitions'; + +import { ROUTES } from '../../../../../routes'; +import { useGetRunQuery } from '../../../../../services/run'; + +export const EventsList = () => { + const { t } = useTranslation(); + const params = useParams(); + const paramProjectName = params.projectName ?? ''; + const paramRunId = params.runId ?? ''; + const paramJobName = params.jobName ?? ''; + const navigate = useNavigate(); + + const { data: runData, isLoading: isLoadingRun } = useGetRunQuery({ + project_name: paramProjectName, + id: paramRunId, + }); + + const jobId = useMemo(() => { + if (!runData) return; + + return runData.jobs.find((job) => job.job_spec.job_name === paramJobName)?.job_submissions?.[0]?.id; + }, [runData]); + + const { data, isLoading, isLoadingMore } = useInfiniteScroll({ + useLazyQuery: useLazyGetAllEventsQuery, + args: { limit: DEFAULT_TABLE_PAGE_SIZE, target_jobs: jobId ? [jobId] : undefined }, + skip: !jobId, + + getPaginationParams: (lastEvent) => ({ + prev_recorded_at: lastEvent.recorded_at, + prev_id: lastEvent.id, + }), + }); + + const goToFullView = () => { + navigate(ROUTES.EVENTS.LIST + `?target_jobs=${jobId}`); + }; + + const { items, collectionProps } = useCollection(data, { + selection: {}, + }); + + const { columns } = useColumnsDefinitions(); + + return ( +
+ {t('common.full_view')} + + } + > + {t('navigation.events')} + + } + footer={} + /> + ); +}; diff --git a/frontend/src/pages/Runs/Details/RunDetails/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/index.tsx index 1547fa8867..c00b2ce9d2 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/index.tsx @@ -25,6 +25,7 @@ import { getRunListItemServiceUrl, getRunListItemSpot, } from '../../List/helpers'; +import { EventsList } from '../Events/List'; import { JobList } from '../Jobs/List'; import { ConnectToRunWithDevEnvConfiguration } from './ConnectToRunWithDevEnvConfiguration'; @@ -202,6 +203,8 @@ export const RunDetails = () => { runPriority={getRunPriority(runData)} /> )} + + {runData.jobs.length > 1 && } ); }; diff --git a/frontend/src/pages/Runs/Details/constants.ts b/frontend/src/pages/Runs/Details/constants.ts new file mode 100644 index 0000000000..1bf4bc69c0 --- /dev/null +++ b/frontend/src/pages/Runs/Details/constants.ts @@ -0,0 +1,6 @@ +export enum CodeTab { + Details = 'details', + Metrics = 'metrics', + Logs = 'logs', + Events = 'events', +} diff --git a/frontend/src/pages/Runs/Details/index.tsx b/frontend/src/pages/Runs/Details/index.tsx index f68c98fa17..78e9850c8e 100644 --- a/frontend/src/pages/Runs/Details/index.tsx +++ b/frontend/src/pages/Runs/Details/index.tsx @@ -15,15 +15,10 @@ import { isAvailableStoppingForRun, // isAvailableDeletingForRun, } from '../utils'; +import { CodeTab } from './constants'; import styles from './styles.module.scss'; -enum CodeTab { - Details = 'details', - Metrics = 'metrics', - Logs = 'logs', -} - export const RunDetailsPage: React.FC = () => { const { t } = useTranslation(); // const navigate = useNavigate(); @@ -189,6 +184,11 @@ export const RunDetailsPage: React.FC = () => { id: CodeTab.Metrics, href: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.METRICS.FORMAT(paramProjectName, paramRunId), }, + { + label: 'Events', + id: CodeTab.Events, + href: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.EVENTS.FORMAT(paramProjectName, paramRunId), + }, ]} /> )} diff --git a/frontend/src/pages/Runs/index.ts b/frontend/src/pages/Runs/index.ts index 5e30508fed..4e97fd2e09 100644 --- a/frontend/src/pages/Runs/index.ts +++ b/frontend/src/pages/Runs/index.ts @@ -2,6 +2,7 @@ export { RunList } from './List'; export { RunDetailsPage } from './Details'; export { RunDetails } from './Details/RunDetails'; export { JobMetrics } from './Details/Jobs/Metrics'; +export { EventsList } from './Details/Events/List'; export { JobLogs } from './Details/Logs'; export { Artifacts } from './Details/Artifacts'; export { CreateDevEnvironment } from './CreateDevEnvironment'; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 4a75bbf510..1bba4cb161 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -11,14 +11,25 @@ import { LoginByOktaCallback } from 'App/Login/LoginByOktaCallback'; import { TokenLogin } from 'App/Login/TokenLogin'; import { Logout } from 'App/Logout'; import { FleetDetails, FleetList } from 'pages/Fleets'; +import { EventsList as FleetEventsList } from 'pages/Fleets/Details/Events'; +import { FleetDetails as FleetDetailsGeneral } from 'pages/Fleets/Details/FleetDetails'; 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 { BackendAdd, BackendEdit } from 'pages/Project/Backends'; import { AddGateway, EditGateway } from 'pages/Project/Gateways'; -import { CreateDevEnvironment, JobLogs, JobMetrics, RunDetails, RunDetailsPage, RunList } from 'pages/Runs'; +import { + CreateDevEnvironment, + EventsList as RunEvents, + JobLogs, + JobMetrics, + RunDetails, + RunDetailsPage, + RunList, +} from 'pages/Runs'; 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'; @@ -107,6 +118,10 @@ export const router = createBrowserRouter([ path: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.LOGS.TEMPLATE, element: , }, + { + path: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.EVENTS.TEMPLATE, + element: , + }, ], }, { @@ -125,6 +140,10 @@ export const router = createBrowserRouter([ path: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.JOBS.DETAILS.LOGS.TEMPLATE, element: , }, + { + path: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.JOBS.DETAILS.EVENTS.TEMPLATE, + element: , + }, ], }, @@ -180,6 +199,16 @@ export const router = createBrowserRouter([ { path: ROUTES.FLEETS.DETAILS.TEMPLATE, element: , + children: [ + { + index: true, + element: , + }, + { + path: ROUTES.FLEETS.DETAILS.EVENTS.TEMPLATE, + element: , + }, + ], }, // Instances diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index b591af5f67..6bc1fb0e5a 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -33,6 +33,11 @@ export const ROUTES = { FORMAT: (projectName: string, runId: string) => buildRoute(ROUTES.PROJECT.DETAILS.RUNS.DETAILS.METRICS.TEMPLATE, { projectName, runId }), }, + EVENTS: { + TEMPLATE: `/projects/:projectName/runs/:runId/events`, + FORMAT: (projectName: string, runId: string) => + buildRoute(ROUTES.PROJECT.DETAILS.RUNS.DETAILS.EVENTS.TEMPLATE, { projectName, runId }), + }, LOGS: { TEMPLATE: `/projects/:projectName/runs/:runId/logs`, FORMAT: (projectName: string, runId: string) => @@ -65,6 +70,15 @@ export const ROUTES = { jobName, }), }, + EVENTS: { + TEMPLATE: `/projects/:projectName/runs/:runId/jobs/:jobName/events`, + FORMAT: (projectName: string, runId: string, jobName: string) => + buildRoute(ROUTES.PROJECT.DETAILS.RUNS.DETAILS.JOBS.DETAILS.EVENTS.TEMPLATE, { + projectName, + runId, + jobName, + }), + }, }, }, }, @@ -122,6 +136,11 @@ export const ROUTES = { TEMPLATE: `/projects/:projectName/fleets/:fleetId`, FORMAT: (projectName: string, fleetId: string) => buildRoute(ROUTES.FLEETS.DETAILS.TEMPLATE, { projectName, fleetId }), + EVENTS: { + TEMPLATE: `/projects/:projectName/fleets/:fleetId/events`, + FORMAT: (projectName: string, fleetId: string) => + buildRoute(ROUTES.FLEETS.DETAILS.EVENTS.TEMPLATE, { projectName, fleetId }), + }, }, },