Compare commits

...

15 Commits

Author SHA1 Message Date
Ashley Harrison
50150f36fc TableRT sorta working 2025-12-03 12:33:16 +00:00
Ashley Harrison
c32da02543 migrate ResourceCards 2025-12-01 16:27:06 +00:00
Ashley Harrison
0f56441b8b upgrade Logs panel 2025-12-01 16:11:36 +00:00
Ashley Harrison
951994641f convert RawListContainer 2025-12-01 13:21:06 +00:00
Ashley Harrison
546aa0f614 convert DashboardPicker 2025-12-01 12:01:48 +00:00
Ashley Harrison
095c9b1a10 convert Typeahead 2025-12-01 11:41:18 +00:00
Ashley Harrison
fdcdf80eb6 convert LokiLabelBrowser 2025-12-01 11:36:08 +00:00
Ashley Harrison
039ee5802f convert AlertInstanceModalSelector 2025-12-01 11:33:56 +00:00
Ashley Harrison
cd50a4cef7 convert Table filters 2025-12-01 11:11:14 +00:00
Ashley Harrison
2ae4ff8b4d convert prometheus metrics browser 2025-12-01 10:54:47 +00:00
Ashley Harrison
6de2336232 convert NestedFolderList 2025-12-01 10:34:33 +00:00
Ashley Harrison
1e769cbc1b convert SelectMenu 2025-12-01 10:14:44 +00:00
Ashley Harrison
660dfb3aba update SearchResultsTable 2025-11-28 17:12:56 +00:00
Ashley Harrison
86294ca337 convert DashboardsTree 2025-11-28 15:59:06 +00:00
Ashley Harrison
2f32b59c2f update packages and remove @types/ packages since they're now included 2025-11-28 14:42:13 +00:00
28 changed files with 573 additions and 696 deletions

View File

@@ -843,9 +843,6 @@
} }
}, },
"packages/grafana-ui/src/components/Table/TableRT/Table.tsx": { "packages/grafana-ui/src/components/Table/TableRT/Table.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
},
"@typescript-eslint/no-explicit-any": { "@typescript-eslint/no-explicit-any": {
"count": 2 "count": 2
} }

View File

@@ -151,8 +151,6 @@
"@types/react-table": "7.7.20", "@types/react-table": "7.7.20",
"@types/react-transition-group": "4.4.12", "@types/react-transition-group": "4.4.12",
"@types/react-virtualized-auto-sizer": "1.0.8", "@types/react-virtualized-auto-sizer": "1.0.8",
"@types/react-window": "1.8.8",
"@types/react-window-infinite-loader": "^1",
"@types/redux-mock-store": "1.5.0", "@types/redux-mock-store": "1.5.0",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/slate": "0.47.11", "@types/slate": "0.47.11",
@@ -414,8 +412,8 @@
"react-use": "17.6.0", "react-use": "17.6.0",
"react-virtual": "2.10.4", "react-virtual": "2.10.4",
"react-virtualized-auto-sizer": "1.0.26", "react-virtualized-auto-sizer": "1.0.26",
"react-window": "1.8.11", "react-window": "2.2.3",
"react-window-infinite-loader": "1.0.10", "react-window-infinite-loader": "2.0.0",
"reduce-reducers": "^1.0.4", "reduce-reducers": "^1.0.4",
"redux": "5.0.1", "redux": "5.0.1",
"redux-thunk": "3.1.0", "redux-thunk": "3.1.0",

View File

@@ -60,7 +60,6 @@
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"@types/react-highlight-words": "0.20.0", "@types/react-highlight-words": "0.20.0",
"@types/react-window": "1.8.8",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/uuid": "10.0.0", "@types/uuid": "10.0.0",
"debounce-promise": "3.1.2", "debounce-promise": "3.1.2",
@@ -73,7 +72,7 @@
"prismjs": "1.30.0", "prismjs": "1.30.0",
"react-highlight-words": "0.21.0", "react-highlight-words": "0.21.0",
"react-use": "17.6.0", "react-use": "17.6.0",
"react-window": "1.8.11", "react-window": "2.2.3",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"semver": "7.7.3", "semver": "7.7.3",
"uuid": "11.1.0" "uuid": "11.1.0"

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { FixedSizeList } from 'react-window'; import { List } from 'react-window';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
@@ -65,15 +65,12 @@ export function MetricSelector() {
className={styles.valueListWrapper} className={styles.valueListWrapper}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList} data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList}
> >
<FixedSizeList <List
height={Math.min(450, filteredMetrics.length * LIST_ITEM_SIZE)} rowProps={{}}
itemCount={filteredMetrics.length} rowCount={filteredMetrics.length}
itemSize={LIST_ITEM_SIZE} rowHeight={LIST_ITEM_SIZE}
itemKey={(i) => filteredMetrics[i].name}
width={300}
className={styles.valueList} className={styles.valueList}
> rowComponent={({ index, style }) => {
{({ index, style }) => {
const metric = filteredMetrics[index]; const metric = filteredMetrics[index];
return ( return (
<div style={style}> <div style={style}>
@@ -92,7 +89,11 @@ export function MetricSelector() {
</div> </div>
); );
}} }}
</FixedSizeList> style={{
height: Math.min(450, filteredMetrics.length * LIST_ITEM_SIZE),
width: 300,
}}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FixedSizeList } from 'react-window'; import { List } from 'react-window';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n'; import { t, Trans } from '@grafana/i18n';
@@ -78,15 +78,16 @@ export function ValueSelector() {
<div className={styles.valueTitle}> <div className={styles.valueTitle}>
<PromLabel name={lk} active={true} hidden={false} facets={lv.length} onClick={onLabelKeyClick} /> <PromLabel name={lk} active={true} hidden={false} facets={lv.length} onClick={onLabelKeyClick} />
</div> </div>
<FixedSizeList <List
height={Math.min(200, LIST_ITEM_SIZE * (lv.length || 0))} rowProps={{}}
itemCount={lv.length || 0} rowCount={lv.length || 0}
itemSize={28} rowHeight={28}
itemKey={(i) => lv[i]} style={{
width={200} height: Math.min(200, LIST_ITEM_SIZE * (lv.length || 0)),
width: 200,
}}
className={styles.valueList} className={styles.valueList}
> rowComponent={({ index, style }) => {
{({ index, style }) => {
const value = lv[index]; const value = lv[index];
const isSelected = selectedLabelValues[lk]?.includes(value); const isSelected = selectedLabelValues[lk]?.includes(value);
return ( return (
@@ -101,7 +102,7 @@ export function ValueSelector() {
</div> </div>
); );
}} }}
</FixedSizeList> />
</div> </div>
); );
})} })}

View File

@@ -122,7 +122,7 @@
"react-table": "7.8.0", "react-table": "7.8.0",
"react-transition-group": "4.4.5", "react-transition-group": "4.4.5",
"react-use": "17.6.0", "react-use": "17.6.0",
"react-window": "1.8.11", "react-window": "2.2.3",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"slate": "0.47.9", "slate": "0.47.9",
"slate-plain-serializer": "0.7.13", "slate-plain-serializer": "0.7.13",
@@ -172,7 +172,6 @@
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"@types/react-highlight-words": "0.20.0", "@types/react-highlight-words": "0.20.0",
"@types/react-transition-group": "4.4.12", "@types/react-transition-group": "4.4.12",
"@types/react-window": "1.8.8",
"@types/slate": "0.47.11", "@types/slate": "0.47.11",
"@types/slate-plain-serializer": "0.7.5", "@types/slate-plain-serializer": "0.7.5",
"@types/slate-react": "0.22.9", "@types/slate-react": "0.22.9",

View File

@@ -1,8 +1,8 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { max } from 'lodash'; import { max } from 'lodash';
import { RefCallback, useLayoutEffect, useMemo, useRef, type JSX } from 'react'; import { RefCallback, useLayoutEffect, useMemo, type JSX } from 'react';
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList as List } from 'react-window'; import { List, useListRef } from 'react-window';
import { SelectableValue, toIconName } from '@grafana/data'; import { SelectableValue, toIconName } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@@ -101,14 +101,13 @@ interface VirtualSelectMenuProps<T> {
export const VirtualizedSelectMenu = ({ export const VirtualizedSelectMenu = ({
children, children,
maxHeight, maxHeight,
innerRef: scrollRef,
options, options,
selectProps, selectProps,
focusedOption, focusedOption,
}: VirtualSelectMenuProps<SelectableValue>) => { }: VirtualSelectMenuProps<SelectableValue>) => {
const theme = useTheme2(); const theme = useTheme2();
const styles = getSelectStyles(theme); const styles = getSelectStyles(theme);
const listRef = useRef<List>(null); const listRef = useListRef(null);
const { toggleAllOptions, components } = selectProps; const { toggleAllOptions, components } = selectProps;
const optionComponent = components?.Option ?? SelectMenuOptions; const optionComponent = components?.Option ?? SelectMenuOptions;
@@ -126,8 +125,10 @@ export const VirtualizedSelectMenu = ({
(option: SelectableValue<unknown>) => option.value === focusedOption?.value (option: SelectableValue<unknown>) => option.value === focusedOption?.value
); );
useLayoutEffect(() => { useLayoutEffect(() => {
listRef.current?.scrollToItem(focusedIndex); listRef.current?.scrollToRow({
}, [focusedIndex]); index: focusedIndex,
});
}, [focusedIndex, listRef]);
if (!Array.isArray(children)) { if (!Array.isArray(children)) {
return null; return null;
@@ -180,17 +181,20 @@ export const VirtualizedSelectMenu = ({
return ( return (
<List <List
outerRef={scrollRef} rowComponent={({ index, style }) => (
ref={listRef} <div style={{ ...style, overflow: 'hidden' }}>{flattenedChildren[index]}</div>
)}
rowCount={flattenedChildren.length}
rowHeight={VIRTUAL_LIST_ITEM_HEIGHT}
rowProps={{}}
listRef={listRef}
className={styles.menu} className={styles.menu}
height={heightEstimate} style={{
width={widthEstimate} height: heightEstimate,
width: widthEstimate,
}}
aria-label={t('grafana-ui.select.menu-label', 'Select options menu')} aria-label={t('grafana-ui.select.menu-label', 'Select options menu')}
itemCount={flattenedChildren.length} />
itemSize={VIRTUAL_LIST_ITEM_HEIGHT}
>
{({ index, style }) => <div style={{ ...style, overflow: 'hidden' }}>{flattenedChildren[index]}</div>}
</List>
); );
}; };

View File

@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { List, type RowComponentProps } from 'react-window';
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@@ -162,15 +162,16 @@ export const FilterList = ({ options, values, caseSensitive, onChange, searchFil
{items.length > 0 ? ( {items.length > 0 ? (
<> <>
<List <List
height={height} rowComponent={ItemRenderer}
itemCount={items.length} rowCount={items.length}
itemSize={ITEM_HEIGHT} rowHeight={ITEM_HEIGHT}
itemData={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }} rowProps={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
width="100%" style={{
height,
width: '100%',
}}
className={styles.filterList} className={styles.filterList}
> />
{ItemRenderer}
</List>
<div <div
className={styles.filterListRow} className={styles.filterListRow}
data-testid={selectors.components.Panels.Visualization.TableNG.Filters.SelectAll} data-testid={selectors.components.Panels.Visualization.TableNG.Filters.SelectAll}
@@ -193,16 +194,21 @@ export const FilterList = ({ options, values, caseSensitive, onChange, searchFil
); );
}; };
interface ItemRendererProps extends ListChildComponentProps { interface ItemRendererProps {
data: { onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void; items: SelectableValue[];
items: SelectableValue[]; values: SelectableValue[];
values: SelectableValue[]; className: string;
className: string;
};
} }
function ItemRenderer({ index, style, data: { onCheckedChanged, items, values, className } }: ItemRendererProps) { function ItemRenderer({
index,
style,
onCheckedChanged,
items,
values,
className,
}: RowComponentProps<ItemRendererProps>) {
const option = items[index]; const option = items[index];
const { value, label } = option; const { value, label } = option;
const isChecked = values.find((s) => s.value === value) !== undefined; const isChecked = values.find((s) => s.value === value) !== undefined;

View File

@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { List, type RowComponentProps } from 'react-window';
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
import { t, Trans } from '@grafana/i18n'; import { t, Trans } from '@grafana/i18n';
@@ -202,15 +202,16 @@ export const FilterList = ({
{items.length > 0 ? ( {items.length > 0 ? (
<> <>
<List <List
height={height} rowComponent={ItemRenderer}
itemCount={items.length} rowCount={items.length}
itemSize={ITEM_HEIGHT} rowHeight={ITEM_HEIGHT}
itemData={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }} rowProps={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
width="100%" style={{
height,
width: '100%',
}}
className={styles.filterList} className={styles.filterList}
> />
{ItemRenderer}
</List>
<Stack direction="column" gap={0.25}> <Stack direction="column" gap={0.25}>
<div className={cx(styles.selectDivider)} /> <div className={cx(styles.selectDivider)} />
<div className={cx(styles.filterListRow)}> <div className={cx(styles.filterListRow)}>
@@ -233,16 +234,21 @@ export const FilterList = ({
); );
}; };
interface ItemRendererProps extends ListChildComponentProps { interface ItemRendererProps {
data: { onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void; items: SelectableValue[];
items: SelectableValue[]; values: SelectableValue[];
values: SelectableValue[]; className: string;
className: string;
};
} }
function ItemRenderer({ index, style, data: { onCheckedChanged, items, values, className } }: ItemRendererProps) { function ItemRenderer({
index,
style,
onCheckedChanged,
items,
values,
className,
}: RowComponentProps<ItemRendererProps>) {
const option = items[index]; const option = items[index];
const { value, label } = option; const { value, label } = option;
const isChecked = values.find((s) => s.value === value) !== undefined; const isChecked = values.find((s) => s.value === value) !== undefined;

View File

@@ -1,8 +1,8 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; import { CSSProperties, useCallback, useEffect, useMemo, useState } from 'react';
import * as React from 'react'; import * as React from 'react';
import { Cell, Row, TableState, HeaderGroup } from 'react-table'; import { Cell, Row, TableState, HeaderGroup } from 'react-table';
import { VariableSizeList } from 'react-window'; import { List, useListRef } from 'react-window';
import { Subscription, debounceTime } from 'rxjs'; import { Subscription, debounceTime } from 'rxjs';
import { import {
@@ -18,7 +18,6 @@ import {
import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema'; import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema';
import { useTheme2 } from '../../../themes/ThemeContext'; import { useTheme2 } from '../../../themes/ThemeContext';
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
import { usePanelContext } from '../../PanelChrome'; import { usePanelContext } from '../../PanelChrome';
import { TableCell } from '../Cells/TableCell'; import { TableCell } from '../Cells/TableCell';
import { import {
@@ -42,14 +41,10 @@ interface RowsListProps {
data: DataFrame; data: DataFrame;
rows: Row[]; rows: Row[];
enableSharedCrosshair: boolean; enableSharedCrosshair: boolean;
headerHeight: number;
rowHeight: number;
itemCount: number; itemCount: number;
pageIndex: number;
listHeight: number; listHeight: number;
width: number; width: number;
cellHeight?: TableCellHeight; cellHeight?: TableCellHeight;
listRef: React.RefObject<VariableSizeList>;
tableState: TableState; tableState: TableState;
tableStyles: TableStyles; tableStyles: TableStyles;
nestedDataField?: Field; nestedDataField?: Field;
@@ -70,11 +65,8 @@ export const RowsList = (props: RowsListProps) => {
const { const {
data, data,
rows, rows,
headerHeight,
footerPaginationEnabled, footerPaginationEnabled,
rowHeight,
itemCount, itemCount,
pageIndex,
tableState, tableState,
prepareRow, prepareRow,
onCellFilterAdded, onCellFilterAdded,
@@ -84,7 +76,6 @@ export const RowsList = (props: RowsListProps) => {
tableStyles, tableStyles,
nestedDataField, nestedDataField,
listHeight, listHeight,
listRef,
enableSharedCrosshair = false, enableSharedCrosshair = false,
initialRowIndex = undefined, initialRowIndex = undefined,
headerGroups, headerGroups,
@@ -95,11 +86,25 @@ export const RowsList = (props: RowsListProps) => {
setInspectCell, setInspectCell,
} = props; } = props;
const listRef = useListRef(null);
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex); const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
if (initialRowIndex === undefined && rowHighlightIndex !== undefined) { if (initialRowIndex === undefined && rowHighlightIndex !== undefined) {
setRowHighlightIndex(undefined); setRowHighlightIndex(undefined);
} }
useEffect(() => {
if (rowHighlightIndex !== undefined) {
// TODO can we do this without a setTimeout?
setTimeout(() => {
listRef.current?.scrollToRow({
index: rowHighlightIndex,
align: 'center',
behavior: 'instant',
});
});
}
}, [rowHighlightIndex, listRef]);
const theme = useTheme2(); const theme = useTheme2();
const panelContext = usePanelContext(); const panelContext = usePanelContext();
@@ -234,15 +239,6 @@ export const RowsList = (props: RowsListProps) => {
}; };
}, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]); }, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]);
let scrollTop: number | undefined = undefined;
if (rowHighlightIndex !== undefined) {
const firstMatchedRowIndex = rows.findIndex((row) => row.index === rowHighlightIndex);
if (firstMatchedRowIndex !== -1) {
scrollTop = headerHeight + (firstMatchedRowIndex - 1) * rowHeight;
}
}
const rowIndexForPagination = useCallback( const rowIndexForPagination = useCallback(
(index: number) => { (index: number) => {
return tableState.pageIndex * tableState.pageSize + index; return tableState.pageIndex * tableState.pageSize + index;
@@ -316,6 +312,17 @@ export const RowsList = (props: RowsListProps) => {
); );
style.height = bbox.height; style.height = bbox.height;
} }
// some disgusting code to mutate the style object to convert transform to top
// this is all so that hover behaviour is maintained
// using transform creates new stacking contexts which means hover states don't overlay correctly
const yPos = style.transform?.match(/translateY\((.*)\)/)?.[1];
style = {
...style,
top: yPos,
transform: undefined,
};
const { key, ...rowProps } = row.getRowProps({ style, ...additionalProps }); const { key, ...rowProps } = row.getRowProps({ style, ...additionalProps });
return ( return (
@@ -414,36 +421,17 @@ export const RowsList = (props: RowsListProps) => {
return tableStyles.rowHeight; return tableStyles.rowHeight;
}; };
const handleScroll: UIEventHandler = (event) => {
const { scrollTop } = event.currentTarget;
if (listRef.current !== null) {
listRef.current.scrollTo(scrollTop);
}
};
// It's a hack for text wrapping.
// VariableSizeList component didn't know that we manually set row height.
// So we need to reset the list when the rows high changes.
useEffect(() => {
if (listRef.current) {
listRef.current.resetAfterIndex(0);
}
}, [rows, listRef]);
return ( return (
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}> <List
<VariableSizeList rowProps={{}}
key={`${rowHeight}${pageIndex}`} rowHeight={getItemSize}
height={listHeight} rowCount={itemCount}
itemCount={itemCount} listRef={listRef}
itemSize={getItemSize} style={{
width={'100%'} height: listHeight,
ref={listRef} width,
style={{ overflow: undefined }} }}
> rowComponent={({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })} />
</VariableSizeList>
</CustomScrollbar>
); );
}; };

View File

@@ -8,7 +8,6 @@ import {
useSortBy, useSortBy,
useTable, useTable,
} from 'react-table'; } from 'react-table';
import { VariableSizeList } from 'react-window';
import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data'; import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@@ -16,12 +15,10 @@ import { Trans } from '@grafana/i18n';
import { TableCellHeight } from '@grafana/schema'; import { TableCellHeight } from '@grafana/schema';
import { useTheme2 } from '../../../themes/ThemeContext'; import { useTheme2 } from '../../../themes/ThemeContext';
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
import { Pagination } from '../../Pagination/Pagination'; import { Pagination } from '../../Pagination/Pagination';
import { TableCellInspector } from '../TableCellInspector'; import { TableCellInspector } from '../TableCellInspector';
import { useFixScrollbarContainer, useResetVariableListSizeCache } from '../hooks';
import { getInitialState, useTableStateReducer } from '../reducer'; import { getInitialState, useTableStateReducer } from '../reducer';
import { FooterItem, GrafanaTableState, InspectCell, TableRTProps as Props } from '../types'; import { FooterItem, InspectCell, TableRTProps as Props } from '../types';
import { import {
getColumns, getColumns,
sortCaseInsensitive, sortCaseInsensitive,
@@ -70,7 +67,6 @@ export const Table = memo((props: Props) => {
replaceVariables, replaceVariables,
} = props; } = props;
const listRef = useRef<VariableSizeList>(null);
const tableDivRef = useRef<HTMLDivElement>(null); const tableDivRef = useRef<HTMLDivElement>(null);
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null); const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
const theme = useTheme2(); const theme = useTheme2();
@@ -200,7 +196,6 @@ export const Table = memo((props: Props) => {
toggleAllRowsExpanded, toggleAllRowsExpanded,
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination); } = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
const extendedState = state as GrafanaTableState;
toggleAllRowsExpandedRef.current = toggleAllRowsExpanded; toggleAllRowsExpandedRef.current = toggleAllRowsExpanded;
/* /*
@@ -270,9 +265,6 @@ export const Table = memo((props: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]); }, [data]);
useResetVariableListSizeCache(extendedState, listRef, data, hasUniqueId);
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
const onNavigate = useCallback( const onNavigate = useCallback(
(toPage: number) => { (toPage: number) => {
gotoPage(toPage - 1); gotoPage(toPage - 1);
@@ -340,60 +332,51 @@ export const Table = memo((props: Props) => {
ref={tableDivRef} ref={tableDivRef}
style={{ width, height }} style={{ width, height }}
> >
<CustomScrollbar hideVerticalTrack={true}> <div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}> {!noHeader && (
{!noHeader && ( <HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} /> )}
)} {itemCount > 0 ? (
{itemCount > 0 ? ( <div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
<div <RowsList
data-testid={selectors.components.Panels.Visualization.Table.body} headerGroups={headerGroups}
ref={variableSizeListScrollbarRef} data={data}
> rows={rows}
<RowsList width={width}
headerGroups={headerGroups} cellHeight={cellHeight}
data={data} itemCount={itemCount}
rows={rows} listHeight={listHeight}
width={width} tableState={state}
cellHeight={cellHeight} prepareRow={prepareRow}
headerHeight={headerHeight} timeRange={timeRange}
rowHeight={tableStyles.rowHeight} onCellFilterAdded={onCellFilterAdded}
itemCount={itemCount} nestedDataField={nestedDataField}
pageIndex={state.pageIndex}
listHeight={listHeight}
listRef={listRef}
tableState={state}
prepareRow={prepareRow}
timeRange={timeRange}
onCellFilterAdded={onCellFilterAdded}
nestedDataField={nestedDataField}
tableStyles={tableStyles}
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
textWrapField={textWrapField}
getActions={getActions}
replaceVariables={replaceVariables}
setInspectCell={setInspectCell}
/>
</div>
) : (
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
{noValuesDisplayText}
</div>
)}
{footerItems && (
<FooterRow
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
tableStyles={tableStyles} tableStyles={tableStyles}
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
textWrapField={textWrapField}
getActions={getActions}
replaceVariables={replaceVariables}
setInspectCell={setInspectCell}
/> />
)} </div>
</div> ) : (
</CustomScrollbar> <div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
{noValuesDisplayText}
</div>
)}
{footerItems && (
<FooterRow
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
tableStyles={tableStyles}
/>
)}
</div>
{paginationEl} {paginationEl}
</div> </div>

View File

@@ -111,6 +111,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
width: '100%', width: '100%',
overflow: 'auto', overflow: 'auto',
display: 'flex', display: 'flex',
position: 'relative',
flexDirection: 'column', flexDirection: 'column',
}), }),
thead: css({ thead: css({

View File

@@ -1,83 +0,0 @@
import { useEffect } from 'react';
import * as React from 'react';
import { VariableSizeList } from 'react-window';
import { DataFrame } from '@grafana/data';
import { GrafanaTableState } from './types';
/**
To have the custom vertical scrollbar always visible (https://github.com/grafana/grafana/issues/52136),
we need to bring the element from the VariableSizeList scope to the outer Table container scope,
because the VariableSizeList scope has overflow. By moving scrollbar to container scope we will have
it always visible since the entire width is in view.
Select the scrollbar element from the VariableSizeList scope
*/
export function useFixScrollbarContainer(
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement>,
tableDivRef: React.RefObject<HTMLDivElement>
) {
useEffect(() => {
if (variableSizeListScrollbarRef.current && tableDivRef.current) {
const listVerticalScrollbarHTML = variableSizeListScrollbarRef.current.querySelector('.track-vertical');
// Select Table custom scrollbars
const tableScrollbarView = tableDivRef.current.firstChild;
//If they exist, move the scrollbar element to the Table container scope
if (tableScrollbarView && listVerticalScrollbarHTML) {
listVerticalScrollbarHTML.remove();
if (tableScrollbarView instanceof HTMLElement) {
tableScrollbarView.querySelector(':scope > .track-vertical')?.remove();
tableScrollbarView.append(listVerticalScrollbarHTML);
}
}
}
});
}
/**
react-table caches the height of cells, so we need to reset them when expanding/collapsing rows.
We use `lastExpandedOrCollapsedIndex` since collapsed rows disappear from `expandedIndexes` but still keep their expanded
height.
*/
export function useResetVariableListSizeCache(
extendedState: GrafanaTableState,
listRef: React.RefObject<VariableSizeList>,
data: DataFrame,
hasUniqueId: boolean
) {
// Make sure we trigger the reset when keys change in any way
const expandedRowsRepr = JSON.stringify(Object.keys(extendedState.expanded));
useEffect(() => {
// By default, reset all rows
let resetIndex = 0;
// If we have unique field, extendedState.expanded keys are not row indexes but IDs so instead of trying to search
// for correct index we just reset the whole table.
if (!hasUniqueId) {
// If we don't have we reset from the last changed index.
if (Number.isFinite(extendedState.lastExpandedOrCollapsedIndex)) {
resetIndex = extendedState.lastExpandedOrCollapsedIndex!;
}
// Account for paging.
resetIndex =
extendedState.pageIndex === 0
? resetIndex - 1
: resetIndex - extendedState.pageIndex - extendedState.pageIndex * extendedState.pageSize;
}
listRef.current?.resetAfterIndex(Math.max(resetIndex, 0));
return;
}, [
extendedState.lastExpandedOrCollapsedIndex,
extendedState.pageSize,
extendedState.pageIndex,
listRef,
data,
expandedRowsRepr,
hasUniqueId,
]);
}

View File

@@ -3,7 +3,7 @@ import { isEqual } from 'lodash';
import { createRef, PureComponent } from 'react'; import { createRef, PureComponent } from 'react';
import * as React from 'react'; import * as React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { FixedSizeList } from 'react-window'; import { List, type ListImperativeAPI } from 'react-window';
import { GrafanaTheme2, ThemeContext } from '@grafana/data'; import { GrafanaTheme2, ThemeContext } from '@grafana/data';
@@ -36,7 +36,7 @@ export interface State {
export class Typeahead extends PureComponent<Props, State> { export class Typeahead extends PureComponent<Props, State> {
static contextType = ThemeContext; static contextType = ThemeContext;
context!: React.ContextType<typeof ThemeContext>; context!: React.ContextType<typeof ThemeContext>;
listRef = createRef<FixedSizeList>(); listRef = createRef<ListImperativeAPI>();
state: State = { state: State = {
hoveredItem: null, hoveredItem: null,
@@ -81,10 +81,14 @@ export class Typeahead extends PureComponent<Props, State> {
this.listRef.current this.listRef.current
) { ) {
if (this.state.typeaheadIndex === 1) { if (this.state.typeaheadIndex === 1) {
this.listRef.current.scrollToItem(0); // special case for handling the first group label this.listRef.current.scrollToRow({
index: 0,
}); // special case for handling the first group label
return; return;
} }
this.listRef.current.scrollToItem(this.state.typeaheadIndex); this.listRef.current.scrollToRow({
index: this.state.typeaheadIndex,
});
} }
if (isEqual(prevProps.groupedItems, this.props.groupedItems) === false) { if (isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
@@ -167,22 +171,19 @@ export class Typeahead extends PureComponent<Props, State> {
return ( return (
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}> <Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
<ul role="menu" className={styles.typeahead} data-testid="typeahead"> <ul role="menu" className={styles.typeahead} data-testid="typeahead">
<FixedSizeList <List
ref={this.listRef} listRef={this.listRef}
itemCount={allItems.length} rowCount={allItems.length}
itemSize={itemHeight} rowHeight={itemHeight}
itemKey={(index) => { rowProps={{}}
const item = allItems && allItems[index]; style={{
const key = item ? `${index}-${item.label}` : `${index}`; width: listWidth,
return key; height: listHeight,
}} }}
width={listWidth} rowComponent={({ index, style }) => {
height={listHeight}
>
{({ index, style }) => {
const item = allItems && allItems[index]; const item = allItems && allItems[index];
if (!item) { if (!item) {
return null; return <></>;
} }
return ( return (
@@ -197,7 +198,7 @@ export class Typeahead extends PureComponent<Props, State> {
/> />
); );
}} }}
</FixedSizeList> />
</ul> </ul>
{showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />} {showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />}

View File

@@ -1,9 +1,9 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useCallback, useId, useMemo, useRef } from 'react'; import { useCallback, useId, useRef } from 'react';
import * as React from 'react'; import * as React from 'react';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import { FixedSizeList as List } from 'react-window'; import { List, type RowComponentProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader'; import { useInfiniteLoader } from 'react-window-infinite-loader';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n'; import { Trans } from '@grafana/i18n';
@@ -47,32 +47,8 @@ export function NestedFolderList({
requestLoadMore, requestLoadMore,
emptyFolders, emptyFolders,
}: NestedFolderListProps) { }: NestedFolderListProps) {
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const virtualData = useMemo(
(): VirtualData => ({
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
}),
[
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
]
);
const handleIsItemLoaded = useCallback( const handleIsItemLoaded = useCallback(
(itemIndex: number) => { (itemIndex: number) => {
return isItemLoaded(itemIndex); return isItemLoaded(itemIndex);
@@ -81,36 +57,42 @@ export function NestedFolderList({
); );
const handleLoadMore = useCallback( const handleLoadMore = useCallback(
(startIndex: number, endIndex: number) => { async (startIndex: number, endIndex: number) => {
const { parentUID } = items[startIndex]; const { parentUID } = items[startIndex];
requestLoadMore(parentUID); requestLoadMore(parentUID);
}, },
[requestLoadMore, items] [requestLoadMore, items]
); );
const onRowsRendered = useInfiniteLoader({
rowCount: items.length,
isRowLoaded: handleIsItemLoaded,
loadMoreRows: handleLoadMore,
});
return ( return (
<div className={styles.table} role="tree"> <div className={styles.table} role="tree">
{items.length > 0 ? ( {items.length > 0 ? (
<InfiniteLoader <List
ref={infiniteLoaderRef} onRowsRendered={onRowsRendered}
itemCount={items.length} rowProps={{
isItemLoaded={handleIsItemLoaded} items,
loadMoreItems={handleLoadMore} focusedItemIndex,
> foldersAreOpenable,
{({ onItemsRendered, ref }) => ( selectedFolder,
<List onFolderExpand,
ref={ref} onFolderSelect,
height={ROW_HEIGHT * Math.min(6.5, items.length)} idPrefix,
width="100%" emptyFolders,
itemData={virtualData} }}
itemSize={ROW_HEIGHT} rowComponent={Row}
itemCount={items.length} rowCount={items.length}
onItemsRendered={onItemsRendered} rowHeight={ROW_HEIGHT}
> style={{
{Row} height: ROW_HEIGHT * Math.min(6.5, items.length),
</List> width: '100%',
)} }}
</InfiniteLoader> />
) : ( ) : (
<div className={styles.emptyMessage}> <div className={styles.emptyMessage}>
<Trans i18nKey="browse-dashboards.folder-picker.empty-message">No folders found</Trans> <Trans i18nKey="browse-dashboards.folder-picker.empty-message">No folders found</Trans>
@@ -122,25 +104,20 @@ export function NestedFolderList({
interface VirtualData extends Omit<NestedFolderListProps, 'isItemLoaded' | 'requestLoadMore'> {} interface VirtualData extends Omit<NestedFolderListProps, 'isItemLoaded' | 'requestLoadMore'> {}
interface RowProps {
index: number;
style: React.CSSProperties;
data: VirtualData;
}
const SKELETON_WIDTHS = [100, 200, 130, 160, 150]; const SKELETON_WIDTHS = [100, 200, 130, 160, 150];
function Row({ index, style: virtualStyles, data }: RowProps) { function Row({
const { index,
items, style: virtualStyles,
focusedItemIndex, items,
foldersAreOpenable, focusedItemIndex,
selectedFolder, foldersAreOpenable,
onFolderExpand, selectedFolder,
onFolderSelect, onFolderExpand,
idPrefix, onFolderSelect,
emptyFolders, idPrefix,
} = data; emptyFolders,
}: RowComponentProps<VirtualData>) {
const { item, isOpen, level, parentUID } = items[index]; const { item, isOpen, level, parentUID } = items[index];
const rowRef = useRef<HTMLDivElement>(null); const rowRef = useRef<HTMLDivElement>(null);
const labelId = useId(); const labelId = useId();
@@ -190,7 +167,9 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
Non-folder {{ itemKind }} {{ itemUID }} Non-folder {{ itemKind }} {{ itemUID }}
</Trans> </Trans>
</span> </span>
) : null; ) : (
<></>
);
} }
// We don't have a direct value of whether things are coming from user searching but this seems to be a good // We don't have a direct value of whether things are coming from user searching but this seems to be a good

View File

@@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { CSSProperties, useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window'; import { List, type RowComponentProps } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
@@ -95,10 +95,7 @@ export function AlertInstanceModalSelector({
const filteredRulesKeys = Object.keys(filteredRules || []); const filteredRulesKeys = Object.keys(filteredRules || []);
const RuleRow = ({ index, style }: { index: number; style?: CSSProperties }) => { const RuleRow = ({ index, style }: RowComponentProps) => {
if (!filteredRules) {
return null;
}
const ruleName = filteredRulesKeys[index]; const ruleName = filteredRulesKeys[index];
const isSelected = ruleName === selectedRule; const isSelected = ruleName === selectedRule;
@@ -133,7 +130,7 @@ export function AlertInstanceModalSelector({
return tags; return tags;
}; };
const InstanceRow = ({ index, style }: { index: number; style: CSSProperties }) => { const InstanceRow = ({ index, style }: RowComponentProps) => {
const alerts = useMemo(() => (selectedRule ? rulesWithInstances[selectedRule] : []), []); const alerts = useMemo(() => (selectedRule ? rulesWithInstances[selectedRule] : []), []);
const alert = alerts[index]; const alert = alerts[index];
const isSelected = selectedInstances?.includes(alert); const isSelected = selectedInstances?.includes(alert);
@@ -236,9 +233,16 @@ export function AlertInstanceModalSelector({
{!loading && ( {!loading && (
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<FixedSizeList itemSize={50} height={height} width={width} itemCount={filteredRulesKeys.length}> <List
{RuleRow} rowComponent={RuleRow}
</FixedSizeList> rowCount={filteredRulesKeys.length}
rowHeight={50}
rowProps={{}}
style={{
height,
width,
}}
/>
)} )}
</AutoSizer> </AutoSizer>
)} )}
@@ -264,14 +268,16 @@ export function AlertInstanceModalSelector({
{selectedRule && rulesWithInstances[selectedRule].length && !loading && ( {selectedRule && rulesWithInstances[selectedRule].length && !loading && (
<AutoSizer> <AutoSizer>
{({ width, height }) => ( {({ width, height }) => (
<FixedSizeList <List
itemSize={32} rowComponent={InstanceRow}
height={height} rowCount={rulesWithInstances[selectedRule].length || 0}
width={width} rowHeight={32}
itemCount={rulesWithInstances[selectedRule].length || 0} rowProps={{}}
> style={{
{InstanceRow} height,
</FixedSizeList> width,
}}
/>
)} )}
</AutoSizer> </AutoSizer>
)} )}

View File

@@ -1,9 +1,9 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { CSSProperties, useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useAsync, useDebounce } from 'react-use'; import { useAsync, useDebounce } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window'; import { List, type ListImperativeAPI, type RowComponentProps } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
@@ -104,11 +104,13 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
const selectedDashboardIsInPageResult = selectedDashboardIndex >= 0; const selectedDashboardIsInPageResult = selectedDashboardIndex >= 0;
const scrollToItem = useCallback( const scrollToItem = useCallback(
(node: FixedSizeList) => { (list: ListImperativeAPI) => {
const canScroll = selectedDashboardIndex >= 0; const canScroll = selectedDashboardIndex >= 0;
if (isDefaultSelection && canScroll) { if (isDefaultSelection && canScroll) {
node?.scrollToItem(selectedDashboardIndex, 'smart'); list?.scrollToRow({
index: selectedDashboardIndex,
});
} }
}, },
[isDefaultSelection, selectedDashboardIndex] [isDefaultSelection, selectedDashboardIndex]
@@ -122,7 +124,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
[dashboardFilter] [dashboardFilter]
); );
const DashboardRow = ({ index, style }: { index: number; style?: CSSProperties }) => { const DashboardRow = ({ index, style }: RowComponentProps) => {
const dashboard = filteredDashboards[index]; const dashboard = filteredDashboards[index];
const isSelected = selectedDashboardUid === dashboard.uid; const isSelected = selectedDashboardUid === dashboard.uid;
const folderTitle = locationInfo?.[dashboard.location]?.name ?? 'Dashboards'; const folderTitle = locationInfo?.[dashboard.location]?.name ?? 'Dashboards';
@@ -143,7 +145,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
); );
}; };
const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => { const PanelRow = ({ index, style }: RowComponentProps) => {
const panel = filteredPanels[index]; const panel = filteredPanels[index];
const panelTitle = panel.title || '<No title>'; const panelTitle = panel.title || '<No title>';
const isSelected = Boolean(panel.id) && selectedPanelId === panel.id; const isSelected = Boolean(panel.id) && selectedPanelId === panel.id;
@@ -258,15 +260,18 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
{!isDashSearchFetching && ( {!isDashSearchFetching && (
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<FixedSizeList <List
ref={scrollToItem} listRef={scrollToItem}
itemSize={50} rowHeight={50}
height={height} rowCount={filteredDashboards.length}
width={width} rowComponent={DashboardRow}
itemCount={filteredDashboards.length} rowProps={{}}
> style={{
{DashboardRow} height,
</FixedSizeList> maxHeight: height,
width,
}}
/>
)} )}
</AutoSizer> </AutoSizer>
)} )}
@@ -292,9 +297,17 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
{selectedDashboardUid && !isDashboardFetching && ( {selectedDashboardUid && !isDashboardFetching && (
<AutoSizer> <AutoSizer>
{({ width, height }) => ( {({ width, height }) => (
<FixedSizeList itemSize={32} height={height} width={width} itemCount={filteredPanels.length}> <List
{PanelRow} rowHeight={32}
</FixedSizeList> rowCount={filteredPanels.length}
rowComponent={PanelRow}
rowProps={{}}
style={{
height,
maxHeight: height,
width,
}}
/>
)} )}
</AutoSizer> </AutoSizer>
)} )}

View File

@@ -1,9 +1,8 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useCallback, useEffect, useId, useMemo, useRef } from 'react'; import { useCallback, useId, useMemo } from 'react';
import * as React from 'react';
import { TableInstance, useTable } from 'react-table'; import { TableInstance, useTable } from 'react-table';
import { VariableSizeList as List } from 'react-window'; import { List, type RowComponentProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader'; import { useInfiniteLoader } from 'react-window-infinite-loader';
import { GrafanaTheme2, isTruthy } from '@grafana/data'; import { GrafanaTheme2, isTruthy } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@@ -45,6 +44,15 @@ const HEADER_HEIGHT = 36;
const ROW_HEIGHT = 36; const ROW_HEIGHT = 36;
const DIVIDER_HEIGHT = 0; // Yes - make it appear as a border on the row rather than a row itself const DIVIDER_HEIGHT = 0; // Yes - make it appear as a border on the row rather than a row itself
function getRowHeight(rowIndex: number, { items }: VirtualListRowProps) {
const row = items[rowIndex];
if (row.item.kind === 'ui' && row.item.uiKind === 'divider') {
return DIVIDER_HEIGHT;
}
return ROW_HEIGHT;
}
export function DashboardsTree({ export function DashboardsTree({
items, items,
width, width,
@@ -59,23 +67,21 @@ export function DashboardsTree({
permissions, permissions,
}: DashboardsTreeProps) { }: DashboardsTreeProps) {
const treeID = useId(); const treeID = useId();
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const listRef = useRef<List | null>(null);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
useEffect(() => { // TODO verify if we need this with v2
// If the tree changed identity, then some indexes that were previously loaded may now be unloaded, // useEffect(() => {
// especially after a refetch after a move/delete. // // If the tree changed identity, then some indexes that were previously loaded may now be unloaded,
// Clear that cache, and check if we need to trigger another load // // especially after a refetch after a move/delete.
if (infiniteLoaderRef.current) { // // Clear that cache, and check if we need to trigger another load
infiniteLoaderRef.current.resetloadMoreItemsCache(true); // if (infiniteLoaderRef.current) {
} // infiniteLoaderRef.current.resetloadMoreItemsCache(true);
// }
if (listRef.current) { // if (listRef.current) {
listRef.current.resetAfterIndex(0); // listRef.current.resetAfterIndex(0);
} // }
}, [items]); // }, [items]);
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
const checkboxColumn: DashboardsTreeColumn = { const checkboxColumn: DashboardsTreeColumn = {
@@ -111,20 +117,6 @@ export function DashboardsTree({
const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout); const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout);
const { getTableProps, getTableBodyProps, headerGroups } = table; const { getTableProps, getTableBodyProps, headerGroups } = table;
const virtualData = useMemo(
() => ({
table,
isSelected,
onAllSelectionChange,
onItemSelectionChange,
treeID,
permissions,
}),
// we need this to rerender if items changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID, permissions]
);
const handleIsItemLoaded = useCallback( const handleIsItemLoaded = useCallback(
(itemIndex: number) => { (itemIndex: number) => {
return isItemLoaded(itemIndex); return isItemLoaded(itemIndex);
@@ -133,24 +125,18 @@ export function DashboardsTree({
); );
const handleLoadMore = useCallback( const handleLoadMore = useCallback(
(startIndex: number, endIndex: number) => { async (startIndex: number, endIndex: number) => {
const { parentUID } = items[startIndex]; const { parentUID } = items[startIndex];
requestLoadMore(parentUID); requestLoadMore(parentUID);
}, },
[requestLoadMore, items] [requestLoadMore, items]
); );
const getRowHeight = useCallback( const onRowsRendered = useInfiniteLoader({
(rowIndex: number) => { rowCount: items.length,
const row = items[rowIndex]; isRowLoaded: handleIsItemLoaded,
if (row.item.kind === 'ui' && row.item.uiKind === 'divider') { loadMoreRows: handleLoadMore,
return DIVIDER_HEIGHT; });
}
return ROW_HEIGHT;
},
[items]
);
return ( return (
<div {...getTableProps()} role="table"> <div {...getTableProps()} role="table">
@@ -175,51 +161,50 @@ export function DashboardsTree({
})} })}
<div {...getTableBodyProps()} data-testid={selectors.pages.BrowseDashboards.table.body}> <div {...getTableBodyProps()} data-testid={selectors.pages.BrowseDashboards.table.body}>
<InfiniteLoader <List
ref={infiniteLoaderRef} rowComponent={VirtualListRow}
itemCount={items.length} rowCount={items.length}
isItemLoaded={handleIsItemLoaded} rowHeight={getRowHeight}
loadMoreItems={handleLoadMore} rowProps={{
> table,
{({ onItemsRendered, ref }) => ( isSelected,
<List onAllSelectionChange,
ref={(elem) => { onItemSelectionChange,
ref(elem); treeID,
listRef.current = elem; permissions,
}} items,
height={height - HEADER_HEIGHT} }}
width={width} onRowsRendered={onRowsRendered}
itemCount={items.length} style={{
itemData={virtualData} height: height - HEADER_HEIGHT,
estimatedItemSize={ROW_HEIGHT} width,
itemSize={getRowHeight} }}
onItemsRendered={onItemsRendered} />
>
{VirtualListRow}
</List>
)}
</InfiniteLoader>
</div> </div>
</div> </div>
); );
} }
interface VirtualListRowProps { interface VirtualListRowProps {
index: number; items: DashboardsTreeItem[];
style: React.CSSProperties; table: TableInstance<DashboardsTreeItem>;
data: { isSelected: DashboardsTreeCellProps['isSelected'];
table: TableInstance<DashboardsTreeItem>; onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];
isSelected: DashboardsTreeCellProps['isSelected']; onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];
onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange']; treeID: string;
onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange']; permissions: BrowseDashboardsPermissions;
treeID: string;
permissions: BrowseDashboardsPermissions;
};
} }
function VirtualListRow({ index, style, data }: VirtualListRowProps) { function VirtualListRow({
index,
style,
table,
isSelected,
onItemSelectionChange,
treeID,
permissions,
}: RowComponentProps<VirtualListRowProps>) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { table, isSelected, onItemSelectionChange, treeID, permissions } = data;
const { rows, prepareRow } = table; const { rows, prepareRow } = table;
const row = rows[index]; const row = rows[index];

View File

@@ -1,8 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { memo, CSSProperties } from 'react'; import { type KeyboardEvent } from 'react';
import * as React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { areEqual, FixedSizeGrid as Grid } from 'react-window'; import { type CellComponentProps, Grid } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
@@ -11,20 +10,14 @@ import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { ResourceItem } from './FolderPickerTab'; import { ResourceItem } from './FolderPickerTab';
interface CellProps { interface CellProps {
columnIndex: number; cards: ResourceItem[];
rowIndex: number; columnCount: number;
style: CSSProperties; onChange: (value: string) => void;
data: { selected?: string;
cards: ResourceItem[];
columnCount: number;
onChange: (value: string) => void;
selected?: string;
};
} }
const MemoizedCell = memo(function Cell(props: CellProps) { function Cell(props: CellComponentProps<CellProps>) {
const { columnIndex, rowIndex, style, data } = props; const { columnIndex, rowIndex, style, cards, columnCount, onChange, selected } = props;
const { cards, columnCount, onChange, selected } = data;
const singleColumnIndex = columnIndex + rowIndex * columnCount; const singleColumnIndex = columnIndex + rowIndex * columnCount;
const card = cards[singleColumnIndex]; const card = cards[singleColumnIndex];
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -36,7 +29,7 @@ const MemoizedCell = memo(function Cell(props: CellProps) {
key={card.value} key={card.value}
className={selected === card.value ? cx(styles.card, styles.selected) : styles.card} className={selected === card.value ? cx(styles.card, styles.selected) : styles.card}
onClick={() => onChange(card.value)} onClick={() => onChange(card.value)}
onKeyDown={(e: React.KeyboardEvent) => { onKeyDown={(e: KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
onChange(card.value); onChange(card.value);
} }
@@ -54,7 +47,7 @@ const MemoizedCell = memo(function Cell(props: CellProps) {
)} )}
</div> </div>
); );
}, areEqual); }
interface CardProps { interface CardProps {
onChange: (value: string) => void; onChange: (value: string) => void;
@@ -75,17 +68,20 @@ export const ResourceCards = (props: CardProps) => {
const rowCount = Math.ceil(cards.length / columnCount); const rowCount = Math.ceil(cards.length / columnCount);
return ( return (
<Grid <Grid
width={width} style={{
height={height} height,
maxHeight: height,
maxWidth: width,
width,
}}
columnCount={columnCount} columnCount={columnCount}
columnWidth={cardWidth} columnWidth={cardWidth}
rowCount={rowCount} rowCount={rowCount}
rowHeight={cardHeight} rowHeight={cardHeight}
itemData={{ cards, columnCount, onChange, selected: value }} cellProps={{ cards, columnCount, onChange, selected: value }}
className={styles.grid} className={styles.grid}
> cellComponent={Cell}
{MemoizedCell} />
</Grid>
); );
}} }}
</AutoSizer> </AutoSizer>

View File

@@ -1,8 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { useEffect, useId, useRef, useState } from 'react'; import { useId, useState } from 'react';
import { useWindowSize } from 'react-use'; import { useWindowSize } from 'react-use';
import { VariableSizeList as List } from 'react-window'; import { List } from 'react-window';
import { DataFrame, Field as DataFrameField } from '@grafana/data'; import { DataFrame, Field as DataFrameField } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
@@ -59,6 +59,24 @@ const styles = {
const mobileWidthThreshold = 480; const mobileWidthThreshold = 480;
const numberOfColumnsBeforeExpandedViewIsDefault = 2; const numberOfColumnsBeforeExpandedViewIsDefault = 2;
interface RowProps {
isExpandedView: boolean;
valueLabels: DataFrameField[];
items: instantQueryRawVirtualizedListData[];
}
function getListItemHeight(itemIndex: number, { isExpandedView, items, valueLabels }: RowProps) {
const singleLineHeight = 32;
const additionalLineHeight = 22;
if (!isExpandedView) {
return singleLineHeight;
}
const item = items[itemIndex];
// Height of 1.5 lines, plus the number of non-value attributes times the height of additional lines
return 1.5 * singleLineHeight + (Object.keys(item).length - valueLabels.length) * additionalLineHeight;
}
/** /**
* The container that provides the virtualized list to the child components * The container that provides the virtualized list to the child components
* @param props * @param props
@@ -67,7 +85,6 @@ const numberOfColumnsBeforeExpandedViewIsDefault = 2;
const RawListContainer = (props: RawListContainerProps) => { const RawListContainer = (props: RawListContainerProps) => {
const { tableResult } = props; const { tableResult } = props;
const dataFrame = cloneDeep(tableResult); const dataFrame = cloneDeep(tableResult);
const listRef = useRef<List | null>(null);
const valueLabels = dataFrame.fields.filter((field) => field.name.includes('Value')); const valueLabels = dataFrame.fields.filter((field) => field.name.includes('Value'));
const items = getRawPrometheusListItemsFromDataFrame(dataFrame); const items = getRawPrometheusListItemsFromDataFrame(dataFrame);
@@ -84,11 +101,6 @@ const RawListContainer = (props: RawListContainerProps) => {
reportInteraction('grafana_explore_prometheus_instant_query_ui_raw_toggle_expand', props); reportInteraction('grafana_explore_prometheus_instant_query_ui_raw_toggle_expand', props);
}; };
useEffect(() => {
// After the expanded view has updated, tell the list to re-render
listRef.current?.resetAfterIndex(0, true);
}, [isExpandedView]);
const calculateInitialHeight = (length: number): number => { const calculateInitialHeight = (length: number): number => {
const maxListHeight = 600; const maxListHeight = 600;
const shortListLength = 10; const shortListLength = 10;
@@ -96,7 +108,11 @@ const RawListContainer = (props: RawListContainerProps) => {
if (length < shortListLength) { if (length < shortListLength) {
let sum = 0; let sum = 0;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
sum += getListItemHeight(i, true); sum += getListItemHeight(i, {
isExpandedView: true,
items,
valueLabels,
});
} }
return Math.min(maxListHeight, sum); return Math.min(maxListHeight, sum);
@@ -105,18 +121,6 @@ const RawListContainer = (props: RawListContainerProps) => {
return maxListHeight; return maxListHeight;
}; };
const getListItemHeight = (itemIndex: number, isExpandedView: boolean) => {
const singleLineHeight = 32;
const additionalLineHeight = 22;
if (!isExpandedView) {
return singleLineHeight;
}
const item = items[itemIndex];
// Height of 1.5 lines, plus the number of non-value attributes times the height of additional lines
return 1.5 * singleLineHeight + (Object.keys(item).length - valueLabels.length) * additionalLineHeight;
};
const switchId = `isExpandedView ${useId()}`; const switchId = `isExpandedView ${useId()}`;
return ( return (
@@ -153,14 +157,19 @@ const RawListContainer = (props: RawListContainerProps) => {
<ItemLabels valueLabels={valueLabels} expanded={isExpandedView} /> <ItemLabels valueLabels={valueLabels} expanded={isExpandedView} />
)} )}
<List <List
ref={listRef}
itemCount={items.length}
className={styles.wrapper} className={styles.wrapper}
itemSize={(index) => getListItemHeight(index, isExpandedView)} style={{
height={calculateInitialHeight(items.length)} height: calculateInitialHeight(items.length),
width="100%" width: '100%',
> }}
{({ index, style }) => { rowCount={items.length}
rowHeight={getListItemHeight}
rowProps={{
isExpandedView,
valueLabels,
items,
}}
rowComponent={({ index, style }) => {
let filteredValueLabels: DataFrameField[] | undefined; let filteredValueLabels: DataFrameField[] | undefined;
if (isExpandedView) { if (isExpandedView) {
filteredValueLabels = valueLabels.filter((valueLabel) => { filteredValueLabels = valueLabels.filter((valueLabel) => {
@@ -181,7 +190,7 @@ const RawListContainer = (props: RawListContainerProps) => {
</div> </div>
); );
}} }}
</List> />
</> </>
} }
</div> </div>

View File

@@ -1,5 +1,5 @@
import { act, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import { VariableSizeList } from 'react-window'; import { List } from 'react-window';
import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data'; import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data';
import { LogsSortOrder } from '@grafana/schema'; import { LogsSortOrder } from '@grafana/schema';
@@ -89,19 +89,19 @@ function setup(
loadMore={loadMoreMock} loadMore={loadMoreMock}
infiniteScrollMode={infiniteScrollMode} infiniteScrollMode={infiniteScrollMode}
> >
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( {({ itemCount, onItemsRendered, Renderer }) => (
<VariableSizeList <List
height={100} rowComponent={Renderer}
itemCount={itemCount} rowCount={itemCount}
itemSize={() => virtualization.getLineHeight()} rowProps={{}}
itemKey={getItemKey} rowHeight={() => virtualization.getLineHeight()}
layout="vertical" onRowsRendered={onItemsRendered}
onItemsRendered={onItemsRendered} style={{
style={{ overflow: 'scroll' }} overflow: 'scroll',
width="100%" height: 100,
> width: '100%',
{Renderer} }}
</VariableSizeList> />
)} )}
</InfiniteScroll> </InfiniteScroll>
); );

View File

@@ -1,6 +1,6 @@
import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent } from 'react'; import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent, type JSX } from 'react';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window'; import { type RowComponentProps, type ListProps } from 'react-window';
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
import { t } from '@grafana/i18n'; import { t } from '@grafana/i18n';
@@ -17,8 +17,8 @@ import { LogLineVirtualization } from './virtualization';
interface ChildrenProps { interface ChildrenProps {
itemCount: number; itemCount: number;
getItemKey: (index: number) => string; getItemKey: (index: number) => string;
onItemsRendered: (props: ListOnItemsRenderedProps) => void; onItemsRendered: ListProps<{}>['onRowsRendered'];
Renderer: (props: ListChildComponentProps) => ReactNode; Renderer: (props: RowComponentProps) => JSX.Element;
} }
export interface Props { export interface Props {
@@ -200,7 +200,7 @@ export const InfiniteScroll = ({
}, [onLoadMore]); }, [onLoadMore]);
const Renderer = useCallback( const Renderer = useCallback(
({ index, style }: ListChildComponentProps) => { ({ index, style }: RowComponentProps) => {
if (!logs[index] && infiniteLoaderState !== 'idle') { if (!logs[index] && infiniteLoaderState !== 'idle') {
return ( return (
<LogLineMessage <LogLineMessage
@@ -248,12 +248,12 @@ export const InfiniteScroll = ({
] ]
); );
const onItemsRendered = useCallback( const onItemsRendered = useCallback<NonNullable<ListProps<{}>['onRowsRendered']>>(
(props: ListOnItemsRenderedProps) => { (props) => {
if (!scrollElement) { if (!scrollElement) {
return; return;
} }
if (props.visibleStartIndex === 0) { if (props.startIndex === 0) {
noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight; noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight;
} }
if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') { if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
@@ -261,9 +261,9 @@ export const InfiniteScroll = ({
} }
const lastLogIndex = logs.length - 1; const lastLogIndex = logs.length - 1;
const preScrollIndex = logs.length - 2; const preScrollIndex = logs.length - 2;
if (props.visibleStopIndex >= lastLogIndex) { if (props.stopIndex >= lastLogIndex) {
setInfiniteLoaderState('pre-scroll-bottom'); setInfiniteLoaderState('pre-scroll-bottom');
} else if (props.visibleStartIndex < preScrollIndex) { } else if (props.startIndex < preScrollIndex) {
setInfiniteLoaderState('idle'); setInfiniteLoaderState('idle');
} }
}, },

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { Grammar } from 'prismjs'; import { Grammar } from 'prismjs';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react';
import { Align, VariableSizeList } from 'react-window'; import { Align, List, ListImperativeAPI, useListRef } from 'react-window';
import { import {
CoreApp, CoreApp,
@@ -282,10 +282,9 @@ const LogListComponent = ({
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]); const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app)); const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
const theme = useTheme2(); const theme = useTheme2();
const listRef = useRef<VariableSizeList | null>(null); const listRef = useListRef(null);
const widthRef = useRef(containerElement.clientWidth); const widthRef = useRef(containerElement.clientWidth);
const wrapperRef = useRef<HTMLDivElement | null>(null); const wrapperRef = useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]); const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]);
const dimensions = useMemo( const dimensions = useMemo(
() => () =>
@@ -329,25 +328,29 @@ const LogListComponent = ({
// When log lines report size discrepancies, we debounce the calculation reset to give time to // When log lines report size discrepancies, we debounce the calculation reset to give time to
// use the smallest log index to reset the heights. // use the smallest log index to reset the heights.
const debouncedResetAfterIndex = useMemo(() => { // TODO do we need this?
return debounce((index: number) => { // const debouncedResetAfterIndex = useMemo(() => {
listRef.current?.resetAfterIndex(index); // return debounce((index: number) => {
overflowIndexRef.current = Infinity; // listRef.current?.resetAfterIndex(index);
}, 0); // overflowIndexRef.current = Infinity;
}, []); // }, 0);
// }, []);
const debouncedScrollToItem = useMemo(() => { const debouncedScrollToItem = useMemo(() => {
return debounce((index: number, align?: Align) => { return debounce((index: number, align?: Align) => {
listRef.current?.scrollToItem(index, align); listRef.current?.scrollToRow({
index,
align,
});
}, 250); }, 250);
}, []); }, [listRef]);
useEffect(() => { useEffect(() => {
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) => const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
handleScrollToEvent(e, filteredLogs, listRef.current) handleScrollToEvent(e, filteredLogs, listRef.current)
); );
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, [eventBus, filteredLogs]); }, [eventBus, filteredLogs, listRef]);
useEffect(() => { useEffect(() => {
setProcessedLogs( setProcessedLogs(
@@ -366,11 +369,13 @@ const LogListComponent = ({
) )
); );
virtualization.resetLogLineSizes(); virtualization.resetLogLineSizes();
listRef.current?.resetAfterIndex(0); // TODO do we need this?
// listRef.current?.resetAfterIndex(0);
}, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]); }, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]);
useEffect(() => { useEffect(() => {
listRef.current?.resetAfterIndex(0); // TODO do we need this?
// listRef.current?.resetAfterIndex(0);
}, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]); }, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]);
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -399,9 +404,10 @@ const LogListComponent = ({
return; return;
} }
overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current; overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current;
debouncedResetAfterIndex(overflowIndexRef.current); // TODO do we need this?
// debouncedResetAfterIndex(overflowIndexRef.current);
}, },
[debouncedResetAfterIndex, virtualization, widthContainer] [virtualization, widthContainer]
); );
const handleScrollPosition = useCallback( const handleScrollPosition = useCallback(
@@ -410,13 +416,13 @@ const LogListComponent = ({
if (scrollToUID) { if (scrollToUID) {
const index = processedLogs.findIndex((log) => log.uid === scrollToUID); const index = processedLogs.findIndex((log) => log.uid === scrollToUID);
if (index >= 0) { if (index >= 0) {
listRef.current?.scrollToItem(index, 'start'); listRef.current?.scrollToRow({ index, align: 'start' });
return; return;
} }
} }
listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : processedLogs.length - 1); listRef.current?.scrollToRow({ index: initialScrollPosition === 'top' ? 0 : processedLogs.length - 1 });
}, },
[initialScrollPosition, permalinkedLogId, processedLogs] [initialScrollPosition, permalinkedLogId, processedLogs, listRef]
); );
const handleLogLineClick = useCallback( const handleLogLineClick = useCallback(
@@ -503,7 +509,7 @@ const LogListComponent = ({
logs={filteredLogs} logs={filteredLogs}
loadMore={loadMore} loadMore={loadMore}
onClick={handleLogLineClick} onClick={handleLogLineClick}
scrollElement={scrollRef.current} scrollElement={listRef.current?.element ?? null}
showTime={showTime} showTime={showTime}
sortOrder={sortOrder} sortOrder={sortOrder}
timeRange={timeRange} timeRange={timeRange}
@@ -512,12 +518,25 @@ const LogListComponent = ({
virtualization={virtualization} virtualization={virtualization}
wrapLogMessage={wrapLogMessage} wrapLogMessage={wrapLogMessage}
> >
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( {({ itemCount, onItemsRendered, Renderer }) => (
<VariableSizeList <List
className={styles.logList} className={styles.logList}
height={listHeight} style={
itemCount={itemCount} wrapLogMessage
itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, { ? {
height: listHeight,
width: '100%',
overflowY: 'scroll',
}
: {
height: listHeight,
width: '100%',
overflow: 'scroll',
}
}
rowProps={{}}
rowCount={itemCount}
rowHeight={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, {
detailsMode, detailsMode,
hasLogsWithErrors, hasLogsWithErrors,
hasSampledLogs, hasSampledLogs,
@@ -526,17 +545,11 @@ const LogListComponent = ({
showTime, showTime,
wrap: wrapLogMessage, wrap: wrapLogMessage,
})} })}
itemKey={getItemKey} rowComponent={Renderer}
layout="vertical" onRowsRendered={onItemsRendered}
onItemsRendered={onItemsRendered}
outerRef={scrollRef}
overscanCount={5} overscanCount={5}
ref={listRef} listRef={listRef}
style={wrapLogMessage ? { overflowY: 'scroll' } : { overflow: 'scroll' }} />
width="100%"
>
{Renderer}
</VariableSizeList>
)} )}
</InfiniteScroll> </InfiniteScroll>
</div> </div>
@@ -586,16 +599,23 @@ function getStyles(
}; };
} }
function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: VariableSizeList | null) { function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: ListImperativeAPI | null) {
if (event.payload.scrollTo === 'top') { if (event.payload.scrollTo === 'top') {
list?.scrollTo(0); list?.scrollToRow({
index: 0,
});
} else if (event.payload.scrollTo === 'bottom') { } else if (event.payload.scrollTo === 'bottom') {
list?.scrollToItem(logs.length - 1); list?.scrollToRow({
index: logs.length - 1,
});
} else { } else {
// uid // uid
const index = logs.findIndex((log) => log.uid === event.payload.scrollTo); const index = logs.findIndex((log) => log.uid === event.payload.scrollTo);
if (index >= 0) { if (index >= 0) {
list?.scrollToItem(index, 'center'); list?.scrollToRow({
index,
align: 'center',
});
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ChangeEvent, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ChangeEvent, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { VariableSizeList } from 'react-window'; import { type ListImperativeAPI } from 'react-window';
import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data'; import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data';
import { t } from '@grafana/i18n'; import { t } from '@grafana/i18n';
@@ -12,7 +12,7 @@ import { useLogListSearchContext } from './LogListSearchContext';
import { LogListModel } from './processing'; import { LogListModel } from './processing';
interface Props { interface Props {
listRef: VariableSizeList | null; listRef: ListImperativeAPI | null;
logs: LogListModel[]; logs: LogListModel[];
} }
@@ -61,7 +61,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
} }
const prev = currentResult > 0 ? currentResult - 1 : matches.length - 1; const prev = currentResult > 0 ? currentResult - 1 : matches.length - 1;
setCurrentResult(prev); setCurrentResult(prev);
listRef?.scrollToItem(logs.indexOf(matches[prev]), 'center'); listRef?.scrollToRow({
index: logs.indexOf(matches[prev]),
align: 'center',
});
}, [currentResult, listRef, logs, matches]); }, [currentResult, listRef, logs, matches]);
const nextResult = useCallback(() => { const nextResult = useCallback(() => {
@@ -70,7 +73,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
} }
const next = currentResult < matches.length - 1 ? currentResult + 1 : 0; const next = currentResult < matches.length - 1 ? currentResult + 1 : 0;
setCurrentResult(next); setCurrentResult(next);
listRef?.scrollToItem(logs.indexOf(matches[next]), 'center'); listRef?.scrollToRow({
index: logs.indexOf(matches[next]),
align: 'center',
});
}, [currentResult, listRef, logs, matches]); }, [currentResult, listRef, logs, matches]);
useEffect(() => { useEffect(() => {
@@ -80,7 +86,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
} }
if (!currentResult) { if (!currentResult) {
setCurrentResult(0); setCurrentResult(0);
listRef?.scrollToItem(logs.indexOf(matches[0]), 'center'); listRef?.scrollToRow({
index: logs.indexOf(matches[0]),
align: 'center',
});
} }
}, [currentResult, listRef, logs, matches]); }, [currentResult, listRef, logs, matches]);

View File

@@ -1,9 +1,9 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useEffect, useMemo, useRef, useCallback, useState, CSSProperties } from 'react'; import { useMemo, useCallback, useEffect } from 'react';
import * as React from 'react'; import * as React from 'react';
import { useTable, Column, TableOptions, Cell } from 'react-table'; import { useTable, Column, TableOptions, Cell } from 'react-table';
import { FixedSizeList } from 'react-window'; import { List, useListRef, type RowComponentProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader'; import { useInfiniteLoader } from 'react-window-infinite-loader';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Field, GrafanaTheme2 } from '@grafana/data'; import { Field, GrafanaTheme2 } from '@grafana/data';
@@ -57,8 +57,7 @@ export const SearchResultsTable = React.memo(
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const columnStyles = useStyles2(getColumnStyles); const columnStyles = useStyles2(getColumnStyles);
const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Sm); const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Sm);
const infiniteLoaderRef = useRef<InfiniteLoader>(null); const listRef = useListRef(null);
const [listEl, setListEl] = useState<FixedSizeList | null>(null);
const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response); const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);
const memoizedData = useMemo(() => { const memoizedData = useMemo(() => {
@@ -72,15 +71,13 @@ export const SearchResultsTable = React.memo(
return Array(response.totalRows).fill(0); return Array(response.totalRows).fill(0);
}, [response]); }, [response]);
// Scroll to the top and clear loader cache when the query results change // Scroll to the top when the query results change
useEffect(() => { useEffect(() => {
if (infiniteLoaderRef.current) { const list = listRef.current;
infiniteLoaderRef.current.resetloadMoreItemsCache(); list?.scrollToRow({
} index: 0,
if (listEl) { });
listEl.scrollTo(0); }, [listRef, memoizedData]);
}
}, [memoizedData, listEl]);
// React-table column definitions // React-table column definitions
const memoizedColumns = useMemo(() => { const memoizedColumns = useMemo(() => {
@@ -129,8 +126,14 @@ export const SearchResultsTable = React.memo(
[response, selection, selectionToggle] [response, selection, selectionToggle]
); );
const onRowsRendered = useInfiniteLoader({
rowCount: rows.length,
isRowLoaded: response.isItemLoaded,
loadMoreRows: handleLoadMore,
});
const RenderRow = useCallback( const RenderRow = useCallback(
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => { ({ index: rowIndex, style }: RowComponentProps) => {
const row = rows[rowIndex]; const row = rows[rowIndex];
prepareRow(row); prepareRow(row);
@@ -225,29 +228,19 @@ export const SearchResultsTable = React.memo(
})} })}
<div {...getTableBodyProps()}> <div {...getTableBodyProps()}>
<InfiniteLoader <List
ref={infiniteLoaderRef} rowProps={{}}
isItemLoaded={response.isItemLoaded} listRef={listRef}
itemCount={rows.length} rowComponent={RenderRow}
loadMoreItems={handleLoadMore} rowCount={rows.length}
> onRowsRendered={onRowsRendered}
{({ onItemsRendered, ref }) => ( rowHeight={tableStyles.rowHeight}
<FixedSizeList style={{
ref={(innerRef) => { height: height - ROW_HEIGHT,
ref(innerRef); width: width,
setListEl(innerRef); overflow: 'hidden auto',
}} }}
onItemsRendered={onItemsRendered} />
height={height - ROW_HEIGHT}
itemCount={rows.length}
itemSize={tableStyles.rowHeight}
width={width}
style={{ overflow: 'hidden auto' }}
>
{RenderRow}
</FixedSizeList>
)}
</InfiniteLoader>
</div> </div>
</div> </div>
); );

View File

@@ -16,7 +16,7 @@
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-select": "5.10.2", "react-select": "5.10.2",
"react-window": "1.8.11", "react-window": "2.2.3",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"stream-browserify": "3.0.0", "stream-browserify": "3.0.0",
"tslib": "2.8.1", "tslib": "2.8.1",
@@ -34,7 +34,6 @@
"@types/node": "24.10.1", "@types/node": "24.10.1",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"@types/react-window": "1.8.8",
"@types/uuid": "10.0.0", "@types/uuid": "10.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"ts-node": "10.9.2", "ts-node": "10.9.2",

View File

@@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList } from 'react-window'; import { List } from 'react-window';
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data'; import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
@@ -505,18 +505,19 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
onClick={this.onClickLabel} onClick={this.onClickLabel}
/> />
</div> </div>
<FixedSizeList <List
height={200} rowCount={label.values?.length || 0}
itemCount={label.values?.length || 0} rowHeight={28}
itemSize={28} rowProps={{}}
itemKey={(i) => label.values?.[i].name ?? i} style={{
width={200} height: 200,
width: 200,
}}
className={styles.valueList} className={styles.valueList}
> rowComponent={({ index, style }) => {
{({ index, style }) => {
const value = label.values?.[index]; const value = label.values?.[index];
if (!value) { if (!value) {
return null; return <></>;
} }
return ( return (
<div style={style}> <div style={style}>
@@ -531,7 +532,7 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
</div> </div>
); );
}} }}
</FixedSizeList> />
</div> </div>
))} ))}
</div> </div>

View File

@@ -1473,7 +1473,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": "@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7":
version: 7.28.4 version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4" resolution: "@babel/runtime@npm:7.28.4"
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
@@ -2654,7 +2654,6 @@ __metadata:
"@types/node": "npm:24.10.1" "@types/node": "npm:24.10.1"
"@types/react": "npm:18.3.18" "@types/react": "npm:18.3.18"
"@types/react-dom": "npm:18.3.5" "@types/react-dom": "npm:18.3.5"
"@types/react-window": "npm:1.8.8"
"@types/uuid": "npm:10.0.0" "@types/uuid": "npm:10.0.0"
jest: "npm:29.7.0" jest: "npm:29.7.0"
lodash: "npm:4.17.21" lodash: "npm:4.17.21"
@@ -2662,7 +2661,7 @@ __metadata:
react: "npm:18.3.1" react: "npm:18.3.1"
react-dom: "npm:18.3.1" react-dom: "npm:18.3.1"
react-select: "npm:5.10.2" react-select: "npm:5.10.2"
react-window: "npm:1.8.11" react-window: "npm:2.2.3"
rxjs: "npm:7.8.2" rxjs: "npm:7.8.2"
stream-browserify: "npm:3.0.0" stream-browserify: "npm:3.0.0"
ts-node: "npm:10.9.2" ts-node: "npm:10.9.2"
@@ -3522,7 +3521,6 @@ __metadata:
"@types/react": "npm:18.3.18" "@types/react": "npm:18.3.18"
"@types/react-dom": "npm:18.3.5" "@types/react-dom": "npm:18.3.5"
"@types/react-highlight-words": "npm:0.20.0" "@types/react-highlight-words": "npm:0.20.0"
"@types/react-window": "npm:1.8.8"
"@types/semver": "npm:7.7.1" "@types/semver": "npm:7.7.1"
"@types/uuid": "npm:10.0.0" "@types/uuid": "npm:10.0.0"
debounce-promise: "npm:3.1.2" debounce-promise: "npm:3.1.2"
@@ -3542,7 +3540,7 @@ __metadata:
react-highlight-words: "npm:0.21.0" react-highlight-words: "npm:0.21.0"
react-select-event: "npm:5.5.1" react-select-event: "npm:5.5.1"
react-use: "npm:17.6.0" react-use: "npm:17.6.0"
react-window: "npm:1.8.11" react-window: "npm:2.2.3"
rimraf: "npm:6.0.1" rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4" rollup: "npm:^4.22.4"
rollup-plugin-esbuild: "npm:6.2.1" rollup-plugin-esbuild: "npm:6.2.1"
@@ -3825,7 +3823,6 @@ __metadata:
"@types/react-highlight-words": "npm:0.20.0" "@types/react-highlight-words": "npm:0.20.0"
"@types/react-table": "npm:7.7.20" "@types/react-table": "npm:7.7.20"
"@types/react-transition-group": "npm:4.4.12" "@types/react-transition-group": "npm:4.4.12"
"@types/react-window": "npm:1.8.8"
"@types/slate": "npm:0.47.11" "@types/slate": "npm:0.47.11"
"@types/slate-plain-serializer": "npm:0.7.5" "@types/slate-plain-serializer": "npm:0.7.5"
"@types/slate-react": "npm:0.22.9" "@types/slate-react": "npm:0.22.9"
@@ -3885,7 +3882,7 @@ __metadata:
react-table: "npm:7.8.0" react-table: "npm:7.8.0"
react-transition-group: "npm:4.4.5" react-transition-group: "npm:4.4.5"
react-use: "npm:17.6.0" react-use: "npm:17.6.0"
react-window: "npm:1.8.11" react-window: "npm:2.2.3"
rimraf: "npm:6.0.1" rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4" rollup: "npm:^4.22.4"
rollup-plugin-copy: "npm:3.5.0" rollup-plugin-copy: "npm:3.5.0"
@@ -10727,25 +10724,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react-window-infinite-loader@npm:^1":
version: 1.0.9
resolution: "@types/react-window-infinite-loader@npm:1.0.9"
dependencies:
"@types/react": "npm:*"
"@types/react-window": "npm:*"
checksum: 10/9f2c27f24bfa726ceaef6612a4adbda745f3455c877193f68dfa48591274c670a6df4fa6870785cff5f948e289ceb9a247fb7cbf67e3cd555ab16d11866fd63f
languageName: node
linkType: hard
"@types/react-window@npm:*, @types/react-window@npm:1.8.8":
version: 1.8.8
resolution: "@types/react-window@npm:1.8.8"
dependencies:
"@types/react": "npm:*"
checksum: 10/79b70b7c33161efb14bf69115792843de8e038594136a8373cfbbcc4066c49fd611dd2d3592a9a81d19d21c075bf14e5e73a64f4d9ad32e45d4d5493f5f53918
languageName: node
linkType: hard
"@types/react@npm:*, @types/react@npm:18.3.18": "@types/react@npm:*, @types/react@npm:18.3.18":
version: 18.3.18 version: 18.3.18
resolution: "@types/react@npm:18.3.18" resolution: "@types/react@npm:18.3.18"
@@ -19319,8 +19297,6 @@ __metadata:
"@types/react-table": "npm:7.7.20" "@types/react-table": "npm:7.7.20"
"@types/react-transition-group": "npm:4.4.12" "@types/react-transition-group": "npm:4.4.12"
"@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/react-virtualized-auto-sizer": "npm:1.0.8"
"@types/react-window": "npm:1.8.8"
"@types/react-window-infinite-loader": "npm:^1"
"@types/redux-mock-store": "npm:1.5.0" "@types/redux-mock-store": "npm:1.5.0"
"@types/semver": "npm:7.7.1" "@types/semver": "npm:7.7.1"
"@types/slate": "npm:0.47.11" "@types/slate": "npm:0.47.11"
@@ -19500,8 +19476,8 @@ __metadata:
react-use: "npm:17.6.0" react-use: "npm:17.6.0"
react-virtual: "npm:2.10.4" react-virtual: "npm:2.10.4"
react-virtualized-auto-sizer: "npm:1.0.26" react-virtualized-auto-sizer: "npm:1.0.26"
react-window: "npm:1.8.11" react-window: "npm:2.2.3"
react-window-infinite-loader: "npm:1.0.10" react-window-infinite-loader: "npm:2.0.0"
reduce-reducers: "npm:^1.0.4" reduce-reducers: "npm:^1.0.4"
redux: "npm:5.0.1" redux: "npm:5.0.1"
redux-mock-store: "npm:1.5.5" redux-mock-store: "npm:1.5.5"
@@ -23894,13 +23870,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"memoize-one@npm:>=3.1.1 <6":
version: 5.2.1
resolution: "memoize-one@npm:5.2.1"
checksum: 10/b7141dc148b5c6fdd51e77ecf0421fd2581681eb8756e0b3dfbd4fe765b5e2b5a6bc90214bb6f19a96b6aed44de17eda3407142a7be9e24ccd0774bbd9874d1b
languageName: node
linkType: hard
"memoize-one@npm:^4.0.0": "memoize-one@npm:^4.0.0":
version: 4.0.3 version: 4.0.3
resolution: "memoize-one@npm:4.0.3" resolution: "memoize-one@npm:4.0.3"
@@ -28964,26 +28933,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-window-infinite-loader@npm:1.0.10": "react-window-infinite-loader@npm:2.0.0":
version: 1.0.10 version: 2.0.0
resolution: "react-window-infinite-loader@npm:1.0.10" resolution: "react-window-infinite-loader@npm:2.0.0"
peerDependencies: peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0
checksum: 10/4f4c097a2948f8da71d13199289d85c89720e41888ea50039d2b8e6d7cf160300e97f66421c53420d8cb95b3ece74940c07ebd23bddd7a705a679d7e2dc2957e checksum: 10/a67ba08dbdb557a46390bf1056ad01d09202fc76492d27818429ac8f9e893792c3476a584fadfc7ae3f2766e1986468658356105000d7b31034287012e7a89dc
languageName: node languageName: node
linkType: hard linkType: hard
"react-window@npm:1.8.11": "react-window@npm:2.2.3":
version: 1.8.11 version: 2.2.3
resolution: "react-window@npm:1.8.11" resolution: "react-window@npm:2.2.3"
dependencies:
"@babel/runtime": "npm:^7.0.0"
memoize-one: "npm:>=3.1.1 <6"
peerDependencies: peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0
checksum: 10/bdbac2b664c5a799443b97a32b2f60a00cc13cc14ca8a8b1e81e2dc7dd00d8d54f05743113972fe1a641b57ada5d874b59c3cbe7e8a07a88c6713a0fb65d60f6 checksum: 10/f676883c6cd16a0f56690b6d9c929a3da8b7d0ccf78dd5247f474b65ead38aae32a328fcd24c95956d1541383b774fca2b6decae65b6991697a90168222de86d
languageName: node languageName: node
linkType: hard linkType: hard