From d71de5493460ce9c097b1494dbea77863d3a3bf7 Mon Sep 17 00:00:00 2001 From: Branco Bruyneel <43569324+brancobruyneel@users.noreply.github.com> Date: Sun, 17 Sep 2023 17:52:52 +0200 Subject: [PATCH] Feat: events page filtering (#611) * wip: inital version of event filtering * chore: generate types * fix: code style * chore: comment & remove socket.off * chore: fix typings * wip * fix: filter tags * chore: change comment * fix: filtering * feat: edit filter * feat: basic search component * chore: use types file * fix: various stuff * chore: rename placeholder * chore: clean up * feat: add refresh button * fix: a brittle test * fix: api client * fix: remove a console.log * fix: code style --------- Co-authored-by: takaro-ci-bot[bot] <138661031+takaro-ci-bot[bot]@users.noreply.github.com> Co-authored-by: Niek Candaele --- packages/lib-apiclient/src/generated/api.ts | 176 ++++++++++- .../components/data/InfiniteScroll/index.tsx | 7 +- .../components/inputs/DatePicker/Context.tsx | 1 + .../inputs/DatePicker/QuickSelect/index.tsx | 7 + .../components/inputs/DatePicker/index.tsx | 27 +- .../src/components/inputs/DatePicker/style.ts | 16 +- .../components/inputs/TextField/Generic.tsx | 2 +- .../src/components/inputs/index.ts | 5 + .../queryBuilder.integration.test.ts | 22 ++ packages/lib-db/src/queryBuilder.ts | 23 +- .../modules/teleports/commands/teleport.js | 6 +- packages/web-main/src/Router.tsx | 2 +- .../{ => events}/EventFeed/EventDetail.tsx | 0 .../{ => events}/EventFeed/EventItem.tsx | 50 ++-- .../{ => events}/EventFeed/index.tsx | 1 + .../src/components/events/EventFilter/Tag.tsx | 73 +++++ .../components/events/EventFilter/TagList.tsx | 13 + .../components/events/EventFilter/index.tsx | 112 +++++++ .../components/events/EventFilter/style.ts | 31 ++ .../components/events/EventSearch/Item.tsx | 33 ++ .../components/events/EventSearch/index.tsx | 234 +++++++++++++++ .../components/events/EventSearch/style.tsx | 65 ++++ .../components/events/TreeFilter/Branch.tsx | 73 +++++ .../src/components/events/TreeFilter/Node.tsx | 47 +++ .../components/events/TreeFilter/index.tsx | 33 ++ .../components/events/TreeFilter/style.tsx | 32 ++ .../web-main/src/components/events/types.ts | 19 ++ packages/web-main/src/pages/Events.tsx | 46 --- packages/web-main/src/pages/events/index.tsx | 282 ++++++++++++++++++ .../web-main/src/queries/events/queries.tsx | 43 ++- 30 files changed, 1365 insertions(+), 116 deletions(-) rename packages/web-main/src/components/{ => events}/EventFeed/EventDetail.tsx (100%) rename packages/web-main/src/components/{ => events}/EventFeed/EventItem.tsx (66%) rename packages/web-main/src/components/{ => events}/EventFeed/index.tsx (95%) create mode 100644 packages/web-main/src/components/events/EventFilter/Tag.tsx create mode 100644 packages/web-main/src/components/events/EventFilter/TagList.tsx create mode 100644 packages/web-main/src/components/events/EventFilter/index.tsx create mode 100644 packages/web-main/src/components/events/EventFilter/style.ts create mode 100644 packages/web-main/src/components/events/EventSearch/Item.tsx create mode 100644 packages/web-main/src/components/events/EventSearch/index.tsx create mode 100644 packages/web-main/src/components/events/EventSearch/style.tsx create mode 100644 packages/web-main/src/components/events/TreeFilter/Branch.tsx create mode 100644 packages/web-main/src/components/events/TreeFilter/Node.tsx create mode 100644 packages/web-main/src/components/events/TreeFilter/index.tsx create mode 100644 packages/web-main/src/components/events/TreeFilter/style.tsx create mode 100644 packages/web-main/src/components/events/types.ts delete mode 100644 packages/web-main/src/pages/Events.tsx create mode 100644 packages/web-main/src/pages/events/index.tsx diff --git a/packages/lib-apiclient/src/generated/api.ts b/packages/lib-apiclient/src/generated/api.ts index 56cd4b0f70..8899262b5f 100644 --- a/packages/lib-apiclient/src/generated/api.ts +++ b/packages/lib-apiclient/src/generated/api.ts @@ -568,6 +568,18 @@ export interface CommandSearchInputDTO { * @memberof CommandSearchInputDTO */ sortDirection?: CommandSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof CommandSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof CommandSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -825,6 +837,18 @@ export interface CronJobSearchInputDTO { * @memberof CronJobSearchInputDTO */ sortDirection?: CronJobSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof CronJobSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof CronJobSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -1116,6 +1140,18 @@ export interface DomainSearchInputDTO { * @memberof DomainSearchInputDTO */ sortDirection?: DomainSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof DomainSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof DomainSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -1547,6 +1583,18 @@ export interface EventSearchInputDTO { * @memberof EventSearchInputDTO */ sortDirection?: EventSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof EventSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof EventSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -1700,6 +1748,18 @@ export interface FunctionSearchInputDTO { * @memberof FunctionSearchInputDTO */ sortDirection?: FunctionSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof FunctionSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof FunctionSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -1932,6 +1992,18 @@ export interface GameServerSearchInputDTO { * @memberof GameServerSearchInputDTO */ sortDirection?: GameServerSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof GameServerSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof GameServerSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -2331,6 +2403,18 @@ export interface GuildSearchInputDTO { * @memberof GuildSearchInputDTO */ sortDirection?: GuildSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof GuildSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof GuildSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -2659,6 +2743,18 @@ export interface HookSearchInputDTO { * @memberof HookSearchInputDTO */ sortDirection?: HookSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof HookSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof HookSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -2908,6 +3004,18 @@ export interface ITakaroQuery { * @memberof ITakaroQuery */ sortDirection?: ITakaroQuerySortDirectionEnum; + /** + * + * @type {string} + * @memberof ITakaroQuery + */ + startDate?: string; + /** + * + * @type {string} + * @memberof ITakaroQuery + */ + endDate?: string; /** * * @type {Array} @@ -3479,6 +3587,18 @@ export interface ModuleSearchInputDTO { * @memberof ModuleSearchInputDTO */ sortDirection?: ModuleSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof ModuleSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof ModuleSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -3665,13 +3785,13 @@ export interface PermissionCreateDTO { * @type {string} * @memberof PermissionCreateDTO */ - friendlyName?: string; + friendlyName: string; /** * * @type {string} * @memberof PermissionCreateDTO */ - description?: string; + description: string; } /** * @@ -3690,13 +3810,13 @@ export interface PermissionOutputDTO { * @type {string} * @memberof PermissionOutputDTO */ - friendlyName?: string; + friendlyName: string; /** * * @type {string} * @memberof PermissionOutputDTO */ - description?: string; + description: string; /** * * @type {string} @@ -4245,6 +4365,18 @@ export interface PlayerSearchInputDTO { * @memberof PlayerSearchInputDTO */ sortDirection?: PlayerSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof PlayerSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof PlayerSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -4485,6 +4617,18 @@ export interface RoleSearchInputDTO { * @memberof RoleSearchInputDTO */ sortDirection?: RoleSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof RoleSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof RoleSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -5111,6 +5255,18 @@ export interface UserSearchInputDTO { * @memberof UserSearchInputDTO */ sortDirection?: UserSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof UserSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof UserSearchInputDTO + */ + endDate?: string; /** * * @type {Array} @@ -5380,6 +5536,18 @@ export interface VariableSearchInputDTO { * @memberof VariableSearchInputDTO */ sortDirection?: VariableSearchInputDTOSortDirectionEnum; + /** + * + * @type {string} + * @memberof VariableSearchInputDTO + */ + startDate?: string; + /** + * + * @type {string} + * @memberof VariableSearchInputDTO + */ + endDate?: string; /** * * @type {Array} diff --git a/packages/lib-components/src/components/data/InfiniteScroll/index.tsx b/packages/lib-components/src/components/data/InfiniteScroll/index.tsx index dfdda2610b..b75e677d8b 100644 --- a/packages/lib-components/src/components/data/InfiniteScroll/index.tsx +++ b/packages/lib-components/src/components/data/InfiniteScroll/index.tsx @@ -5,8 +5,7 @@ import { Button } from '../../../components'; export interface InfiniteScrollProps { isFetchingNextPage: boolean; - // TODO: correct type - fetchNextPage: any; + fetchNextPage: () => void; isFetching: boolean; hasNextPage?: boolean; } @@ -29,7 +28,7 @@ export const InfiniteScroll = forwardRef }; return ( -
+ <> {hasNextPage && (
+ ); } ); diff --git a/packages/lib-components/src/components/inputs/DatePicker/Context.tsx b/packages/lib-components/src/components/inputs/DatePicker/Context.tsx index 0f1e11031a..7640dc9736 100644 --- a/packages/lib-components/src/components/inputs/DatePicker/Context.tsx +++ b/packages/lib-components/src/components/inputs/DatePicker/Context.tsx @@ -70,6 +70,7 @@ export function reducer(state: DatePickerState, action: Action): DatePickerState friendlyRange: action.payload.friendlyRange, friendlyEndDate: undefined, friendlyStartDate: undefined, + showQuickSelect: false, }; } } diff --git a/packages/lib-components/src/components/inputs/DatePicker/QuickSelect/index.tsx b/packages/lib-components/src/components/inputs/DatePicker/QuickSelect/index.tsx index 20c3627376..c4e4ab2756 100644 --- a/packages/lib-components/src/components/inputs/DatePicker/QuickSelect/index.tsx +++ b/packages/lib-components/src/components/inputs/DatePicker/QuickSelect/index.tsx @@ -117,6 +117,13 @@ export const QuickSelect: FC = ({ id }) => { friendlyRange: friendlyRangeName, }, }); + + dispatch({ + type: 'toggle_quick_select_popover', + payload: { + toggleQuickSelect: false, + }, + }); }; return ( diff --git a/packages/lib-components/src/components/inputs/DatePicker/index.tsx b/packages/lib-components/src/components/inputs/DatePicker/index.tsx index 0d0fe064a7..6128cda8ab 100644 --- a/packages/lib-components/src/components/inputs/DatePicker/index.tsx +++ b/packages/lib-components/src/components/inputs/DatePicker/index.tsx @@ -12,7 +12,7 @@ import { DateSelector } from './DateSelector'; import { DatePickerContext, DatePickerDispatchContext, reducer } from './Context'; export interface DatePickerProps { - value: string; + value?: string; readOnly?: boolean; id: string; onChange: (start: DateTime, end: DateTime) => void; @@ -51,19 +51,18 @@ export const DatePicker: FC = ({ readOnly = false, id, onChange } > - - - dispatch({ - type: 'toggle_quick_select_popover', - payload: { toggleQuickSelect: !state.showQuickSelect }, - }) - } - > - - - - + + dispatch({ + type: 'toggle_quick_select_popover', + payload: { toggleQuickSelect: !state.showQuickSelect }, + }) + } + > + + + diff --git a/packages/lib-components/src/components/inputs/DatePicker/style.ts b/packages/lib-components/src/components/inputs/DatePicker/style.ts index d69389be13..155fc01463 100644 --- a/packages/lib-components/src/components/inputs/DatePicker/style.ts +++ b/packages/lib-components/src/components/inputs/DatePicker/style.ts @@ -4,6 +4,7 @@ export const Container = styled.div<{ isOpen: boolean; hasError: boolean; }>` + width: 100%; position: relative; display: flex; flex-direction: row; @@ -16,7 +17,7 @@ export const Container = styled.div<{ outline: 0; font-weight: 500; text-transform: capitalize; - padding: ${({ theme }) => `0 ${theme.spacing['0_5']}`}; + background-color: ${({ theme }) => theme.colors.backgroundAlt}; border: 0.1rem solid ${({ theme, isOpen, hasError }) => { if (hasError) return theme.colors.error; @@ -26,16 +27,23 @@ export const Container = styled.div<{ &:focus { border-color: ${({ theme, hasError }) => (hasError ? theme.colors.error : theme.colors.primary)}; } + flex-shrink: 0; `; export const ItemContainer = styled.div<{ readOnly: boolean }>` - padding: ${({ theme }) => `${theme.spacing['0_75']} ${theme.spacing['0_5']}`}; cursor: ${({ readOnly }) => (readOnly ? 'default' : 'pointer')}; + padding: ${({ theme }) => `${theme.spacing['0_75']}`}; `; -export const QuickSelectContainer = styled.div` +export const QuickSelectContainer = styled.div<{ readOnly: boolean }>` + border-radius: ${({ theme }) => `${theme.borderRadius.medium} 0 0 ${theme.borderRadius.medium};`}; + height: 100%; + cursor: ${({ readOnly }) => (readOnly ? 'default' : 'pointer')}; + gap: ${({ theme }) => theme.spacing['0_25']}; display: flex; align-items: center; justify-content: center; - padding-left: ${({ theme }) => theme.spacing['0_5']}; + padding: ${({ theme }) => `0 ${theme.spacing['0_75']}`}; + background-color: ${({ theme }) => theme.colors.primary}; + flex-shrink: 0; `; diff --git a/packages/lib-components/src/components/inputs/TextField/Generic.tsx b/packages/lib-components/src/components/inputs/TextField/Generic.tsx index 8da35c3670..9752c61f9d 100644 --- a/packages/lib-components/src/components/inputs/TextField/Generic.tsx +++ b/packages/lib-components/src/components/inputs/TextField/Generic.tsx @@ -14,7 +14,7 @@ export interface TextFieldProps { placeholder?: string; size?: Size; prefix?: string; - suffix?: string; + suffix?: string | ReactElement; icon?: ReactElement; } diff --git a/packages/lib-components/src/components/inputs/index.ts b/packages/lib-components/src/components/inputs/index.ts index ab811f909c..a456b87ca5 100644 --- a/packages/lib-components/src/components/inputs/index.ts +++ b/packages/lib-components/src/components/inputs/index.ts @@ -1,6 +1,8 @@ export { ControlledCheckBox as CheckBox } from './CheckBox'; export type { ControlledCheckBoxProps as CheckBoxProps } from './CheckBox'; +export { GenericCheckBox as UnControlledCheckBox } from './CheckBox/Generic'; + export { ControlledSelect as Select } from './Select'; export type { ControlledSelectProps as SelectProps } from './Select'; @@ -33,4 +35,7 @@ export type { EditableFieldProps } from './EditableField'; export { Label } from './layout/Label'; export type { LabelProps } from './layout/Label'; +export { DatePicker } from './DatePicker'; +export type { DatePickerProps } from './DatePicker'; + export { JsonSchemaForm, SchemaGenerator, generateJsonSchema } from './JsonSchemaForm'; diff --git a/packages/lib-db/src/__tests__/queryBuilder.integration.test.ts b/packages/lib-db/src/__tests__/queryBuilder.integration.test.ts index edb9a3abc6..e741d693f0 100644 --- a/packages/lib-db/src/__tests__/queryBuilder.integration.test.ts +++ b/packages/lib-db/src/__tests__/queryBuilder.integration.test.ts @@ -3,6 +3,7 @@ import { QueryBuilder, SortDirection } from '../queryBuilder.js'; import { expect } from '@takaro/test'; import { TakaroModel } from '../TakaroModel.js'; import { Model } from 'objection'; +import { sleep } from '@takaro/util'; const TEST_TABLE_USERS_NAME = 'test_users'; const TEST_TABLE_POSTS_NAME = 'test_posts'; @@ -177,4 +178,25 @@ describe('QueryBuilder', () => { expect(res.results).to.have.lengthOf(2); }); + + it('Can search between a date range', async () => { + const start = new Date(); + await TestUserModel.query().insert({ name: 'test1' }); + await TestUserModel.query().insert({ name: 'test2' }); + + const end = new Date(); + + // Quick hack to make the test pass + // Relying on times like this with an external DB is troublesome... + await sleep(250); + + await TestUserModel.query().insert({ name: 'test3', createdAt: end.toISOString() }); + + const res = await new QueryBuilder({ + startDate: start.toISOString(), + endDate: end.toISOString(), + }).build(TestUserModel.query()); + + expect(res.results).to.have.lengthOf(2); + }); }); diff --git a/packages/lib-db/src/queryBuilder.ts b/packages/lib-db/src/queryBuilder.ts index a035dacb17..90b73057e9 100644 --- a/packages/lib-db/src/queryBuilder.ts +++ b/packages/lib-db/src/queryBuilder.ts @@ -1,15 +1,15 @@ -import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsDateString, IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; import { QueryBuilder as ObjectionQueryBuilder, Model as ObjectionModel, Page, AnyQueryBuilder } from 'objection'; export class ITakaroQuery { @IsOptional() filters?: { - [key in keyof T]?: unknown[]; + [key in keyof T]?: unknown[] | unknown; }; @IsOptional() search?: { - [key in keyof T]?: unknown[]; + [key in keyof T]?: unknown[] | unknown; }; @IsOptional() @@ -29,6 +29,14 @@ export class ITakaroQuery { @IsEnum(['asc', 'desc']) sortDirection?: SortDirection; + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + endDate?: string; + @IsOptional() @IsString({ each: true }) extend?: string[]; @@ -50,6 +58,13 @@ export class QueryBuilder { let qry = query.page(pagination.page, pagination.limit).orderBy(sorting.sortBy, sorting.sortDirection); + if (this.query.startDate) { + qry = qry.where(`${tableName}.createdAt`, '>=', this.query.startDate); + } + if (this.query.endDate) { + qry = qry.where(`${tableName}.createdAt`, '<=', this.query.endDate); + } + qry = this.filters(tableName, qry); if (this.query.search) { @@ -84,7 +99,7 @@ export class QueryBuilder { if (Object.prototype.hasOwnProperty.call(this.query.filters, filter)) { const searchVal = this.query.filters[filter]; - if (searchVal) { + if (searchVal && Array.isArray(searchVal)) { const filtered = searchVal.filter(Boolean); if (filtered.length) { query.whereIn(`${tableName}.${filter}`, searchVal.filter(Boolean) as unknown as AnyQueryBuilder); diff --git a/packages/lib-modules/src/modules/teleports/commands/teleport.js b/packages/lib-modules/src/modules/teleports/commands/teleport.js index f8edcefcd2..91930870d2 100644 --- a/packages/lib-modules/src/modules/teleports/commands/teleport.js +++ b/packages/lib-modules/src/modules/teleports/commands/teleport.js @@ -7,9 +7,9 @@ async function main() { const { player, gameServerId, arguments: args, module: mod } = data; if (!checkPermission(player, 'TELEPORTS_USE')) { - await data.player.pm('You do not have permission to use teleports.'); - return; - } + await data.player.pm('You do not have permission to use teleports.'); + return; + } const ownedTeleportRes = await takaro.variable.variableControllerSearch({ filters: { diff --git a/packages/web-main/src/Router.tsx b/packages/web-main/src/Router.tsx index 14e6bd75ed..38ec26f022 100644 --- a/packages/web-main/src/Router.tsx +++ b/packages/web-main/src/Router.tsx @@ -26,7 +26,7 @@ import { AuthSettings } from 'pages/auth/profile'; import { AuthVerification } from 'pages/auth/verification'; import Users from 'pages/Users'; import Variables from 'pages/Variables'; -import { Events } from 'pages/Events'; +import { Events } from 'pages/events'; import { Roles } from './pages/roles'; import { RolesCreate } from './pages/roles/RolesCreate'; diff --git a/packages/web-main/src/components/EventFeed/EventDetail.tsx b/packages/web-main/src/components/events/EventFeed/EventDetail.tsx similarity index 100% rename from packages/web-main/src/components/EventFeed/EventDetail.tsx rename to packages/web-main/src/components/events/EventFeed/EventDetail.tsx diff --git a/packages/web-main/src/components/EventFeed/EventItem.tsx b/packages/web-main/src/components/events/EventFeed/EventItem.tsx similarity index 66% rename from packages/web-main/src/components/EventFeed/EventItem.tsx rename to packages/web-main/src/components/events/EventFeed/EventItem.tsx index 90029f3f96..b756aaff5a 100644 --- a/packages/web-main/src/components/EventFeed/EventItem.tsx +++ b/packages/web-main/src/components/events/EventFeed/EventItem.tsx @@ -2,6 +2,7 @@ import { FC } from 'react'; import { styled } from '@takaro/lib-components'; import { EventDetail } from './EventDetail'; import { DateTime } from 'luxon'; +import { EnrichedEvent } from 'queries/events/queries'; const Header = styled.div` display: flex; @@ -62,46 +63,34 @@ const EventProperty: FC<{ name: string; value: unknown }> = ({ name, value }) => }; export type EventItemProps = { - eventType: string; - createdAt: string; - playerName?: string; - gamserverName?: string; - moduleName?: string; - commandName?: string; - data: Record; + event: EnrichedEvent; onDetailClick: () => void; }; -export const EventItem: FC = ({ - eventType, - createdAt, - data, - playerName, - gamserverName, - moduleName, - commandName, -}) => { - const timestamp = Date.parse(createdAt); +export const EventItem: FC = ({ event }) => { + const timestamp = Date.parse(event.createdAt); const timeAgo = DateTime.fromMillis(timestamp).toRelative(); + const meta = event.meta as any; + let properties = <>; - switch (eventType) { + switch (event.eventName) { case 'chat-message': properties = ( <> - - - + + + ); break; case 'command-executed': properties = ( <> - - - + + + ); break; @@ -109,8 +98,8 @@ export const EventItem: FC = ({ case 'cronjob-executed': properties = ( <> - - + + ); break; @@ -118,8 +107,9 @@ export const EventItem: FC = ({ case 'player-disconnected': properties = ( <> - - + + + ); } @@ -129,10 +119,10 @@ export const EventItem: FC = ({
-

{eventType}

+

{event.eventName}

{timeAgo}

- +
{properties} diff --git a/packages/web-main/src/components/EventFeed/index.tsx b/packages/web-main/src/components/events/EventFeed/index.tsx similarity index 95% rename from packages/web-main/src/components/EventFeed/index.tsx rename to packages/web-main/src/components/events/EventFeed/index.tsx index 59af600370..c0c9cb3b76 100644 --- a/packages/web-main/src/components/EventFeed/index.tsx +++ b/packages/web-main/src/components/events/EventFeed/index.tsx @@ -6,6 +6,7 @@ const List = styled.ol` padding: 0; border-left: 1px solid ${({ theme }) => theme.colors.textAlt}; width: 100%; + height: fit-content; `; export const EventFeed: FC = ({ children }) => { diff --git a/packages/web-main/src/components/events/EventFilter/Tag.tsx b/packages/web-main/src/components/events/EventFilter/Tag.tsx new file mode 100644 index 0000000000..480279ec1b --- /dev/null +++ b/packages/web-main/src/components/events/EventFilter/Tag.tsx @@ -0,0 +1,73 @@ +import { Popover, styled, Tooltip } from '@takaro/lib-components'; +import { FC, useState } from 'react'; +import { HiXMark as CloseIcon } from 'react-icons/hi2'; +import { FilterPopup } from '.'; +import { Filter as FilterType, operators } from '../types'; + +const Wrapper = styled.div` + display: flex; + width: fit-content; + align-items: center; + gap: ${({ theme }) => theme.spacing['0_5']}; + justify-content: space-between; + border: 1px solid ${({ theme }) => theme.colors.secondary}; + padding: ${({ theme }) => `${theme.spacing['0_5']} ${theme.spacing['0_75']}`}; + + border-radius: ${({ theme }) => theme.borderRadius.medium}; +`; + +const Label = styled.div` + display: flex; + width: fit-content; + align-items: center; + width: 100%; + border-bottom: 1px solid ${({ theme }) => theme.colors.background}; + + &:hover { + cursor: pointer; + border-bottom: 1px solid ${({ theme }) => theme.colors.textAlt}; + } +`; + +type FilterTagProps = { + fields: string[]; + filter: FilterType; + editFilter: (filter: FilterType) => void; + onClear: () => void; +}; + +export const EventFilterTag: FC = ({ filter, fields, editFilter, onClear }) => { + const [open, setOpen] = useState(false); + + const operator = operators[filter.operator as keyof typeof operators]; + + const handleClick = () => { + setOpen(true); + }; + + return ( + + + + + + + + Edit filter + + + + + + Delete filter + + + + + + + + ); +}; diff --git a/packages/web-main/src/components/events/EventFilter/TagList.tsx b/packages/web-main/src/components/events/EventFilter/TagList.tsx new file mode 100644 index 0000000000..8bbd4b05e2 --- /dev/null +++ b/packages/web-main/src/components/events/EventFilter/TagList.tsx @@ -0,0 +1,13 @@ +import { styled } from '@takaro/lib-components'; +import { FC, PropsWithChildren } from 'react'; + +const List = styled.div` + display: flex; + justify-content: flex-start; + gap: ${({ theme }) => theme.spacing['1']}; + width: 100%; +`; + +export const EventFilterTagList: FC = ({ children }) => { + return {children}; +}; diff --git a/packages/web-main/src/components/events/EventFilter/index.tsx b/packages/web-main/src/components/events/EventFilter/index.tsx new file mode 100644 index 0000000000..e7cd65faa5 --- /dev/null +++ b/packages/web-main/src/components/events/EventFilter/index.tsx @@ -0,0 +1,112 @@ +import { FC, useState } from 'react'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { Button, Divider, Popover, Select, TextField } from '@takaro/lib-components'; +import { HiFunnel as FilterIcon } from 'react-icons/hi2'; +import { ButtonContainer, FilterContainer, Box, OperatorSelect } from './style'; +import { Filter, operators } from '../types'; + +type FormInputs = { + filter: Filter; +}; + +type EventFilterProps = { + mode: 'add' | 'edit'; + fields: string[]; + selectedFilter?: Filter; + addFilter: (filter: Filter) => void; +}; + +type FilterPopupProps = EventFilterProps & { + setOpen: (open: boolean) => void; +}; + +export const FilterPopup: FC = ({ selectedFilter, fields, addFilter, mode, setOpen }) => { + const { control, handleSubmit } = useForm({ + mode: 'onSubmit', + shouldUnregister: true, + ...(selectedFilter && { + defaultValues: { + filter: { + field: selectedFilter.field, + operator: selectedFilter.operator, + value: selectedFilter.value, + }, + }, + }), + }); + + const onSubmit: SubmitHandler = ({ filter }) => { + addFilter(filter); + setOpen(false); + }; + + return ( + +

{mode} filter

+ +
+ + + + { + return Object.keys(operators)[selectedIndex] ?? 'select operator'; + }} + > + + {Object.keys(operators).map((operator) => ( + +
+ {operator} +
+
+ ))} +
+
+ +
+ + +