diff --git a/frontend/src/hooks/useInfiniteScroll.ts b/frontend/src/hooks/useInfiniteScroll.ts index 97541efb25..a6986a8226 100644 --- a/frontend/src/hooks/useInfiniteScroll.ts +++ b/frontend/src/hooks/useInfiniteScroll.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { isEqual } from 'lodash'; import { UseLazyQuery /*, UseQueryStateOptions*/ } from '@reduxjs/toolkit/dist/query/react/buildHooks'; import { QueryDefinition } from '@reduxjs/toolkit/query'; @@ -28,6 +29,9 @@ export const useInfiniteScroll = ({ const lastRequestParams = useRef(undefined); const [disabledMore, setDisabledMore] = useState(false); const { limit, ...argsProp } = args; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const oldArgsProps = useRef>(argsProp); const [getItems, { isLoading, isFetching }] = useLazyQuery({ ...args } as Args); @@ -53,8 +57,13 @@ export const useInfiniteScroll = ({ }; useEffect(() => { - getEmptyList(); - }, Object.values(argsProp)); + if (!isEqual(argsProp, oldArgsProps.current)) { + getEmptyList(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + oldArgsProps.current = argsProp; + } + }, [argsProp, oldArgsProps]); const getMore = async () => { if (isLoadingRef.current || disabledMore) { diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 3b1c16c892..8cda89caef 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -322,6 +322,7 @@ "quickstart_message_text": "Check out the quickstart guide to get started with dstack", "nomatch_message_title": "No matches", "nomatch_message_text": "We can't find a match. Try to change project or clear filter", + "filter_property_placeholder": "Filter runs by properties", "project": "Project", "project_placeholder": "Filtering by project", "repo": "Repository", diff --git a/frontend/src/pages/Runs/Details/Logs/index.tsx b/frontend/src/pages/Runs/Details/Logs/index.tsx index 8af316a758..721b0527de 100644 --- a/frontend/src/pages/Runs/Details/Logs/index.tsx +++ b/frontend/src/pages/Runs/Details/Logs/index.tsx @@ -37,7 +37,7 @@ export const Logs: React.FC = ({ className, projectName, runName, jobSub } else { terminalInstance.current.options.theme = { foreground: '#b6bec9', - background: '#0f1b2a', + background: '#161d26', }; } }, [appliedTheme]); diff --git a/frontend/src/pages/Runs/List/hooks/useFilters.ts b/frontend/src/pages/Runs/List/hooks/useFilters.ts index f48759337e..70b87d0f30 100644 --- a/frontend/src/pages/Runs/List/hooks/useFilters.ts +++ b/frontend/src/pages/Runs/List/hooks/useFilters.ts @@ -1,10 +1,13 @@ import { useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { ToggleProps } from '@cloudscape-design/components'; import type { PropertyFilterProps } from 'components'; import { useProjectFilter } from 'hooks/useProjectFilter'; +import { useGetUserListQuery } from '../../../../services/user'; + type Args = { localStorePrefix: string; }; @@ -14,7 +17,6 @@ type RequestParamsKeys = keyof Pick = { PROJECT_NAME: 'project_name', USER_NAME: 'username', - ACTIVE: 'only_active', }; const EMPTY_QUERY: PropertyFilterProps.Query = { @@ -22,19 +24,27 @@ const EMPTY_QUERY: PropertyFilterProps.Query = { operation: 'and', }; -const tokensToRequestParams = (tokens: PropertyFilterProps.Query['tokens']) => { - return tokens.reduce((acc, token) => { +const tokensToRequestParams = (tokens: PropertyFilterProps.Query['tokens'], onlyActive?: boolean) => { + const params = tokens.reduce((acc, token) => { if (token.propertyKey) { acc[token.propertyKey as RequestParamsKeys] = token.value; } return acc; }, {} as Record); + + if (onlyActive) { + params['only_active'] = 'true'; + } + + return params; }; export const useFilters = ({ localStorePrefix }: Args) => { const [searchParams, setSearchParams] = useSearchParams(); + const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true'); const { projectOptions } = useProjectFilter({ localStorePrefix }); + const { data: usersData } = useGetUserListQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => { const tokens = []; @@ -59,6 +69,7 @@ export const useFilters = ({ localStorePrefix }: Args) => { const clearFilter = () => { setSearchParams({}); + setOnlyActive(false); setPropertyFilterQuery(EMPTY_QUERY); }; @@ -73,13 +84,15 @@ export const useFilters = ({ localStorePrefix }: Args) => { }); }); - options.push({ - propertyKey: FilterKeys.ACTIVE, - value: 'True', + usersData?.forEach(({ username }) => { + options.push({ + propertyKey: FilterKeys.USER_NAME, + value: username, + }); }); return options; - }, [projectOptions]); + }, [projectOptions, usersData]); const filteringProperties = [ { @@ -93,12 +106,6 @@ export const useFilters = ({ localStorePrefix }: Args) => { operators: ['='], propertyLabel: 'User', }, - { - key: FilterKeys.ACTIVE, - operators: ['='], - propertyLabel: 'Only active', - groupValuesLabel: 'Active values', - }, ]; const onChangePropertyFilter: PropertyFilterProps['onChange'] = ({ detail }) => { @@ -108,7 +115,7 @@ export const useFilters = ({ localStorePrefix }: Args) => { return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex); }); - setSearchParams(tokensToRequestParams(filteredTokens)); + setSearchParams(tokensToRequestParams(filteredTokens, onlyActive)); setPropertyFilterQuery({ operation, @@ -116,12 +123,18 @@ export const useFilters = ({ localStorePrefix }: Args) => { }); }; + const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { + setOnlyActive(detail.checked); + + setSearchParams(tokensToRequestParams(propertyFilterQuery.tokens, detail.checked)); + }; + const filteringRequestParams = useMemo(() => { const params = tokensToRequestParams(propertyFilterQuery.tokens); return { ...params, - only_active: params.only_active === 'True', + only_active: onlyActive, }; }, [propertyFilterQuery]); @@ -132,5 +145,7 @@ export const useFilters = ({ localStorePrefix }: Args) => { onChangePropertyFilter, filteringOptions, filteringProperties, + onlyActive, + onChangeOnlyActive, } as const; }; diff --git a/frontend/src/pages/Runs/List/index.tsx b/frontend/src/pages/Runs/List/index.tsx index d857a3d30f..6ac11f990d 100644 --- a/frontend/src/pages/Runs/List/index.tsx +++ b/frontend/src/pages/Runs/List/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Header, ListEmptyMessage, Loader, PropertyFilter, SpaceBetween, Table } from 'components'; +import { Button, Header, Loader, PropertyFilter, SpaceBetween, Table, Toggle } from 'components'; import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; import { useBreadcrumbs, useCollection, useInfiniteScroll } from 'hooks'; @@ -40,6 +40,8 @@ export const RunList: React.FC = () => { filteringOptions, filteringProperties, filteringRequestParams, + onlyActive, + onChangeOnlyActive, } = useFilters({ localStorePrefix: 'administration-run-list-page', }); @@ -154,15 +156,21 @@ export const RunList: React.FC = () => { expandToViewport hideOperations i18nStrings={{ - clearFiltersText: 'Clear filter', - filteringAriaLabel: 'Find runs', - filteringPlaceholder: 'Find runs', + clearFiltersText: t('common.clearFilter'), + filteringAriaLabel: t('projects.run.filter_property_placeholder'), + filteringPlaceholder: t('projects.run.filter_property_placeholder'), operationAndText: 'and', }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} /> + +
+ + {t('projects.run.active_only')} + +
} footer={} diff --git a/frontend/src/pages/Runs/List/styles.module.scss b/frontend/src/pages/Runs/List/styles.module.scss index 6cf06c41da..0b5efa7b66 100644 --- a/frontend/src/pages/Runs/List/styles.module.scss +++ b/frontend/src/pages/Runs/List/styles.module.scss @@ -1,29 +1,19 @@ .selectFilters { - --select-width: calc((688px - 3 * 20px) / 2); display: flex; flex-wrap: wrap; gap: 0 20px; - .select { - width: var(--select-width, 30%); - } - .propertyFilter { - //width: 400px; - //max-width: 100%; + max-width: 640px; flex-grow: 1; min-width: 0; } .activeOnly { display: flex; - align-items: center; - padding-top: 26px; + padding-top: 7px; } - .clear { - padding-top: 26px; - } } .emptyMessage {