mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 03:54:29 +08:00
Compare commits
15 Commits
provisioni
...
ash/upgrad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50150f36fc | ||
|
|
c32da02543 | ||
|
|
0f56441b8b | ||
|
|
951994641f | ||
|
|
546aa0f614 | ||
|
|
095c9b1a10 | ||
|
|
fdcdf80eb6 | ||
|
|
039ee5802f | ||
|
|
cd50a4cef7 | ||
|
|
2ae4ff8b4d | ||
|
|
6de2336232 | ||
|
|
1e769cbc1b | ||
|
|
660dfb3aba | ||
|
|
86294ca337 | ||
|
|
2f32b59c2f |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
thead: css({
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
70
yarn.lock
70
yarn.lock
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user