From 55dfd3cd8d04914b4bd0aa3264332be464e3040d Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Mon, 31 Jul 2023 21:52:27 +0300 Subject: [PATCH 1/2] #588 Created All run list page. --- hub/src/api.ts | 5 + hub/src/components/index.ts | 2 + hub/src/layouts/AppLayout/index.tsx | 1 + hub/src/locale/en.json | 7 + hub/src/pages/Runs/List/index.tsx | 15 +- hub/src/pages/Runs/MainList/index.tsx | 357 ++++++++++++++++++ .../pages/Runs/MainList/styles.module.scss | 21 ++ hub/src/pages/Runs/constants.ts | 1 + hub/src/pages/Runs/index.ts | 1 + hub/src/pages/Runs/utils.ts | 12 +- hub/src/router.tsx | 13 +- hub/src/routes.ts | 4 + hub/src/services/run.ts | 45 ++- hub/src/types/run.d.ts | 49 ++- 14 files changed, 503 insertions(+), 30 deletions(-) create mode 100644 hub/src/pages/Runs/MainList/index.tsx create mode 100644 hub/src/pages/Runs/MainList/styles.module.scss diff --git a/hub/src/api.ts b/hub/src/api.ts index 2b8b01b41..6cd76d738 100644 --- a/hub/src/api.ts +++ b/hub/src/api.ts @@ -50,4 +50,9 @@ export const API = { SECRET_DELETE: (projectName: IProject['project_name'], secretName: ISecret['secret_name']) => `${API.BASE()}/project/${projectName}/secrets/${secretName}/delete`, }, + + RUNS: { + BASE: () => `${API.BASE()}/runs`, + LIST: () => `${API.RUNS.BASE()}/list`, + }, }; diff --git a/hub/src/components/index.ts b/hub/src/components/index.ts index ed982e44e..5897a7325 100644 --- a/hub/src/components/index.ts +++ b/hub/src/components/index.ts @@ -25,6 +25,7 @@ export { default as FormUI } from '@cloudscape-design/components/form'; export { default as FormField } from '@cloudscape-design/components/form-field'; export { default as InputCSD } from '@cloudscape-design/components/input'; export { default as SelectCSD } from '@cloudscape-design/components/select'; +export type { SelectProps as SelectCSDProps } from '@cloudscape-design/components/select'; export { default as StatusIndicator } from '@cloudscape-design/components/status-indicator'; export type { StatusIndicatorProps } from '@cloudscape-design/components/status-indicator'; export { default as Popover } from '@cloudscape-design/components/popover'; @@ -34,6 +35,7 @@ export { default as Grid } from '@cloudscape-design/components/grid'; export { default as HelpPanel } from '@cloudscape-design/components/help-panel'; export type { HelpPanelProps } from '@cloudscape-design/components/help-panel'; export { default as TextContent } from '@cloudscape-design/components/text-content'; +export { default as Toggle } from '@cloudscape-design/components/toggle'; export { default as Modal } from '@cloudscape-design/components/modal'; export type { ModalProps } from '@cloudscape-design/components/modal'; export type { TilesProps } from '@cloudscape-design/components/tiles'; diff --git a/hub/src/layouts/AppLayout/index.tsx b/hub/src/layouts/AppLayout/index.tsx index 6198357c3..055474475 100644 --- a/hub/src/layouts/AppLayout/index.tsx +++ b/hub/src/layouts/AppLayout/index.tsx @@ -105,6 +105,7 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { type: 'section-group', title: t('navigation.settings'), items: [ + { type: 'link', text: t('navigation.runs'), href: ROUTES.RUNS.LIST }, { type: 'link', text: t('navigation.projects'), href: ROUTES.PROJECT.LIST }, { type: 'link', text: t('navigation.users'), href: ROUTES.USER.LIST }, ], diff --git a/hub/src/locale/en.json b/hub/src/locale/en.json index d505e829c..a1a22080f 100644 --- a/hub/src/locale/en.json +++ b/hub/src/locale/en.json @@ -39,6 +39,7 @@ "navigation": { "settings": "Settings", + "runs": "Runs", "projects": "Projects", "users": "Users" }, @@ -223,6 +224,11 @@ "empty_message_text": "No runs to display.", "nomatch_message_title": "No matches", "nomatch_message_text": "We can't find a match.", + "project": "Project", + "repo": "Repository", + "project_placeholder": "Filtering by project", + "repo_placeholder": "Filtering by repository", + "active_only": "Active runs", "log": "Logs", "run_name": "Name", "workflow_name": "Workflow", @@ -235,6 +241,7 @@ "artifacts": "Artifacts", "artifacts_count": "Artifacts", "hub_user_name": "User", + "statuses": { "pending": "Pending", "submitted": "Submitted", diff --git a/hub/src/pages/Runs/List/index.tsx b/hub/src/pages/Runs/List/index.tsx index 34c50d7f5..16dfcc142 100644 --- a/hub/src/pages/Runs/List/index.tsx +++ b/hub/src/pages/Runs/List/index.tsx @@ -171,19 +171,28 @@ export const RunList: React.FC = () => { const isDisabledAbortButton = useMemo(() => { return ( - !selectedItems?.length || selectedItems.some((item) => !isAvailableAbortingForRun(item)) || isStopping || isDeleting + !selectedItems?.length || + selectedItems.some((item) => !isAvailableAbortingForRun(item.status)) || + isStopping || + isDeleting ); }, [selectedItems, isStopping, isDeleting]); const isDisabledStopButton = useMemo(() => { return ( - !selectedItems?.length || selectedItems.some((item) => !isAvailableStoppingForRun(item)) || isStopping || isDeleting + !selectedItems?.length || + selectedItems.some((item) => !isAvailableStoppingForRun(item.status)) || + isStopping || + isDeleting ); }, [selectedItems, isStopping, isDeleting]); const isDisabledDeleteButton = useMemo(() => { return ( - !selectedItems?.length || selectedItems.some((item) => !isAvailableDeletingForRun(item)) || isStopping || isDeleting + !selectedItems?.length || + selectedItems.some((item) => !isAvailableDeletingForRun(item.status)) || + isStopping || + isDeleting ); }, [selectedItems, isStopping, isDeleting]); diff --git a/hub/src/pages/Runs/MainList/index.tsx b/hub/src/pages/Runs/MainList/index.tsx new file mode 100644 index 000000000..00a8b24cc --- /dev/null +++ b/hub/src/pages/Runs/MainList/index.tsx @@ -0,0 +1,357 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { get as _get, uniqBy as _uniqBy } from 'lodash'; +import { format } from 'date-fns'; + +import { + Button, + FormField, + Header, + ListEmptyMessage, + NavigateLink, + Pagination, + SelectCSD, + SelectCSDProps, + SpaceBetween, + StatusIndicator, + Table, + TextFilter, + Toggle, +} from 'components'; + +import { DATE_TIME_FORMAT } from 'consts'; +import { useCollection, useNotifications } from 'hooks'; +import { getStatusIconType } from 'libs/run'; +import { ROUTES } from 'routes'; +import { useDeleteRunsMutation, useGetAllRunsQuery, useStopRunsMutation } from 'services/run'; + +import { unfinishedRuns } from '../constants'; +import { isAvailableAbortingForRun, isAvailableDeletingForRun, isAvailableStoppingForRun } from '../utils'; + +import styles from './styles.module.scss'; + +export const SEARCHABLE_COLUMNS = [ + 'run_head.run_name', + 'run_head.job_heads.[0].configuration_path', + 'run_head.job_heads.[0].instance_type', + 'run_head.hub_user_name', + 'run_head.status', +]; + +export const List: React.FC = () => { + const { t } = useTranslation(); + const [pushNotification] = useNotifications(); + const [selectedProject, setSelectedProject] = useState(null); + const [selectedRepo, setSelectedRepo] = useState(null); + const [onlyActive, setOnlyActive] = useState(false); + + const isFirstRunsFetchRef = useRef(true); + + const { data, isLoading } = useGetAllRunsQuery(undefined, { + pollingInterval: 10000, + }); + + const [stopRun, { isLoading: isStopping }] = useStopRunsMutation(); + const [deleteRun, { isLoading: isDeleting }] = useDeleteRunsMutation(); + + const COLUMN_DEFINITIONS = [ + { + id: 'run_name', + header: t('projects.run.run_name'), + cell: (item: IRunListItem) => ( + + {item.run_head.run_name} + + ), + }, + { + id: 'configuration', + header: `${t('projects.run.configuration')}`, + cell: (item: IRunListItem) => item.run_head.job_heads?.[0].configuration_path, + }, + { + id: 'instance', + header: `${t('projects.run.instance')}`, + cell: (item: IRunListItem) => item.run_head.job_heads?.[0].instance_type, + }, + { + id: 'hub_user_name', + header: `${t('projects.run.hub_user_name')}`, + cell: (item: IRunListItem) => item.run_head.hub_user_name, + }, + { + id: 'status', + header: t('projects.run.status'), + cell: (item: IRunListItem) => ( + + {t(`projects.run.statuses.${item.run_head.status}`)} + + ), + }, + { + id: 'submitted_at', + header: t('projects.run.submitted_at'), + cell: (item: IRunListItem) => format(new Date(item.run_head.submitted_at), DATE_TIME_FORMAT), + }, + { + id: 'artifacts', + header: t('projects.run.artifacts_count'), + cell: (item: IRunListItem) => item.run_head.artifact_heads?.length ?? '-', + }, + ]; + + useEffect(() => { + if (data && isFirstRunsFetchRef.current) { + isFirstRunsFetchRef.current = false; + const hasUnfinished = data.some((run) => unfinishedRuns.includes(run.run_head.status)); + setOnlyActive(hasUnfinished); + } + }, [data]); + + const clearFilter = () => { + actions.setFiltering(''); + setSelectedProject(null); + setSelectedRepo(null); + setOnlyActive(false); + }; + const renderEmptyMessage = (): React.ReactNode => { + return ( + + ); + }; + + const renderNoMatchMessage = (): React.ReactNode => { + return ( + + + + ); + }; + + const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps } = useCollection(data ?? [], { + filtering: { + empty: renderEmptyMessage(), + noMatch: renderNoMatchMessage(), + + filteringFunction: (runItem, filteringText) => { + const filteringTextLowerCase = filteringText.toLowerCase(); + + if (selectedProject?.value && runItem.project !== selectedProject.value) return false; + + if (selectedRepo?.value && runItem.repo.repo_id !== selectedRepo.value) return false; + + if (onlyActive && !unfinishedRuns.includes(runItem.run_head.status)) return false; + + return SEARCHABLE_COLUMNS.map((key) => _get(runItem, key)).some( + (value) => typeof value === 'string' && value.toLowerCase().indexOf(filteringTextLowerCase) > -1, + ); + }, + }, + pagination: { pageSize: 20 }, + selection: {}, + }); + + const { selectedItems } = collectionProps; + + const projectOptions = useMemo(() => { + if (!data?.length) return []; + + return _uniqBy( + data.map((run) => ({ label: run.project, value: run.project })), + (option) => option.value, + ); + }, [data]); + + const repoOptions = useMemo(() => { + if (!data?.length) return []; + + return _uniqBy( + data.map((run) => ({ label: run.repo.repo_info.repo_name, value: run.repo.repo_id })), + (option) => option.value, + ); + }, [data]); + + const abortClickHandle = () => { + if (!selectedItems?.length) return; + + Promise.all( + selectedItems.map((item) => + stopRun({ + name: item.project, + repo_id: item.repo.repo_id, + run_names: [item.run_head.run_name], + abort: true, + }).unwrap(), + ), + ) + .then(() => actions.setSelectedItems([])) + .catch((error) => { + pushNotification({ + type: 'error', + content: t('common.server_error', { error: error?.error }), + }); + }); + }; + + const stopClickHandle = () => { + if (!selectedItems?.length) return; + + Promise.all( + selectedItems.map((item) => + stopRun({ + name: item.project, + repo_id: item.repo.repo_id, + run_names: [item.run_head.run_name], + abort: false, + }).unwrap(), + ), + ) + .then(() => actions.setSelectedItems([])) + .catch((error) => { + pushNotification({ + type: 'error', + content: t('common.server_error', { error: error?.error }), + }); + }); + }; + + const deleteClickHandle = () => { + if (!selectedItems?.length) return; + + Promise.all( + selectedItems.map((item) => + deleteRun({ + name: item.project, + repo_id: item.repo.repo_id, + run_names: [item.run_head.run_name], + }).unwrap(), + ), + ).catch((error) => { + pushNotification({ + type: 'error', + content: t('common.server_error', { error: error?.error }), + }); + }); + }; + + const isDisabledAbortButton = useMemo(() => { + return ( + !selectedItems?.length || + selectedItems.some((item) => !isAvailableAbortingForRun(item.run_head.status)) || + isStopping || + isDeleting + ); + }, [selectedItems, isStopping, isDeleting]); + + const isDisabledStopButton = useMemo(() => { + return ( + !selectedItems?.length || + selectedItems.some((item) => !isAvailableStoppingForRun(item.run_head.status)) || + isStopping || + isDeleting + ); + }, [selectedItems, isStopping, isDeleting]); + + const isDisabledDeleteButton = useMemo(() => { + return ( + !selectedItems?.length || + selectedItems.some((item) => !isAvailableDeletingForRun(item.run_head.status)) || + isStopping || + isDeleting + ); + }, [selectedItems, isStopping, isDeleting]); + + const isDisabledClearFilter = !selectedProject && !selectedRepo && !filterProps.filteringText && !onlyActive; + + return ( + + + + + + + + } + > + {t('projects.runs')} + + } + filter={ +
+ + +
+
+ + { + setSelectedProject(event.detail.selectedOption); + }} + expandToViewport={true} + placeholder={t('projects.run.project_placeholder')} + /> + +
+ +
+ + { + setSelectedRepo(event.detail.selectedOption); + }} + placeholder={t('projects.run.repo_placeholder')} + expandToViewport={true} + /> + +
+ +
+ setOnlyActive(detail.checked)} checked={onlyActive}> + {t('projects.run.active_only')} + +
+ +
+ +
+
+
+ } + pagination={} + /> + ); +}; diff --git a/hub/src/pages/Runs/MainList/styles.module.scss b/hub/src/pages/Runs/MainList/styles.module.scss new file mode 100644 index 000000000..804c579a9 --- /dev/null +++ b/hub/src/pages/Runs/MainList/styles.module.scss @@ -0,0 +1,21 @@ +.selectFilters { + --select-width: calc((688px - 3 * 20px) / 2); + + display: flex; + gap: 20px; + margin-top: 16px; + + .select { + width: var(--select-width, 30%); + } + + .activeOnly { + display: flex; + align-items: center; + padding-top: 26px; + } + + .clear { + padding-top: 26px; + } +} diff --git a/hub/src/pages/Runs/constants.ts b/hub/src/pages/Runs/constants.ts index 4263ae00a..c1f2f21b0 100644 --- a/hub/src/pages/Runs/constants.ts +++ b/hub/src/pages/Runs/constants.ts @@ -1,3 +1,4 @@ export const runStatusForDeleting: TRunStatus[] = ['failed', 'stopped', 'aborted', 'done']; export const runStatusForStopping: TRunStatus[] = ['submitted', 'pending', 'running']; export const runStatusForAborting: TRunStatus[] = ['submitted', 'pending', 'running', 'stopping']; +export const unfinishedRuns: TRunStatus[] = ['building', 'running', 'uploading', 'downloading', 'stopping', 'terminating']; diff --git a/hub/src/pages/Runs/index.ts b/hub/src/pages/Runs/index.ts index 4f9c20200..4198d5f0d 100644 --- a/hub/src/pages/Runs/index.ts +++ b/hub/src/pages/Runs/index.ts @@ -1,3 +1,4 @@ +export { List } from './MainList'; export { RunList } from './List'; export { RunDetails } from './Details'; export { Logs } from './Details/Logs'; diff --git a/hub/src/pages/Runs/utils.ts b/hub/src/pages/Runs/utils.ts index 2c1c38f63..80dd1ef97 100644 --- a/hub/src/pages/Runs/utils.ts +++ b/hub/src/pages/Runs/utils.ts @@ -1,13 +1,13 @@ import { runStatusForAborting, runStatusForDeleting, runStatusForStopping } from './constants'; -export const isAvailableDeletingForRun = (run: IRun): boolean => { - return runStatusForDeleting.includes(run.status); +export const isAvailableDeletingForRun = (status: IRunHead['status']): boolean => { + return runStatusForDeleting.includes(status); }; -export const isAvailableStoppingForRun = (run: IRun): boolean => { - return runStatusForStopping.includes(run.status); +export const isAvailableStoppingForRun = (status: IRunHead['status']): boolean => { + return runStatusForStopping.includes(status); }; -export const isAvailableAbortingForRun = (run: IRun): boolean => { - return runStatusForAborting.includes(run.status); +export const isAvailableAbortingForRun = (status: IRunHead['status']): boolean => { + return runStatusForAborting.includes(status); }; diff --git a/hub/src/router.tsx b/hub/src/router.tsx index 318a1c4cd..39e97d26c 100644 --- a/hub/src/router.tsx +++ b/hub/src/router.tsx @@ -6,7 +6,7 @@ import App from 'App'; import { Logout } from 'App/Logout'; import { ProjectAdd, ProjectDetails, ProjectEditBackend, ProjectList, ProjectSettings } from 'pages/Project'; import { RepositoryDetails, RepositoryList, RepositorySettings } from 'pages/Repositories'; -import { Artifacts as RunArtifacts, Logs as RunLogs, RunDetails, RunList } from 'pages/Runs'; +import { Artifacts as RunArtifacts, List as MainRunList, Logs as RunLogs, RunDetails, RunList } from 'pages/Runs'; import { TagDetails, TagList } from 'pages/Tags'; import { UserAdd, UserDetails, UserEdit, UserList } from 'pages/User'; @@ -22,7 +22,7 @@ export const router = createBrowserRouter([ // hubs { path: ROUTES.BASE, - element: , + element: , }, { path: ROUTES.PROJECT.LIST, @@ -97,7 +97,14 @@ export const router = createBrowserRouter([ path: ROUTES.PROJECT.ADD, element: , }, - // members + + // Runs + { + path: ROUTES.RUNS.LIST, + element: , + }, + + // Users { path: ROUTES.USER.LIST, element: , diff --git a/hub/src/routes.ts b/hub/src/routes.ts index 519dc2a09..1cac3328c 100644 --- a/hub/src/routes.ts +++ b/hub/src/routes.ts @@ -62,6 +62,10 @@ export const ROUTES = { }, }, + RUNS: { + LIST: '/runs', + }, + USER: { LIST: '/users', ADD: '/users/add', diff --git a/hub/src/services/run.ts b/hub/src/services/run.ts index e8cbbe6a0..5e5f29fe0 100644 --- a/hub/src/services/run.ts +++ b/hub/src/services/run.ts @@ -4,15 +4,42 @@ import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import fetchBaseQueryHeaders from 'libs/fetchBaseQueryHeaders'; +const reduceInvalidateTagsFromRunNames = (names: IRun['run_name'][]) => { + return names.reduce((accumulator, runName: string) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + accumulator.push({ type: 'Runs', id: runName }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + accumulator.push({ type: 'AllRuns', id: runName }); + + return accumulator; + }, []); +}; + export const runApi = createApi({ reducerPath: 'runApi', baseQuery: fetchBaseQuery({ prepareHeaders: fetchBaseQueryHeaders, }), - tagTypes: ['Runs'], + tagTypes: ['Runs', 'AllRuns'], endpoints: (builder) => ({ + getAllRuns: builder.query({ + query: () => { + return { + url: API.RUNS.LIST(), + method: 'POST', + }; + }, + + providesTags: (result) => + result + ? [...result.map(({ run_head }) => ({ type: 'AllRuns' as const, id: run_head.run_name })), 'AllRuns'] + : ['AllRuns'], + }), + getRuns: builder.query({ query: ({ name, ...body }) => { return { @@ -37,7 +64,13 @@ export const runApi = createApi({ transformResponse: (response: IRun[]) => response[0], - providesTags: (result) => (result ? [{ type: 'Runs' as const, id: result?.run_name }] : []), + providesTags: (result) => + result + ? [ + { type: 'Runs' as const, id: result?.run_name }, + { type: 'AllRuns' as const, id: result?.run_name }, + ] + : [], }), stopRuns: builder.mutation({ @@ -47,8 +80,7 @@ export const runApi = createApi({ body, }), - invalidatesTags: (result, error, params) => - params.run_names.map((run: string) => ({ type: 'Runs' as const, id: run })), + invalidatesTags: (result, error, params) => reduceInvalidateTagsFromRunNames(params.run_names), }), deleteRuns: builder.mutation({ @@ -58,10 +90,9 @@ export const runApi = createApi({ body, }), - invalidatesTags: (result, error, params) => - params.run_names.map((run: string) => ({ type: 'Runs' as const, id: run })), + invalidatesTags: (result, error, params) => reduceInvalidateTagsFromRunNames(params.run_names), }), }), }); -export const { useGetRunsQuery, useGetRunQuery, useStopRunsMutation, useDeleteRunsMutation } = runApi; +export const { useGetAllRunsQuery, useGetRunsQuery, useGetRunQuery, useStopRunsMutation, useDeleteRunsMutation } = runApi; diff --git a/hub/src/types/run.d.ts b/hub/src/types/run.d.ts index 20c1e40ea..b3bacfcb2 100644 --- a/hub/src/types/run.d.ts +++ b/hub/src/types/run.d.ts @@ -57,24 +57,51 @@ declare interface IRunJobHead { app_names: string[] } -declare interface IRun { +declare interface IRunAppHad { + job_id: string + artifact_path: string +} + +declare interface IRunArtifactHad { + job_id: string + artifact_path: string +} + +declare interface IRunRepo { + repo_id: string, + last_run_at: number, + tags_count: number, + repo_info: { + repo_type: string, + "repo_host_name": string, + "repo_port": number | null, + "repo_user_name": string, + "repo_name": string + } +} + +declare interface IRunHead { run_name: string, workflow_name: string | null, provider_name: string | null, - repo_user_id: string, + configuration_path: string, hub_user_name: string, - artifact_heads: null | { - job_id: string - artifact_path: string - }[], + artifact_heads: IRunArtifactHad[] | null, status: TRunStatus, submitted_at: number, tag_name: string | null, - "app_heads": null | - { - job_id: string - artifact_path: string - }[], + app_heads: IRunAppHad[] | null, request_heads: string | null, job_heads: IRunJobHead[] } + +declare interface IRunListItem { + project: string, + repo: IRunRepo; + run_head: IRunHead +} + +declare interface IRun extends Omit { + repo: IRunRepo, + repo_user_id: string, +} From 6ca4df6cd87c0a9e70c7e8a0c1c2ef622e1e75d1 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Tue, 1 Aug 2023 16:51:35 +0300 Subject: [PATCH 2/2] Close #588 Fixes after review --- hub/src/pages/Runs/MainList/index.tsx | 92 +++++++++++-------- .../pages/Runs/MainList/styles.module.scss | 2 - 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/hub/src/pages/Runs/MainList/index.tsx b/hub/src/pages/Runs/MainList/index.tsx index 00a8b24cc..d18a698fa 100644 --- a/hub/src/pages/Runs/MainList/index.tsx +++ b/hub/src/pages/Runs/MainList/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { get as _get, uniqBy as _uniqBy } from 'lodash'; +import { /*get as _get,*/ sortBy as _sortBy, uniqBy as _uniqBy } from 'lodash'; import { format } from 'date-fns'; import { @@ -15,7 +15,7 @@ import { SpaceBetween, StatusIndicator, Table, - TextFilter, + // TextFilter, Toggle, } from 'components'; @@ -30,13 +30,13 @@ import { isAvailableAbortingForRun, isAvailableDeletingForRun, isAvailableStoppi import styles from './styles.module.scss'; -export const SEARCHABLE_COLUMNS = [ - 'run_head.run_name', - 'run_head.job_heads.[0].configuration_path', - 'run_head.job_heads.[0].instance_type', - 'run_head.hub_user_name', - 'run_head.status', -]; +// export const SEARCHABLE_COLUMNS = [ +// 'run_head.run_name', +// 'run_head.job_heads.[0].configuration_path', +// 'run_head.job_heads.[0].instance_type', +// 'run_head.hub_user_name', +// 'run_head.status', +// ]; export const List: React.FC = () => { const { t } = useTranslation(); @@ -66,6 +66,16 @@ export const List: React.FC = () => { ), }, + { + id: 'project', + header: `${t('projects.run.project')}`, + cell: (item: IRunListItem) => item.project, + }, + { + id: 'repo', + header: `${t('projects.run.repo')}`, + cell: (item: IRunListItem) => item.repo.repo_info.repo_name, + }, { id: 'configuration', header: `${t('projects.run.configuration')}`, @@ -95,11 +105,11 @@ export const List: React.FC = () => { header: t('projects.run.submitted_at'), cell: (item: IRunListItem) => format(new Date(item.run_head.submitted_at), DATE_TIME_FORMAT), }, - { - id: 'artifacts', - header: t('projects.run.artifacts_count'), - cell: (item: IRunListItem) => item.run_head.artifact_heads?.length ?? '-', - }, + // { + // id: 'artifacts', + // header: t('projects.run.artifacts_count'), + // cell: (item: IRunListItem) => item.run_head.artifact_heads?.length ?? '-', + // }, ]; useEffect(() => { @@ -130,28 +140,39 @@ export const List: React.FC = () => { ); }; - const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps } = useCollection(data ?? [], { - filtering: { - empty: renderEmptyMessage(), - noMatch: renderNoMatchMessage(), + const sortedData = useMemo(() => { + if (!data) return []; + + return _sortBy(data, [(i) => -i.run_head.submitted_at]); + }, [data]); + + const { items, actions, /*filteredItemsCount,*/ collectionProps, filterProps, paginationProps } = useCollection( + sortedData ?? [], + { + filtering: { + empty: renderEmptyMessage(), + noMatch: renderNoMatchMessage(), + + filteringFunction: (runItem /*, filteringText*/) => { + // const filteringTextLowerCase = filteringText.toLowerCase(); - filteringFunction: (runItem, filteringText) => { - const filteringTextLowerCase = filteringText.toLowerCase(); + if (selectedProject?.value && runItem.project !== selectedProject.value) return false; - if (selectedProject?.value && runItem.project !== selectedProject.value) return false; + if (selectedRepo?.value && runItem.repo.repo_id !== selectedRepo.value) return false; - if (selectedRepo?.value && runItem.repo.repo_id !== selectedRepo.value) return false; + if (onlyActive && !unfinishedRuns.includes(runItem.run_head.status)) return false; - if (onlyActive && !unfinishedRuns.includes(runItem.run_head.status)) return false; + // return SEARCHABLE_COLUMNS.map((key) => _get(runItem, key)).some( + // (value) => typeof value === 'string' && value.toLowerCase().indexOf(filteringTextLowerCase) > -1, + // ); - return SEARCHABLE_COLUMNS.map((key) => _get(runItem, key)).some( - (value) => typeof value === 'string' && value.toLowerCase().indexOf(filteringTextLowerCase) > -1, - ); + return true; + }, }, + pagination: { pageSize: 20 }, + selection: {}, }, - pagination: { pageSize: 20 }, - selection: {}, - }); + ); const { selectedItems } = collectionProps; @@ -299,13 +320,12 @@ export const List: React.FC = () => { } filter={
- - + {/**/}
diff --git a/hub/src/pages/Runs/MainList/styles.module.scss b/hub/src/pages/Runs/MainList/styles.module.scss index 804c579a9..37df7783d 100644 --- a/hub/src/pages/Runs/MainList/styles.module.scss +++ b/hub/src/pages/Runs/MainList/styles.module.scss @@ -1,9 +1,7 @@ .selectFilters { --select-width: calc((688px - 3 * 20px) / 2); - display: flex; gap: 20px; - margin-top: 16px; .select { width: var(--select-width, 30%);