diff --git a/src/diagnostics-schema.ts b/src/diagnostics-schema.ts index f888df7..fcc0221 100644 --- a/src/diagnostics-schema.ts +++ b/src/diagnostics-schema.ts @@ -25,6 +25,7 @@ export interface DiagnosticsTabDescriptor { target_value?: number; key_label?: string; value_labels?: string[]; + show_search_box?: boolean; statistic_id?: string; statistic_ids?: string[]; is_empty_tab?: boolean; diff --git a/src/table-search.test.ts b/src/table-search.test.ts new file mode 100644 index 0000000..6e7c00f --- /dev/null +++ b/src/table-search.test.ts @@ -0,0 +1,29 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +import { describe, expect, it } from 'vitest'; +import { matchesTableSearch } from './table-search'; + +describe('matchesTableSearch', () => { + const cells = ['Player Count', 'Active', '12.0']; + + it('shows all rows for an empty or whitespace-only query', () => { + expect(matchesTableSearch('', cells)).toBe(true); + expect(matchesTableSearch(' ', cells)).toBe(true); + expect(matchesTableSearch('', [''])).toBe(true); + }); + + it('matches keys and values without regard to case', () => { + expect(matchesTableSearch('PLAYER', cells)).toBe(true); + expect(matchesTableSearch('active', cells)).toBe(true); + expect(matchesTableSearch('12.0', cells)).toBe(true); + }); + + it('ignores surrounding query whitespace', () => { + expect(matchesTableSearch(' count ', cells)).toBe(true); + }); + + it('does not match unrelated rows or across cell boundaries', () => { + expect(matchesTableSearch('missing', cells)).toBe(false); + expect(matchesTableSearch('CountActive', cells)).toBe(false); + }); +}); diff --git a/src/table-search.ts b/src/table-search.ts new file mode 100644 index 0000000..56caf9f --- /dev/null +++ b/src/table-search.ts @@ -0,0 +1,6 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +export function matchesTableSearch(query: string, cells: string[]): boolean { + const normalizedQuery = query.trim().toLowerCase(); + return cells.some(cell => cell.toLowerCase().includes(normalizedQuery)); +} diff --git a/webview-ui/src/diagnostics_panel/DynamicTab.tsx b/webview-ui/src/diagnostics_panel/DynamicTab.tsx index b1736d2..d4ffc2d 100644 --- a/webview-ui/src/diagnostics_panel/DynamicTab.tsx +++ b/webview-ui/src/diagnostics_panel/DynamicTab.tsx @@ -139,6 +139,7 @@ export default function DynamicTab({ descriptor, selectedClient, selectedPlugin valueLabel={descriptor.y_label ?? 'Value'} statisticDataProvider={provider} statisticResolver={resolver} + showSearchBox={descriptor.show_search_box} /> ); @@ -150,6 +151,7 @@ export default function DynamicTab({ descriptor, selectedClient, selectedPlugin valueLabels={descriptor.value_labels ?? ['Value']} statisticDataProvider={provider as MultipleStatisticProvider} statisticResolver={resolver} + showSearchBox={descriptor.show_search_box} /> ); diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftDynamicPropertiesTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftDynamicPropertiesTable.tsx index 6da9d10..8d92aec 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftDynamicPropertiesTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftDynamicPropertiesTable.tsx @@ -1,8 +1,14 @@ // Copyright (C) Microsoft Corporation. All rights reserved. import { useEffect, useState } from 'react'; -import { VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow } from '@vscode/webview-ui-toolkit/react'; +import { + VSCodeDataGrid, + VSCodeDataGridCell, + VSCodeDataGridRow, + VSCodeTextField, +} from '@vscode/webview-ui-toolkit/react'; import { StatisticProvider, StatisticUpdatedMessage } from '../StatisticProvider'; +import { matchesTableSearch } from '../../../../src/table-search'; function updateCachedData(propertyCache: Map, data: string[][]): boolean { let hasChanges = false; @@ -31,63 +37,73 @@ function updateCachedData(propertyCache: Map, data: string[][]): return hasChanges; } -export function MinecraftDynamicPropertiesTable(statisticDataProviders: Record) { +type MinecraftDynamicPropertiesTableProps = { + statisticDataProviders: StatisticProvider; + showSearchBox?: boolean; +}; + +export function MinecraftDynamicPropertiesTable({ + statisticDataProviders, + showSearchBox = false, +}: MinecraftDynamicPropertiesTableProps) { // the groups directly under the 'statParentId' const [dynamicValueCache, setDynamicValueCache] = useState>(new Map()); + const [searchQuery, setSearchQuery] = useState(''); //draws chart useEffect(() => { - const eventHandlersByName = new Map void>(); - - Object.keys(statisticDataProviders).forEach(statisticDataProviderName => { - const statsProvider = statisticDataProviders[statisticDataProviderName]; - const eventHandler = (event: StatisticUpdatedMessage): void => { - // Update data with new data point - setDynamicValueCache((prevCache: Map): Map => { - const propertyCache = new Map(prevCache); - if (updateCachedData(propertyCache, event.children_string_values)) { - return propertyCache; - } - - return prevCache; - }); - }; + const eventHandler = (event: StatisticUpdatedMessage): void => { + // Update data with new data point + setDynamicValueCache((prevCache: Map): Map => { + const propertyCache = new Map(prevCache); + if (updateCachedData(propertyCache, event.children_string_values)) { + return propertyCache; + } - statsProvider.registerWindowListener(window); - statsProvider.addSubscriber(eventHandler); + return prevCache; + }); + }; - eventHandlersByName.set(statisticDataProviderName, eventHandler); - }); + statisticDataProviders.registerWindowListener(window); + statisticDataProviders.addSubscriber(eventHandler); // Remove old listener return () => { - Object.keys(statisticDataProviders).forEach(statisticDataProviderName => { - const statsProvider = statisticDataProviders[statisticDataProviderName]; - - const eventHandler = eventHandlersByName.get(statisticDataProviderName); - if (eventHandler) { - statsProvider.removeSubscriber(eventHandler); - } - statsProvider.unregisterWindowListener(window); - }); + statisticDataProviders.removeSubscriber(eventHandler); + statisticDataProviders.unregisterWindowListener(window); }; }, [dynamicValueCache]); + const visibleProperties = Array.from(dynamicValueCache.entries()).filter(([name, value]) => + matchesTableSearch(searchQuery, [name, value]) + ); + return ( - - - - {'Name'} - - - {'Value'} - - - {Array.from(dynamicValueCache.entries()).map(event => ( - - {`${event[0]}`} - {`${event[1]}`} + <> + {showSearchBox && ( + setSearchQuery((event.target as HTMLInputElement).value)} + > + Search + + )} + + + + {'Name'} + + + {'Value'} + - ))} - + {visibleProperties.map(([name, value]) => ( + + {name} + {value} + + ))} + + ); } diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index 8ad4c41..426cc28 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -10,7 +10,9 @@ import { VSCodeDropdown, VSCodeOption, VSCodeButton, + VSCodeTextField, } from '@vscode/webview-ui-toolkit/react'; +import { matchesTableSearch } from '../../../../src/table-search'; export enum MinecraftMultiColumnStatisticTableSortOrder { Ascending, @@ -59,6 +61,7 @@ type MinecraftMultiColumnStatisticTableProps = { rowAction?: MinecraftMultiColumnStatisticTableRowAction; // Optional per-row action button nonConsolidatedColumnResolver?: NonConsolidatedColumnResolver; // Maps split events to target columns for non-consolidated streams valueFormatter?: (value: string | number, columnIndex: number) => string; // Optional custom display formatter for cell values + showSearchBox?: boolean; }; const sortOrderOptions = [ @@ -146,6 +149,7 @@ export default function MinecraftMultiColumnStatisticTable({ rowAction, nonConsolidatedColumnResolver, valueFormatter, + showSearchBox = false }: MinecraftMultiColumnStatisticTableProps): JSX.Element { // Memoize sort column options to prevent unnecessary recreations const sortColumnOptions = useMemo( @@ -158,6 +162,7 @@ export default function MinecraftMultiColumnStatisticTable({ // states const [data, setData] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); const [selectedSortOrder, setSelectedSortOrder] = useState(defaultSortOrder); const [selectedSortType, setSelectedSortType] = @@ -370,9 +375,31 @@ export default function MinecraftMultiColumnStatisticTable({ prettifyNames, ]); + const visibleData = showSearchBox + ? data.filter(dataPoint => matchesTableSearch(searchQuery, [ + dataPoint.category, + ...dataPoint.values.map((value, index) => + valueFormatter + ? valueFormatter(value, index) + : typeof value === 'number' + ? value.toFixed(1) + : value, + ), + ])) + : data; + return (

{title}

+ {showSearchBox && ( + setSearchQuery((event.target as HTMLInputElement).value)} + > + Search + + )} {actions?.length && (
{actions.map(action => ( @@ -464,7 +491,7 @@ export default function MinecraftMultiColumnStatisticTable({ )} - {data.map(dataPoint => ( + {visibleData.map(dataPoint => ( ([]); + const [searchQuery, setSearchQuery] = useState(''); const [selectedSortOrder, setSelectedSortOrder] = useState(defaultSortOrder); const [selectedSortType, setSelectedSortType] = useState(defaultSortType); @@ -115,9 +120,25 @@ export default function MinecraftStatisticTable({ }; }, [statisticDataProvider, selectedSortOrder, selectedSortType]); + const visibleData = showSearchBox + ? data.filter(dataPoint => matchesTableSearch(searchQuery, [ + dataPoint.category ?? '', + dataPoint.absoluteValue.toFixed(1), + ])) + : data; + return (

{title}

+ {showSearchBox && ( + setSearchQuery((event.target as HTMLInputElement).value)} + > + Search + + )}
@@ -154,7 +175,7 @@ export default function MinecraftStatisticTable({ {valueLabel} - {data.map(dataPoint => ( + {visibleData.map(dataPoint => ( {dataPoint.category} {dataPoint.absoluteValue.toFixed(1)} diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/DynamicProperties.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/DynamicProperties.tsx index 9bb6d00..f2ba2e9 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/DynamicProperties.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/DynamicProperties.tsx @@ -9,6 +9,7 @@ const statsTab: TabPrefab = { return (