From 2e913a63166b78c0904614f083b52ebbbc4debe7 Mon Sep 17 00:00:00 2001 From: tabarra <1808295+tabarra@users.noreply.github.com> Date: Sun, 17 Mar 2024 15:53:23 -0300 Subject: [PATCH] wip(web/players): assorted changes --- core/extras/helpers.ts | 4 +- core/webroutes/player/search.ts | 5 +- docs/dev_notes.md | 11 ++-- panel/src/pages/Players/PlayersPage.tsx | 26 ++++----- panel/src/pages/Players/PlayersSearchBox.tsx | 11 +++- panel/src/pages/Players/PlayersTable.tsx | 59 ++++++++++---------- 6 files changed, 65 insertions(+), 51 deletions(-) diff --git a/core/extras/helpers.ts b/core/extras/helpers.ts index 2a0b6e139..4bc0c2721 100644 --- a/core/extras/helpers.ts +++ b/core/extras/helpers.ts @@ -199,7 +199,9 @@ export const parseLaxIdsArrayInput = (fullInput: string) => { for (const input of inputs) { if (input.includes(':')) { if (consts.regexValidHwidToken.test(input)) { - validHwids.push(input) + validHwids.push(input); + }else if (Object.values(consts.validIdentifiers).some((regex) => regex.test(input))){ + validIds.push(input); } else { const [type, value] = input.split(':', 1); if (consts.validIdentifierParts[type as keyof typeof consts.validIdentifierParts]?.test(value)) { diff --git a/core/webroutes/player/search.ts b/core/webroutes/player/search.ts index e7e673809..8dc19f4aa 100644 --- a/core/webroutes/player/search.ts +++ b/core/webroutes/player/search.ts @@ -39,8 +39,6 @@ export default async function PlayerModal(ctx: AuthedCtx) { const dbo = ctx.txAdmin.playerDatabase.getDb(); let chain = dbo.chain.get('players'); - console.dir(ctx.query); //DEBUG - /* In order: - [X] sort the players by the sortingKey/sortingDesc @@ -109,6 +107,7 @@ export default async function PlayerModal(ctx: AuthedCtx) { } if (searchType === 'playerName') { + //Searching by player name const { pureName } = cleanPlayerName(searchValue); if (pureName === 'emptyname') { return sendTypedResp({ error: 'This player name is unsearchable (pureName is empty).' }); @@ -122,6 +121,7 @@ export default async function PlayerModal(ctx: AuthedCtx) { const filtered = fuse.search(pureName).map(x => x.item); chain = createChain(filtered); } else if (searchType === 'playerNotes') { + //Searching by player notes const players = chain.value(); const fuse = new Fuse(players, { keys: ['notes.text'], @@ -130,6 +130,7 @@ export default async function PlayerModal(ctx: AuthedCtx) { const filtered = fuse.search(searchValue).map(x => x.item); chain = createChain(filtered); } else if (searchType === 'playerIds') { + //Searching by player identifiers const { validIds, validHwids, invalidIds } = parseLaxIdsArrayInput(searchValue); if (invalidIds.length) { return sendTypedResp({ error: `Invalid identifiers (${invalidIds.join(',')}). Prefix any identifier with their type, like 'fivem:123456' instead of just '123456'.` }); diff --git a/docs/dev_notes.md b/docs/dev_notes.md index e2e4bbf17..0dc0c0bc2 100644 --- a/docs/dev_notes.md +++ b/docs/dev_notes.md @@ -18,14 +18,15 @@ - [x] pressing enter on the license input text in setup page refreshes the page - [x] can I remove `/nui/resetSession`? I think we don't even use cookies anymore - [ ] NEW PAGE: Players - - [ ] test everything - - [ ] show online/notes/admin + - [X] make sure it is not spamming search requests at the start (remove debug print on the route) + - [x] show online/notes/admin + - [x] test everything + - [x] ~~Write `estimateSize` function to calculate size dynamically?~~ made it no-wrap + - [x] code the hotkey - [ ] temporarily, dropdown redirects: - Legacy Ban -> old players page - prune players/hwids (from master actions -> clean database) - - [ ] code the hotkey - - [ ] make sure it is not spamming search requests at the start (remove debug print on the route) - - [ ] Write `estimateSize` function to calculate size dynamically? + - [ ] track search duration in StatsManager just like we do for join checks - [ ] NEW PAGE: History - [ ] fix disallowed intents message diff --git a/panel/src/pages/Players/PlayersPage.tsx b/panel/src/pages/Players/PlayersPage.tsx index 5351c6d2f..d4aa3d90c 100644 --- a/panel/src/pages/Players/PlayersPage.tsx +++ b/panel/src/pages/Players/PlayersPage.tsx @@ -15,10 +15,7 @@ const PageCalloutRowMemo = memo(PageCalloutRow); export default function PlayersPage() { const [calloutData, setCalloutData] = useState(undefined); - const [searchBoxReturn, setSearchBoxReturn] = useState({ - search: null, - filters: [], - }); + const [searchBoxReturn, setSearchBoxReturn] = useState(undefined); const statsApi = useBackendApi({ method: 'GET', path: '/player/stats', @@ -46,8 +43,9 @@ export default function PlayersPage() { }, []); + const hasCalloutData = calloutData && !('error' in calloutData); return (
{/*
, prefix: '' }, { label: 'Players Today', - value: calloutData?.playedLast24h ?? false, + value: hasCalloutData ? calloutData.playedLast24h : false, icon: , prefix: '' }, { label: 'New Players Today', - value: calloutData?.joinedLast24h ?? false, + value: hasCalloutData ? calloutData.joinedLast24h : false, icon: , prefix: '+' }, { label: 'New Players This Week', - value: calloutData?.joinedLast7d ?? false, + value: hasCalloutData ? calloutData.joinedLast7d : false, icon: , prefix: '+' } @@ -88,9 +86,11 @@ export default function PlayersPage() { doSearch={doSearch} initialState={initialState} /> - + {searchBoxReturn ? ( + + ) : null}
); } diff --git a/panel/src/pages/Players/PlayersSearchBox.tsx b/panel/src/pages/Players/PlayersSearchBox.tsx index 4c577e128..d0b449e16 100644 --- a/panel/src/pages/Players/PlayersSearchBox.tsx +++ b/panel/src/pages/Players/PlayersSearchBox.tsx @@ -15,6 +15,7 @@ import { } from "@/components/ui/dropdown-menu"; import InlineCode from '@/components/InlineCode'; import { PlayersTableFiltersType, PlayersTableSearchType } from "@shared/playerApiTypes"; +import { useEventListener } from "usehooks-ts"; /** @@ -92,7 +93,7 @@ export function PlayerSearchBox({ doSearch, initialState }: PlayerSearchBoxProps //Call onSearch when params change useEffect(() => { updateSearch(); - }, [inputRef, currSearchType, selectedFilters]); + }, [currSearchType, selectedFilters]); //Input handlers const handleInputKeyDown = (e: React.KeyboardEvent) => { @@ -124,6 +125,14 @@ export function PlayerSearchBox({ doSearch, initialState }: PlayerSearchBoxProps } } + //Search hotkey + useEventListener('keydown', (e: KeyboardEvent) => { + if (e.code === 'KeyF' && (e.ctrlKey || e.metaKey)) { + inputRef.current?.focus(); + e.preventDefault(); + } + }); + //It's render time! 🎉 const selectedSearchType = availableSearchTypes.find((type) => type.value === currSearchType); if (!selectedSearchType) throw new Error(`Invalid search type: ${currSearchType}`); diff --git a/panel/src/pages/Players/PlayersTable.tsx b/panel/src/pages/Players/PlayersTable.tsx index 9caa9a588..7995471e0 100644 --- a/panel/src/pages/Players/PlayersTable.tsx +++ b/panel/src/pages/Players/PlayersTable.tsx @@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { ScrollArea } from '@/components/ui/scroll-area'; import TxAnchor from '@/components/TxAnchor'; -import { cn, createRandomHslColor, msToShortDuration, tsToLocaleDateTime } from '@/lib/utils'; +import { cn, msToShortDuration, tsToLocaleDateTime } from '@/lib/utils'; import { TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table"; -import { Loader2Icon } from 'lucide-react'; +import { Loader2Icon, ShieldCheckIcon, ActivitySquareIcon, FileTextIcon } from 'lucide-react'; import { useOpenPlayerModal } from "@/hooks/playerModal"; import { PlayersTableSearchResp, PlayersTableFiltersType, PlayersTableSearchType, PlayersTableSortingType, PlayersTablePlayerType } from '@shared/playerApiTypes'; import { useBackendApi } from '@/hooks/fetch'; @@ -28,13 +28,20 @@ function PlayerRow({ rowData, modalOpener }: PlayerRowProps) { //border-r whitespace-nowrap text-ellipsis overflow-hidden return ( - {rowData.displayName} - ligma + + {rowData.displayName} +
+ + + +
+
{msToShortDuration(rowData.playTime * 60_000)} {convertRowDateTime(rowData.tsJoined)} {convertRowDateTime(rowData.tsLastConnection)} @@ -55,17 +62,17 @@ type LastRowProps = { function LastRow({ playersCount, hasReachedEnd, isFetching, loadError, retryFetch }: LastRowProps) { let content: React.ReactNode; - if (hasReachedEnd) { - content = - {playersCount ? 'You have reached the end of the list.' : 'No players found.'} - - } else if (isFetching) { + if (isFetching) { content = } else if (loadError) { content = <> Error: {loadError}.
+ } else if (hasReachedEnd) { + content = + {playersCount ? 'You have reached the end of the list.' : 'No players found.'} + } else { content = You've found the end of the rainbow, but there's no pot of gold here.
@@ -75,7 +82,7 @@ function LastRow({ playersCount, hasReachedEnd, isFetching, loadError, retryFetc return ( - + {content} @@ -91,9 +98,10 @@ type SortableTableHeaderProps = { sortKey: 'playTime' | 'tsJoined' | 'tsLastConnection'; sortingState: PlayersTableSortingType; setSorting: (newState: PlayersTableSortingType) => void; + className?: string; } -function SortableTableHeader({ label, sortKey, sortingState, setSorting }: SortableTableHeaderProps) { +function SortableTableHeader({ label, sortKey, sortingState, setSorting, className }: SortableTableHeaderProps) { const isSorted = sortingState.key === sortKey; const isDesc = sortingState.desc; const sortIcon = isSorted ? (isDesc ? 'â–¼' : 'â–²') : <>; @@ -108,8 +116,9 @@ function SortableTableHeader({ label, sortKey, sortingState, setSorting }: Sorta {label} @@ -231,12 +240,7 @@ export default function PlayersTable({ search, filters }: PlayersTableProps) { } }, [players, virtualItems, hasReachedEnd, isFetching]); - //fetch the first page automatically - useEffect(() => { - if (!players.length) fetchNextPage(true); - }, []); - - //on sorting change, reset the list + //on state change, reset the list useEffect(() => { rowVirtualizer.scrollToIndex(0); fetchNextPage(true); @@ -253,14 +257,11 @@ export default function PlayersTable({ search, filters }: PlayersTableProps) { style={{ color: createRandomHslColor() }} >{JSON.stringify({ search, filters, sorting })}
*/} - +
- - + {TopRowPad} {virtualItems.map((virtualItem) => { const isLastRow = virtualItem.index > players.length - 1;
- Display Name - - Status + Display Name