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/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..7c88973dde 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,131 @@ export const useFilters = () => { onChangePropertyFilterHandle(detail); }; + const filteringProperties = useMemo(() => { + const permanentFiltersKeysMap = new Map(); + + for (const prefix of onlyOneFilterGroupPrefixes) { + console.log({ prefix }); + 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 +365,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..cc8c0a605f 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,18 @@ 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; +}; + +export const EventList: React.FC = ({ withSearchParams, permanentFilters, renderHeader, ...props }) => { const { t } = useTranslation(); useBreadcrumbs([ @@ -24,12 +36,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,33 +66,18 @@ export const EventList = () => { const { columns } = useColumnsDefinitions(); + const loading = isLoadingFilters || isLoading; + return ( -