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": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
},
"@typescript-eslint/no-explicit-any": {
"count": 2
}

View File

@@ -151,8 +151,6 @@
"@types/react-table": "7.7.20",
"@types/react-transition-group": "4.4.12",
"@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/semver": "7.7.1",
"@types/slate": "0.47.11",
@@ -414,8 +412,8 @@
"react-use": "17.6.0",
"react-virtual": "2.10.4",
"react-virtualized-auto-sizer": "1.0.26",
"react-window": "1.8.11",
"react-window-infinite-loader": "1.0.10",
"react-window": "2.2.3",
"react-window-infinite-loader": "2.0.0",
"reduce-reducers": "^1.0.4",
"redux": "5.0.1",
"redux-thunk": "3.1.0",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { css, cx } from '@emotion/css';
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 { FixedSizeList as List } from 'react-window';
import { List, useListRef } from 'react-window';
import { SelectableValue, toIconName } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@@ -101,14 +101,13 @@ interface VirtualSelectMenuProps<T> {
export const VirtualizedSelectMenu = ({
children,
maxHeight,
innerRef: scrollRef,
options,
selectProps,
focusedOption,
}: VirtualSelectMenuProps<SelectableValue>) => {
const theme = useTheme2();
const styles = getSelectStyles(theme);
const listRef = useRef<List>(null);
const listRef = useListRef(null);
const { toggleAllOptions, components } = selectProps;
const optionComponent = components?.Option ?? SelectMenuOptions;
@@ -126,8 +125,10 @@ export const VirtualizedSelectMenu = ({
(option: SelectableValue<unknown>) => option.value === focusedOption?.value
);
useLayoutEffect(() => {
listRef.current?.scrollToItem(focusedIndex);
}, [focusedIndex]);
listRef.current?.scrollToRow({
index: focusedIndex,
});
}, [focusedIndex, listRef]);
if (!Array.isArray(children)) {
return null;
@@ -180,17 +181,20 @@ export const VirtualizedSelectMenu = ({
return (
<List
outerRef={scrollRef}
ref={listRef}
rowComponent={({ index, style }) => (
<div style={{ ...style, overflow: 'hidden' }}>{flattenedChildren[index]}</div>
)}
rowCount={flattenedChildren.length}
rowHeight={VIRTUAL_LIST_ITEM_HEIGHT}
rowProps={{}}
listRef={listRef}
className={styles.menu}
height={heightEstimate}
width={widthEstimate}
style={{
height: heightEstimate,
width: widthEstimate,
}}
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 { useCallback, useMemo } 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 { selectors } from '@grafana/e2e-selectors';
@@ -162,15 +162,16 @@ export const FilterList = ({ options, values, caseSensitive, onChange, searchFil
{items.length > 0 ? (
<>
<List
height={height}
itemCount={items.length}
itemSize={ITEM_HEIGHT}
itemData={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
width="100%"
rowComponent={ItemRenderer}
rowCount={items.length}
rowHeight={ITEM_HEIGHT}
rowProps={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
style={{
height,
width: '100%',
}}
className={styles.filterList}
>
{ItemRenderer}
</List>
/>
<div
className={styles.filterListRow}
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 {
data: {
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
items: SelectableValue[];
values: SelectableValue[];
className: string;
};
interface ItemRendererProps {
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
items: SelectableValue[];
values: SelectableValue[];
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 { value, label } = option;
const isChecked = values.find((s) => s.value === value) !== undefined;

View File

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

View File

@@ -1,8 +1,8 @@
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 { Cell, Row, TableState, HeaderGroup } from 'react-table';
import { VariableSizeList } from 'react-window';
import { List, useListRef } from 'react-window';
import { Subscription, debounceTime } from 'rxjs';
import {
@@ -18,7 +18,6 @@ import {
import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema';
import { useTheme2 } from '../../../themes/ThemeContext';
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
import { usePanelContext } from '../../PanelChrome';
import { TableCell } from '../Cells/TableCell';
import {
@@ -42,14 +41,10 @@ interface RowsListProps {
data: DataFrame;
rows: Row[];
enableSharedCrosshair: boolean;
headerHeight: number;
rowHeight: number;
itemCount: number;
pageIndex: number;
listHeight: number;
width: number;
cellHeight?: TableCellHeight;
listRef: React.RefObject<VariableSizeList>;
tableState: TableState;
tableStyles: TableStyles;
nestedDataField?: Field;
@@ -70,11 +65,8 @@ export const RowsList = (props: RowsListProps) => {
const {
data,
rows,
headerHeight,
footerPaginationEnabled,
rowHeight,
itemCount,
pageIndex,
tableState,
prepareRow,
onCellFilterAdded,
@@ -84,7 +76,6 @@ export const RowsList = (props: RowsListProps) => {
tableStyles,
nestedDataField,
listHeight,
listRef,
enableSharedCrosshair = false,
initialRowIndex = undefined,
headerGroups,
@@ -95,11 +86,25 @@ export const RowsList = (props: RowsListProps) => {
setInspectCell,
} = props;
const listRef = useListRef(null);
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
if (initialRowIndex === undefined && rowHighlightIndex !== 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 panelContext = usePanelContext();
@@ -234,15 +239,6 @@ export const RowsList = (props: RowsListProps) => {
};
}, [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(
(index: number) => {
return tableState.pageIndex * tableState.pageSize + index;
@@ -316,6 +312,17 @@ export const RowsList = (props: RowsListProps) => {
);
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 });
return (
@@ -414,36 +421,17 @@ export const RowsList = (props: RowsListProps) => {
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 (
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}>
<VariableSizeList
key={`${rowHeight}${pageIndex}`}
height={listHeight}
itemCount={itemCount}
itemSize={getItemSize}
width={'100%'}
ref={listRef}
style={{ overflow: undefined }}
>
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
</VariableSizeList>
</CustomScrollbar>
<List
rowProps={{}}
rowHeight={getItemSize}
rowCount={itemCount}
listRef={listRef}
style={{
height: listHeight,
width,
}}
rowComponent={({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
/>
);
};

View File

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

View File

@@ -111,6 +111,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
width: '100%',
overflow: 'auto',
display: 'flex',
position: 'relative',
flexDirection: 'column',
}),
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 * as React from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeList } from 'react-window';
import { List, type ListImperativeAPI } from 'react-window';
import { GrafanaTheme2, ThemeContext } from '@grafana/data';
@@ -36,7 +36,7 @@ export interface State {
export class Typeahead extends PureComponent<Props, State> {
static contextType = ThemeContext;
context!: React.ContextType<typeof ThemeContext>;
listRef = createRef<FixedSizeList>();
listRef = createRef<ListImperativeAPI>();
state: State = {
hoveredItem: null,
@@ -81,10 +81,14 @@ export class Typeahead extends PureComponent<Props, State> {
this.listRef.current
) {
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;
}
this.listRef.current.scrollToItem(this.state.typeaheadIndex);
this.listRef.current.scrollToRow({
index: this.state.typeaheadIndex,
});
}
if (isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
@@ -167,22 +171,19 @@ export class Typeahead extends PureComponent<Props, State> {
return (
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
<ul role="menu" className={styles.typeahead} data-testid="typeahead">
<FixedSizeList
ref={this.listRef}
itemCount={allItems.length}
itemSize={itemHeight}
itemKey={(index) => {
const item = allItems && allItems[index];
const key = item ? `${index}-${item.label}` : `${index}`;
return key;
<List
listRef={this.listRef}
rowCount={allItems.length}
rowHeight={itemHeight}
rowProps={{}}
style={{
width: listWidth,
height: listHeight,
}}
width={listWidth}
height={listHeight}
>
{({ index, style }) => {
rowComponent={({ index, style }) => {
const item = allItems && allItems[index];
if (!item) {
return null;
return <></>;
}
return (
@@ -197,7 +198,7 @@ export class Typeahead extends PureComponent<Props, State> {
/>
);
}}
</FixedSizeList>
/>
</ul>
{showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />}

View File

@@ -1,9 +1,9 @@
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 Skeleton from 'react-loading-skeleton';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { List, type RowComponentProps } from 'react-window';
import { useInfiniteLoader } from 'react-window-infinite-loader';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
@@ -47,32 +47,8 @@ export function NestedFolderList({
requestLoadMore,
emptyFolders,
}: NestedFolderListProps) {
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
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(
(itemIndex: number) => {
return isItemLoaded(itemIndex);
@@ -81,36 +57,42 @@ export function NestedFolderList({
);
const handleLoadMore = useCallback(
(startIndex: number, endIndex: number) => {
async (startIndex: number, endIndex: number) => {
const { parentUID } = items[startIndex];
requestLoadMore(parentUID);
},
[requestLoadMore, items]
);
const onRowsRendered = useInfiniteLoader({
rowCount: items.length,
isRowLoaded: handleIsItemLoaded,
loadMoreRows: handleLoadMore,
});
return (
<div className={styles.table} role="tree">
{items.length > 0 ? (
<InfiniteLoader
ref={infiniteLoaderRef}
itemCount={items.length}
isItemLoaded={handleIsItemLoaded}
loadMoreItems={handleLoadMore}
>
{({ onItemsRendered, ref }) => (
<List
ref={ref}
height={ROW_HEIGHT * Math.min(6.5, items.length)}
width="100%"
itemData={virtualData}
itemSize={ROW_HEIGHT}
itemCount={items.length}
onItemsRendered={onItemsRendered}
>
{Row}
</List>
)}
</InfiniteLoader>
<List
onRowsRendered={onRowsRendered}
rowProps={{
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
}}
rowComponent={Row}
rowCount={items.length}
rowHeight={ROW_HEIGHT}
style={{
height: ROW_HEIGHT * Math.min(6.5, items.length),
width: '100%',
}}
/>
) : (
<div className={styles.emptyMessage}>
<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 RowProps {
index: number;
style: React.CSSProperties;
data: VirtualData;
}
const SKELETON_WIDTHS = [100, 200, 130, 160, 150];
function Row({ index, style: virtualStyles, data }: RowProps) {
const {
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
} = data;
function Row({
index,
style: virtualStyles,
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
}: RowComponentProps<VirtualData>) {
const { item, isOpen, level, parentUID } = items[index];
const rowRef = useRef<HTMLDivElement>(null);
const labelId = useId();
@@ -190,7 +167,9 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
Non-folder {{ itemKind }} {{ itemUID }}
</Trans>
</span>
) : null;
) : (
<></>
);
}
// 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 { CSSProperties, useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
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 { Trans, t } from '@grafana/i18n';
@@ -95,10 +95,7 @@ export function AlertInstanceModalSelector({
const filteredRulesKeys = Object.keys(filteredRules || []);
const RuleRow = ({ index, style }: { index: number; style?: CSSProperties }) => {
if (!filteredRules) {
return null;
}
const RuleRow = ({ index, style }: RowComponentProps) => {
const ruleName = filteredRulesKeys[index];
const isSelected = ruleName === selectedRule;
@@ -133,7 +130,7 @@ export function AlertInstanceModalSelector({
return tags;
};
const InstanceRow = ({ index, style }: { index: number; style: CSSProperties }) => {
const InstanceRow = ({ index, style }: RowComponentProps) => {
const alerts = useMemo(() => (selectedRule ? rulesWithInstances[selectedRule] : []), []);
const alert = alerts[index];
const isSelected = selectedInstances?.includes(alert);
@@ -236,9 +233,16 @@ export function AlertInstanceModalSelector({
{!loading && (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList itemSize={50} height={height} width={width} itemCount={filteredRulesKeys.length}>
{RuleRow}
</FixedSizeList>
<List
rowComponent={RuleRow}
rowCount={filteredRulesKeys.length}
rowHeight={50}
rowProps={{}}
style={{
height,
width,
}}
/>
)}
</AutoSizer>
)}
@@ -264,14 +268,16 @@ export function AlertInstanceModalSelector({
{selectedRule && rulesWithInstances[selectedRule].length && !loading && (
<AutoSizer>
{({ width, height }) => (
<FixedSizeList
itemSize={32}
height={height}
width={width}
itemCount={rulesWithInstances[selectedRule].length || 0}
>
{InstanceRow}
</FixedSizeList>
<List
rowComponent={InstanceRow}
rowCount={rulesWithInstances[selectedRule].length || 0}
rowHeight={32}
rowProps={{}}
style={{
height,
width,
}}
/>
)}
</AutoSizer>
)}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import { cloneDeep } from 'lodash';
import { useEffect, useId, useRef, useState } from 'react';
import { useId, useState } from 'react';
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 { Trans, t } from '@grafana/i18n';
@@ -59,6 +59,24 @@ const styles = {
const mobileWidthThreshold = 480;
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
* @param props
@@ -67,7 +85,6 @@ const numberOfColumnsBeforeExpandedViewIsDefault = 2;
const RawListContainer = (props: RawListContainerProps) => {
const { tableResult } = props;
const dataFrame = cloneDeep(tableResult);
const listRef = useRef<List | null>(null);
const valueLabels = dataFrame.fields.filter((field) => field.name.includes('Value'));
const items = getRawPrometheusListItemsFromDataFrame(dataFrame);
@@ -84,11 +101,6 @@ const RawListContainer = (props: RawListContainerProps) => {
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 maxListHeight = 600;
const shortListLength = 10;
@@ -96,7 +108,11 @@ const RawListContainer = (props: RawListContainerProps) => {
if (length < shortListLength) {
let sum = 0;
for (let i = 0; i < length; i++) {
sum += getListItemHeight(i, true);
sum += getListItemHeight(i, {
isExpandedView: true,
items,
valueLabels,
});
}
return Math.min(maxListHeight, sum);
@@ -105,18 +121,6 @@ const RawListContainer = (props: RawListContainerProps) => {
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()}`;
return (
@@ -153,14 +157,19 @@ const RawListContainer = (props: RawListContainerProps) => {
<ItemLabels valueLabels={valueLabels} expanded={isExpandedView} />
)}
<List
ref={listRef}
itemCount={items.length}
className={styles.wrapper}
itemSize={(index) => getListItemHeight(index, isExpandedView)}
height={calculateInitialHeight(items.length)}
width="100%"
>
{({ index, style }) => {
style={{
height: calculateInitialHeight(items.length),
width: '100%',
}}
rowCount={items.length}
rowHeight={getListItemHeight}
rowProps={{
isExpandedView,
valueLabels,
items,
}}
rowComponent={({ index, style }) => {
let filteredValueLabels: DataFrameField[] | undefined;
if (isExpandedView) {
filteredValueLabels = valueLabels.filter((valueLabel) => {
@@ -181,7 +190,7 @@ const RawListContainer = (props: RawListContainerProps) => {
</div>
);
}}
</List>
/>
</>
}
</div>

View File

@@ -1,5 +1,5 @@
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 { LogsSortOrder } from '@grafana/schema';
@@ -89,19 +89,19 @@ function setup(
loadMore={loadMoreMock}
infiniteScrollMode={infiniteScrollMode}
>
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
<VariableSizeList
height={100}
itemCount={itemCount}
itemSize={() => virtualization.getLineHeight()}
itemKey={getItemKey}
layout="vertical"
onItemsRendered={onItemsRendered}
style={{ overflow: 'scroll' }}
width="100%"
>
{Renderer}
</VariableSizeList>
{({ itemCount, onItemsRendered, Renderer }) => (
<List
rowComponent={Renderer}
rowCount={itemCount}
rowProps={{}}
rowHeight={() => virtualization.getLineHeight()}
onRowsRendered={onItemsRendered}
style={{
overflow: 'scroll',
height: 100,
width: '100%',
}}
/>
)}
</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 { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window';
import { type RowComponentProps, type ListProps } from 'react-window';
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
import { t } from '@grafana/i18n';
@@ -17,8 +17,8 @@ import { LogLineVirtualization } from './virtualization';
interface ChildrenProps {
itemCount: number;
getItemKey: (index: number) => string;
onItemsRendered: (props: ListOnItemsRenderedProps) => void;
Renderer: (props: ListChildComponentProps) => ReactNode;
onItemsRendered: ListProps<{}>['onRowsRendered'];
Renderer: (props: RowComponentProps) => JSX.Element;
}
export interface Props {
@@ -200,7 +200,7 @@ export const InfiniteScroll = ({
}, [onLoadMore]);
const Renderer = useCallback(
({ index, style }: ListChildComponentProps) => {
({ index, style }: RowComponentProps) => {
if (!logs[index] && infiniteLoaderState !== 'idle') {
return (
<LogLineMessage
@@ -248,12 +248,12 @@ export const InfiniteScroll = ({
]
);
const onItemsRendered = useCallback(
(props: ListOnItemsRenderedProps) => {
const onItemsRendered = useCallback<NonNullable<ListProps<{}>['onRowsRendered']>>(
(props) => {
if (!scrollElement) {
return;
}
if (props.visibleStartIndex === 0) {
if (props.startIndex === 0) {
noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight;
}
if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
@@ -261,9 +261,9 @@ export const InfiniteScroll = ({
}
const lastLogIndex = logs.length - 1;
const preScrollIndex = logs.length - 2;
if (props.visibleStopIndex >= lastLogIndex) {
if (props.stopIndex >= lastLogIndex) {
setInfiniteLoaderState('pre-scroll-bottom');
} else if (props.visibleStartIndex < preScrollIndex) {
} else if (props.startIndex < preScrollIndex) {
setInfiniteLoaderState('idle');
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1473,7 +1473,7 @@ __metadata:
languageName: node
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
resolution: "@babel/runtime@npm:7.28.4"
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
@@ -2654,7 +2654,6 @@ __metadata:
"@types/node": "npm:24.10.1"
"@types/react": "npm:18.3.18"
"@types/react-dom": "npm:18.3.5"
"@types/react-window": "npm:1.8.8"
"@types/uuid": "npm:10.0.0"
jest: "npm:29.7.0"
lodash: "npm:4.17.21"
@@ -2662,7 +2661,7 @@ __metadata:
react: "npm:18.3.1"
react-dom: "npm:18.3.1"
react-select: "npm:5.10.2"
react-window: "npm:1.8.11"
react-window: "npm:2.2.3"
rxjs: "npm:7.8.2"
stream-browserify: "npm:3.0.0"
ts-node: "npm:10.9.2"
@@ -3522,7 +3521,6 @@ __metadata:
"@types/react": "npm:18.3.18"
"@types/react-dom": "npm:18.3.5"
"@types/react-highlight-words": "npm:0.20.0"
"@types/react-window": "npm:1.8.8"
"@types/semver": "npm:7.7.1"
"@types/uuid": "npm:10.0.0"
debounce-promise: "npm:3.1.2"
@@ -3542,7 +3540,7 @@ __metadata:
react-highlight-words: "npm:0.21.0"
react-select-event: "npm:5.5.1"
react-use: "npm:17.6.0"
react-window: "npm:1.8.11"
react-window: "npm:2.2.3"
rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4"
rollup-plugin-esbuild: "npm:6.2.1"
@@ -3825,7 +3823,6 @@ __metadata:
"@types/react-highlight-words": "npm:0.20.0"
"@types/react-table": "npm:7.7.20"
"@types/react-transition-group": "npm:4.4.12"
"@types/react-window": "npm:1.8.8"
"@types/slate": "npm:0.47.11"
"@types/slate-plain-serializer": "npm:0.7.5"
"@types/slate-react": "npm:0.22.9"
@@ -3885,7 +3882,7 @@ __metadata:
react-table: "npm:7.8.0"
react-transition-group: "npm:4.4.5"
react-use: "npm:17.6.0"
react-window: "npm:1.8.11"
react-window: "npm:2.2.3"
rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4"
rollup-plugin-copy: "npm:3.5.0"
@@ -10727,25 +10724,6 @@ __metadata:
languageName: node
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":
version: 18.3.18
resolution: "@types/react@npm:18.3.18"
@@ -19319,8 +19297,6 @@ __metadata:
"@types/react-table": "npm:7.7.20"
"@types/react-transition-group": "npm:4.4.12"
"@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/semver": "npm:7.7.1"
"@types/slate": "npm:0.47.11"
@@ -19500,8 +19476,8 @@ __metadata:
react-use: "npm:17.6.0"
react-virtual: "npm:2.10.4"
react-virtualized-auto-sizer: "npm:1.0.26"
react-window: "npm:1.8.11"
react-window-infinite-loader: "npm:1.0.10"
react-window: "npm:2.2.3"
react-window-infinite-loader: "npm:2.0.0"
reduce-reducers: "npm:^1.0.4"
redux: "npm:5.0.1"
redux-mock-store: "npm:1.5.5"
@@ -23894,13 +23870,6 @@ __metadata:
languageName: node
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":
version: 4.0.3
resolution: "memoize-one@npm:4.0.3"
@@ -28964,26 +28933,23 @@ __metadata:
languageName: node
linkType: hard
"react-window-infinite-loader@npm:1.0.10":
version: 1.0.10
resolution: "react-window-infinite-loader@npm:1.0.10"
"react-window-infinite-loader@npm:2.0.0":
version: 2.0.0
resolution: "react-window-infinite-loader@npm:2.0.0"
peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/4f4c097a2948f8da71d13199289d85c89720e41888ea50039d2b8e6d7cf160300e97f66421c53420d8cb95b3ece74940c07ebd23bddd7a705a679d7e2dc2957e
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
checksum: 10/a67ba08dbdb557a46390bf1056ad01d09202fc76492d27818429ac8f9e893792c3476a584fadfc7ae3f2766e1986468658356105000d7b31034287012e7a89dc
languageName: node
linkType: hard
"react-window@npm:1.8.11":
version: 1.8.11
resolution: "react-window@npm:1.8.11"
dependencies:
"@babel/runtime": "npm:^7.0.0"
memoize-one: "npm:>=3.1.1 <6"
"react-window@npm:2.2.3":
version: 2.2.3
resolution: "react-window@npm:2.2.3"
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/bdbac2b664c5a799443b97a32b2f60a00cc13cc14ca8a8b1e81e2dc7dd00d8d54f05743113972fe1a641b57ada5d874b59c3cbe7e8a07a88c6713a0fb65d60f6
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
checksum: 10/f676883c6cd16a0f56690b6d9c929a3da8b7d0ccf78dd5247f474b65ead38aae32a328fcd24c95956d1541383b774fca2b6decae65b6991697a90168222de86d
languageName: node
linkType: hard