mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 12:04:45 +08:00
Compare commits
15 Commits
docs/grafa
...
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": {
|
"packages/grafana-ui/src/components/Table/TableRT/Table.tsx": {
|
||||||
"@typescript-eslint/consistent-type-assertions": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,8 +151,6 @@
|
|||||||
"@types/react-table": "7.7.20",
|
"@types/react-table": "7.7.20",
|
||||||
"@types/react-transition-group": "4.4.12",
|
"@types/react-transition-group": "4.4.12",
|
||||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||||
"@types/react-window": "1.8.8",
|
|
||||||
"@types/react-window-infinite-loader": "^1",
|
|
||||||
"@types/redux-mock-store": "1.5.0",
|
"@types/redux-mock-store": "1.5.0",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/slate": "0.47.11",
|
"@types/slate": "0.47.11",
|
||||||
@@ -414,8 +412,8 @@
|
|||||||
"react-use": "17.6.0",
|
"react-use": "17.6.0",
|
||||||
"react-virtual": "2.10.4",
|
"react-virtual": "2.10.4",
|
||||||
"react-virtualized-auto-sizer": "1.0.26",
|
"react-virtualized-auto-sizer": "1.0.26",
|
||||||
"react-window": "1.8.11",
|
"react-window": "2.2.3",
|
||||||
"react-window-infinite-loader": "1.0.10",
|
"react-window-infinite-loader": "2.0.0",
|
||||||
"reduce-reducers": "^1.0.4",
|
"reduce-reducers": "^1.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"redux-thunk": "3.1.0",
|
"redux-thunk": "3.1.0",
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.18",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react-highlight-words": "0.20.0",
|
"@types/react-highlight-words": "0.20.0",
|
||||||
"@types/react-window": "1.8.8",
|
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"debounce-promise": "3.1.2",
|
"debounce-promise": "3.1.2",
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
"react-highlight-words": "0.21.0",
|
"react-highlight-words": "0.21.0",
|
||||||
"react-use": "17.6.0",
|
"react-use": "17.6.0",
|
||||||
"react-window": "1.8.11",
|
"react-window": "2.2.3",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"uuid": "11.1.0"
|
"uuid": "11.1.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List } from 'react-window';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
@@ -65,15 +65,12 @@ export function MetricSelector() {
|
|||||||
className={styles.valueListWrapper}
|
className={styles.valueListWrapper}
|
||||||
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList}
|
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList}
|
||||||
>
|
>
|
||||||
<FixedSizeList
|
<List
|
||||||
height={Math.min(450, filteredMetrics.length * LIST_ITEM_SIZE)}
|
rowProps={{}}
|
||||||
itemCount={filteredMetrics.length}
|
rowCount={filteredMetrics.length}
|
||||||
itemSize={LIST_ITEM_SIZE}
|
rowHeight={LIST_ITEM_SIZE}
|
||||||
itemKey={(i) => filteredMetrics[i].name}
|
|
||||||
width={300}
|
|
||||||
className={styles.valueList}
|
className={styles.valueList}
|
||||||
>
|
rowComponent={({ index, style }) => {
|
||||||
{({ index, style }) => {
|
|
||||||
const metric = filteredMetrics[index];
|
const metric = filteredMetrics[index];
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
@@ -92,7 +89,11 @@ export function MetricSelector() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</FixedSizeList>
|
style={{
|
||||||
|
height: Math.min(450, filteredMetrics.length * LIST_ITEM_SIZE),
|
||||||
|
width: 300,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List } from 'react-window';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { t, Trans } from '@grafana/i18n';
|
import { t, Trans } from '@grafana/i18n';
|
||||||
@@ -78,15 +78,16 @@ export function ValueSelector() {
|
|||||||
<div className={styles.valueTitle}>
|
<div className={styles.valueTitle}>
|
||||||
<PromLabel name={lk} active={true} hidden={false} facets={lv.length} onClick={onLabelKeyClick} />
|
<PromLabel name={lk} active={true} hidden={false} facets={lv.length} onClick={onLabelKeyClick} />
|
||||||
</div>
|
</div>
|
||||||
<FixedSizeList
|
<List
|
||||||
height={Math.min(200, LIST_ITEM_SIZE * (lv.length || 0))}
|
rowProps={{}}
|
||||||
itemCount={lv.length || 0}
|
rowCount={lv.length || 0}
|
||||||
itemSize={28}
|
rowHeight={28}
|
||||||
itemKey={(i) => lv[i]}
|
style={{
|
||||||
width={200}
|
height: Math.min(200, LIST_ITEM_SIZE * (lv.length || 0)),
|
||||||
|
width: 200,
|
||||||
|
}}
|
||||||
className={styles.valueList}
|
className={styles.valueList}
|
||||||
>
|
rowComponent={({ index, style }) => {
|
||||||
{({ index, style }) => {
|
|
||||||
const value = lv[index];
|
const value = lv[index];
|
||||||
const isSelected = selectedLabelValues[lk]?.includes(value);
|
const isSelected = selectedLabelValues[lk]?.includes(value);
|
||||||
return (
|
return (
|
||||||
@@ -101,7 +102,7 @@ export function ValueSelector() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</FixedSizeList>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
"react-table": "7.8.0",
|
"react-table": "7.8.0",
|
||||||
"react-transition-group": "4.4.5",
|
"react-transition-group": "4.4.5",
|
||||||
"react-use": "17.6.0",
|
"react-use": "17.6.0",
|
||||||
"react-window": "1.8.11",
|
"react-window": "2.2.3",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"slate": "0.47.9",
|
"slate": "0.47.9",
|
||||||
"slate-plain-serializer": "0.7.13",
|
"slate-plain-serializer": "0.7.13",
|
||||||
@@ -172,7 +172,6 @@
|
|||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react-highlight-words": "0.20.0",
|
"@types/react-highlight-words": "0.20.0",
|
||||||
"@types/react-transition-group": "4.4.12",
|
"@types/react-transition-group": "4.4.12",
|
||||||
"@types/react-window": "1.8.8",
|
|
||||||
"@types/slate": "0.47.11",
|
"@types/slate": "0.47.11",
|
||||||
"@types/slate-plain-serializer": "0.7.5",
|
"@types/slate-plain-serializer": "0.7.5",
|
||||||
"@types/slate-react": "0.22.9",
|
"@types/slate-react": "0.22.9",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { max } from 'lodash';
|
import { max } from 'lodash';
|
||||||
import { RefCallback, useLayoutEffect, useMemo, useRef, type JSX } from 'react';
|
import { RefCallback, useLayoutEffect, useMemo, type JSX } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { List, useListRef } from 'react-window';
|
||||||
|
|
||||||
import { SelectableValue, toIconName } from '@grafana/data';
|
import { SelectableValue, toIconName } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@@ -101,14 +101,13 @@ interface VirtualSelectMenuProps<T> {
|
|||||||
export const VirtualizedSelectMenu = ({
|
export const VirtualizedSelectMenu = ({
|
||||||
children,
|
children,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
innerRef: scrollRef,
|
|
||||||
options,
|
options,
|
||||||
selectProps,
|
selectProps,
|
||||||
focusedOption,
|
focusedOption,
|
||||||
}: VirtualSelectMenuProps<SelectableValue>) => {
|
}: VirtualSelectMenuProps<SelectableValue>) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getSelectStyles(theme);
|
const styles = getSelectStyles(theme);
|
||||||
const listRef = useRef<List>(null);
|
const listRef = useListRef(null);
|
||||||
const { toggleAllOptions, components } = selectProps;
|
const { toggleAllOptions, components } = selectProps;
|
||||||
|
|
||||||
const optionComponent = components?.Option ?? SelectMenuOptions;
|
const optionComponent = components?.Option ?? SelectMenuOptions;
|
||||||
@@ -126,8 +125,10 @@ export const VirtualizedSelectMenu = ({
|
|||||||
(option: SelectableValue<unknown>) => option.value === focusedOption?.value
|
(option: SelectableValue<unknown>) => option.value === focusedOption?.value
|
||||||
);
|
);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
listRef.current?.scrollToItem(focusedIndex);
|
listRef.current?.scrollToRow({
|
||||||
}, [focusedIndex]);
|
index: focusedIndex,
|
||||||
|
});
|
||||||
|
}, [focusedIndex, listRef]);
|
||||||
|
|
||||||
if (!Array.isArray(children)) {
|
if (!Array.isArray(children)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -180,17 +181,20 @@ export const VirtualizedSelectMenu = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
outerRef={scrollRef}
|
rowComponent={({ index, style }) => (
|
||||||
ref={listRef}
|
<div style={{ ...style, overflow: 'hidden' }}>{flattenedChildren[index]}</div>
|
||||||
|
)}
|
||||||
|
rowCount={flattenedChildren.length}
|
||||||
|
rowHeight={VIRTUAL_LIST_ITEM_HEIGHT}
|
||||||
|
rowProps={{}}
|
||||||
|
listRef={listRef}
|
||||||
className={styles.menu}
|
className={styles.menu}
|
||||||
height={heightEstimate}
|
style={{
|
||||||
width={widthEstimate}
|
height: heightEstimate,
|
||||||
|
width: widthEstimate,
|
||||||
|
}}
|
||||||
aria-label={t('grafana-ui.select.menu-label', 'Select options menu')}
|
aria-label={t('grafana-ui.select.menu-label', 'Select options menu')}
|
||||||
itemCount={flattenedChildren.length}
|
/>
|
||||||
itemSize={VIRTUAL_LIST_ITEM_HEIGHT}
|
|
||||||
>
|
|
||||||
{({ index, style }) => <div style={{ ...style, overflow: 'hidden' }}>{flattenedChildren[index]}</div>}
|
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { List, type RowComponentProps } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@@ -162,15 +162,16 @@ export const FilterList = ({ options, values, caseSensitive, onChange, searchFil
|
|||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<List
|
<List
|
||||||
height={height}
|
rowComponent={ItemRenderer}
|
||||||
itemCount={items.length}
|
rowCount={items.length}
|
||||||
itemSize={ITEM_HEIGHT}
|
rowHeight={ITEM_HEIGHT}
|
||||||
itemData={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
|
rowProps={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
|
||||||
width="100%"
|
style={{
|
||||||
|
height,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
className={styles.filterList}
|
className={styles.filterList}
|
||||||
>
|
/>
|
||||||
{ItemRenderer}
|
|
||||||
</List>
|
|
||||||
<div
|
<div
|
||||||
className={styles.filterListRow}
|
className={styles.filterListRow}
|
||||||
data-testid={selectors.components.Panels.Visualization.TableNG.Filters.SelectAll}
|
data-testid={selectors.components.Panels.Visualization.TableNG.Filters.SelectAll}
|
||||||
@@ -193,16 +194,21 @@ export const FilterList = ({ options, values, caseSensitive, onChange, searchFil
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ItemRendererProps extends ListChildComponentProps {
|
interface ItemRendererProps {
|
||||||
data: {
|
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
|
items: SelectableValue[];
|
||||||
items: SelectableValue[];
|
values: SelectableValue[];
|
||||||
values: SelectableValue[];
|
className: string;
|
||||||
className: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemRenderer({ index, style, data: { onCheckedChanged, items, values, className } }: ItemRendererProps) {
|
function ItemRenderer({
|
||||||
|
index,
|
||||||
|
style,
|
||||||
|
onCheckedChanged,
|
||||||
|
items,
|
||||||
|
values,
|
||||||
|
className,
|
||||||
|
}: RowComponentProps<ItemRendererProps>) {
|
||||||
const option = items[index];
|
const option = items[index];
|
||||||
const { value, label } = option;
|
const { value, label } = option;
|
||||||
const isChecked = values.find((s) => s.value === value) !== undefined;
|
const isChecked = values.find((s) => s.value === value) !== undefined;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { List, type RowComponentProps } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
|
||||||
import { t, Trans } from '@grafana/i18n';
|
import { t, Trans } from '@grafana/i18n';
|
||||||
@@ -202,15 +202,16 @@ export const FilterList = ({
|
|||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<List
|
<List
|
||||||
height={height}
|
rowComponent={ItemRenderer}
|
||||||
itemCount={items.length}
|
rowCount={items.length}
|
||||||
itemSize={ITEM_HEIGHT}
|
rowHeight={ITEM_HEIGHT}
|
||||||
itemData={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
|
rowProps={{ items, values: selectedItems, onCheckedChanged, className: styles.filterListRow }}
|
||||||
width="100%"
|
style={{
|
||||||
|
height,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
className={styles.filterList}
|
className={styles.filterList}
|
||||||
>
|
/>
|
||||||
{ItemRenderer}
|
|
||||||
</List>
|
|
||||||
<Stack direction="column" gap={0.25}>
|
<Stack direction="column" gap={0.25}>
|
||||||
<div className={cx(styles.selectDivider)} />
|
<div className={cx(styles.selectDivider)} />
|
||||||
<div className={cx(styles.filterListRow)}>
|
<div className={cx(styles.filterListRow)}>
|
||||||
@@ -233,16 +234,21 @@ export const FilterList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ItemRendererProps extends ListChildComponentProps {
|
interface ItemRendererProps {
|
||||||
data: {
|
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent<HTMLInputElement>) => void;
|
items: SelectableValue[];
|
||||||
items: SelectableValue[];
|
values: SelectableValue[];
|
||||||
values: SelectableValue[];
|
className: string;
|
||||||
className: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemRenderer({ index, style, data: { onCheckedChanged, items, values, className } }: ItemRendererProps) {
|
function ItemRenderer({
|
||||||
|
index,
|
||||||
|
style,
|
||||||
|
onCheckedChanged,
|
||||||
|
items,
|
||||||
|
values,
|
||||||
|
className,
|
||||||
|
}: RowComponentProps<ItemRendererProps>) {
|
||||||
const option = items[index];
|
const option = items[index];
|
||||||
const { value, label } = option;
|
const { value, label } = option;
|
||||||
const isChecked = values.find((s) => s.value === value) !== undefined;
|
const isChecked = values.find((s) => s.value === value) !== undefined;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
|
import { CSSProperties, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Cell, Row, TableState, HeaderGroup } from 'react-table';
|
import { Cell, Row, TableState, HeaderGroup } from 'react-table';
|
||||||
import { VariableSizeList } from 'react-window';
|
import { List, useListRef } from 'react-window';
|
||||||
import { Subscription, debounceTime } from 'rxjs';
|
import { Subscription, debounceTime } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema';
|
import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema';
|
||||||
|
|
||||||
import { useTheme2 } from '../../../themes/ThemeContext';
|
import { useTheme2 } from '../../../themes/ThemeContext';
|
||||||
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar';
|
|
||||||
import { usePanelContext } from '../../PanelChrome';
|
import { usePanelContext } from '../../PanelChrome';
|
||||||
import { TableCell } from '../Cells/TableCell';
|
import { TableCell } from '../Cells/TableCell';
|
||||||
import {
|
import {
|
||||||
@@ -42,14 +41,10 @@ interface RowsListProps {
|
|||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
rows: Row[];
|
rows: Row[];
|
||||||
enableSharedCrosshair: boolean;
|
enableSharedCrosshair: boolean;
|
||||||
headerHeight: number;
|
|
||||||
rowHeight: number;
|
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
pageIndex: number;
|
|
||||||
listHeight: number;
|
listHeight: number;
|
||||||
width: number;
|
width: number;
|
||||||
cellHeight?: TableCellHeight;
|
cellHeight?: TableCellHeight;
|
||||||
listRef: React.RefObject<VariableSizeList>;
|
|
||||||
tableState: TableState;
|
tableState: TableState;
|
||||||
tableStyles: TableStyles;
|
tableStyles: TableStyles;
|
||||||
nestedDataField?: Field;
|
nestedDataField?: Field;
|
||||||
@@ -70,11 +65,8 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
rows,
|
rows,
|
||||||
headerHeight,
|
|
||||||
footerPaginationEnabled,
|
footerPaginationEnabled,
|
||||||
rowHeight,
|
|
||||||
itemCount,
|
itemCount,
|
||||||
pageIndex,
|
|
||||||
tableState,
|
tableState,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
onCellFilterAdded,
|
onCellFilterAdded,
|
||||||
@@ -84,7 +76,6 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
tableStyles,
|
tableStyles,
|
||||||
nestedDataField,
|
nestedDataField,
|
||||||
listHeight,
|
listHeight,
|
||||||
listRef,
|
|
||||||
enableSharedCrosshair = false,
|
enableSharedCrosshair = false,
|
||||||
initialRowIndex = undefined,
|
initialRowIndex = undefined,
|
||||||
headerGroups,
|
headerGroups,
|
||||||
@@ -95,11 +86,25 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
setInspectCell,
|
setInspectCell,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const listRef = useListRef(null);
|
||||||
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
|
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
|
||||||
if (initialRowIndex === undefined && rowHighlightIndex !== undefined) {
|
if (initialRowIndex === undefined && rowHighlightIndex !== undefined) {
|
||||||
setRowHighlightIndex(undefined);
|
setRowHighlightIndex(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rowHighlightIndex !== undefined) {
|
||||||
|
// TODO can we do this without a setTimeout?
|
||||||
|
setTimeout(() => {
|
||||||
|
listRef.current?.scrollToRow({
|
||||||
|
index: rowHighlightIndex,
|
||||||
|
align: 'center',
|
||||||
|
behavior: 'instant',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [rowHighlightIndex, listRef]);
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const panelContext = usePanelContext();
|
const panelContext = usePanelContext();
|
||||||
|
|
||||||
@@ -234,15 +239,6 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
};
|
};
|
||||||
}, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]);
|
}, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]);
|
||||||
|
|
||||||
let scrollTop: number | undefined = undefined;
|
|
||||||
if (rowHighlightIndex !== undefined) {
|
|
||||||
const firstMatchedRowIndex = rows.findIndex((row) => row.index === rowHighlightIndex);
|
|
||||||
|
|
||||||
if (firstMatchedRowIndex !== -1) {
|
|
||||||
scrollTop = headerHeight + (firstMatchedRowIndex - 1) * rowHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowIndexForPagination = useCallback(
|
const rowIndexForPagination = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
return tableState.pageIndex * tableState.pageSize + index;
|
return tableState.pageIndex * tableState.pageSize + index;
|
||||||
@@ -316,6 +312,17 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
);
|
);
|
||||||
style.height = bbox.height;
|
style.height = bbox.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// some disgusting code to mutate the style object to convert transform to top
|
||||||
|
// this is all so that hover behaviour is maintained
|
||||||
|
// using transform creates new stacking contexts which means hover states don't overlay correctly
|
||||||
|
const yPos = style.transform?.match(/translateY\((.*)\)/)?.[1];
|
||||||
|
|
||||||
|
style = {
|
||||||
|
...style,
|
||||||
|
top: yPos,
|
||||||
|
transform: undefined,
|
||||||
|
};
|
||||||
const { key, ...rowProps } = row.getRowProps({ style, ...additionalProps });
|
const { key, ...rowProps } = row.getRowProps({ style, ...additionalProps });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -414,36 +421,17 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
return tableStyles.rowHeight;
|
return tableStyles.rowHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll: UIEventHandler = (event) => {
|
|
||||||
const { scrollTop } = event.currentTarget;
|
|
||||||
|
|
||||||
if (listRef.current !== null) {
|
|
||||||
listRef.current.scrollTo(scrollTop);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// It's a hack for text wrapping.
|
|
||||||
// VariableSizeList component didn't know that we manually set row height.
|
|
||||||
// So we need to reset the list when the rows high changes.
|
|
||||||
useEffect(() => {
|
|
||||||
if (listRef.current) {
|
|
||||||
listRef.current.resetAfterIndex(0);
|
|
||||||
}
|
|
||||||
}, [rows, listRef]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}>
|
<List
|
||||||
<VariableSizeList
|
rowProps={{}}
|
||||||
key={`${rowHeight}${pageIndex}`}
|
rowHeight={getItemSize}
|
||||||
height={listHeight}
|
rowCount={itemCount}
|
||||||
itemCount={itemCount}
|
listRef={listRef}
|
||||||
itemSize={getItemSize}
|
style={{
|
||||||
width={'100%'}
|
height: listHeight,
|
||||||
ref={listRef}
|
width,
|
||||||
style={{ overflow: undefined }}
|
}}
|
||||||
>
|
rowComponent={({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
|
||||||
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
|
/>
|
||||||
</VariableSizeList>
|
|
||||||
</CustomScrollbar>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
useSortBy,
|
useSortBy,
|
||||||
useTable,
|
useTable,
|
||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { VariableSizeList } from 'react-window';
|
|
||||||
|
|
||||||
import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data';
|
import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@@ -16,12 +15,10 @@ import { Trans } from '@grafana/i18n';
|
|||||||
import { TableCellHeight } from '@grafana/schema';
|
import { TableCellHeight } from '@grafana/schema';
|
||||||
|
|
||||||
import { useTheme2 } from '../../../themes/ThemeContext';
|
import { useTheme2 } from '../../../themes/ThemeContext';
|
||||||
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
|
|
||||||
import { Pagination } from '../../Pagination/Pagination';
|
import { Pagination } from '../../Pagination/Pagination';
|
||||||
import { TableCellInspector } from '../TableCellInspector';
|
import { TableCellInspector } from '../TableCellInspector';
|
||||||
import { useFixScrollbarContainer, useResetVariableListSizeCache } from '../hooks';
|
|
||||||
import { getInitialState, useTableStateReducer } from '../reducer';
|
import { getInitialState, useTableStateReducer } from '../reducer';
|
||||||
import { FooterItem, GrafanaTableState, InspectCell, TableRTProps as Props } from '../types';
|
import { FooterItem, InspectCell, TableRTProps as Props } from '../types';
|
||||||
import {
|
import {
|
||||||
getColumns,
|
getColumns,
|
||||||
sortCaseInsensitive,
|
sortCaseInsensitive,
|
||||||
@@ -70,7 +67,6 @@ export const Table = memo((props: Props) => {
|
|||||||
replaceVariables,
|
replaceVariables,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const listRef = useRef<VariableSizeList>(null);
|
|
||||||
const tableDivRef = useRef<HTMLDivElement>(null);
|
const tableDivRef = useRef<HTMLDivElement>(null);
|
||||||
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
|
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
@@ -200,7 +196,6 @@ export const Table = memo((props: Props) => {
|
|||||||
toggleAllRowsExpanded,
|
toggleAllRowsExpanded,
|
||||||
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
|
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
|
||||||
|
|
||||||
const extendedState = state as GrafanaTableState;
|
|
||||||
toggleAllRowsExpandedRef.current = toggleAllRowsExpanded;
|
toggleAllRowsExpandedRef.current = toggleAllRowsExpanded;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -270,9 +265,6 @@ export const Table = memo((props: Props) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useResetVariableListSizeCache(extendedState, listRef, data, hasUniqueId);
|
|
||||||
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
|
|
||||||
|
|
||||||
const onNavigate = useCallback(
|
const onNavigate = useCallback(
|
||||||
(toPage: number) => {
|
(toPage: number) => {
|
||||||
gotoPage(toPage - 1);
|
gotoPage(toPage - 1);
|
||||||
@@ -340,60 +332,51 @@ export const Table = memo((props: Props) => {
|
|||||||
ref={tableDivRef}
|
ref={tableDivRef}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
>
|
>
|
||||||
<CustomScrollbar hideVerticalTrack={true}>
|
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
|
||||||
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
|
{!noHeader && (
|
||||||
{!noHeader && (
|
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
|
||||||
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
|
)}
|
||||||
)}
|
{itemCount > 0 ? (
|
||||||
{itemCount > 0 ? (
|
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
|
||||||
<div
|
<RowsList
|
||||||
data-testid={selectors.components.Panels.Visualization.Table.body}
|
headerGroups={headerGroups}
|
||||||
ref={variableSizeListScrollbarRef}
|
data={data}
|
||||||
>
|
rows={rows}
|
||||||
<RowsList
|
width={width}
|
||||||
headerGroups={headerGroups}
|
cellHeight={cellHeight}
|
||||||
data={data}
|
itemCount={itemCount}
|
||||||
rows={rows}
|
listHeight={listHeight}
|
||||||
width={width}
|
tableState={state}
|
||||||
cellHeight={cellHeight}
|
prepareRow={prepareRow}
|
||||||
headerHeight={headerHeight}
|
timeRange={timeRange}
|
||||||
rowHeight={tableStyles.rowHeight}
|
onCellFilterAdded={onCellFilterAdded}
|
||||||
itemCount={itemCount}
|
nestedDataField={nestedDataField}
|
||||||
pageIndex={state.pageIndex}
|
|
||||||
listHeight={listHeight}
|
|
||||||
listRef={listRef}
|
|
||||||
tableState={state}
|
|
||||||
prepareRow={prepareRow}
|
|
||||||
timeRange={timeRange}
|
|
||||||
onCellFilterAdded={onCellFilterAdded}
|
|
||||||
nestedDataField={nestedDataField}
|
|
||||||
tableStyles={tableStyles}
|
|
||||||
footerPaginationEnabled={Boolean(enablePagination)}
|
|
||||||
enableSharedCrosshair={enableSharedCrosshair}
|
|
||||||
initialRowIndex={initialRowIndex}
|
|
||||||
longestField={longestField}
|
|
||||||
textWrapField={textWrapField}
|
|
||||||
getActions={getActions}
|
|
||||||
replaceVariables={replaceVariables}
|
|
||||||
setInspectCell={setInspectCell}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
|
|
||||||
{noValuesDisplayText}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{footerItems && (
|
|
||||||
<FooterRow
|
|
||||||
isPaginationVisible={Boolean(enablePagination)}
|
|
||||||
footerValues={footerItems}
|
|
||||||
footerGroups={footerGroups}
|
|
||||||
totalColumnsWidth={totalColumnsWidth}
|
|
||||||
tableStyles={tableStyles}
|
tableStyles={tableStyles}
|
||||||
|
footerPaginationEnabled={Boolean(enablePagination)}
|
||||||
|
enableSharedCrosshair={enableSharedCrosshair}
|
||||||
|
initialRowIndex={initialRowIndex}
|
||||||
|
longestField={longestField}
|
||||||
|
textWrapField={textWrapField}
|
||||||
|
getActions={getActions}
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
|
setInspectCell={setInspectCell}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
</CustomScrollbar>
|
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
|
||||||
|
{noValuesDisplayText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{footerItems && (
|
||||||
|
<FooterRow
|
||||||
|
isPaginationVisible={Boolean(enablePagination)}
|
||||||
|
footerValues={footerItems}
|
||||||
|
footerGroups={footerGroups}
|
||||||
|
totalColumnsWidth={totalColumnsWidth}
|
||||||
|
tableStyles={tableStyles}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{paginationEl}
|
{paginationEl}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}),
|
}),
|
||||||
thead: css({
|
thead: css({
|
||||||
|
|||||||
@@ -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 { createRef, PureComponent } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List, type ListImperativeAPI } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2, ThemeContext } from '@grafana/data';
|
import { GrafanaTheme2, ThemeContext } from '@grafana/data';
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export interface State {
|
|||||||
export class Typeahead extends PureComponent<Props, State> {
|
export class Typeahead extends PureComponent<Props, State> {
|
||||||
static contextType = ThemeContext;
|
static contextType = ThemeContext;
|
||||||
context!: React.ContextType<typeof ThemeContext>;
|
context!: React.ContextType<typeof ThemeContext>;
|
||||||
listRef = createRef<FixedSizeList>();
|
listRef = createRef<ListImperativeAPI>();
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
hoveredItem: null,
|
hoveredItem: null,
|
||||||
@@ -81,10 +81,14 @@ export class Typeahead extends PureComponent<Props, State> {
|
|||||||
this.listRef.current
|
this.listRef.current
|
||||||
) {
|
) {
|
||||||
if (this.state.typeaheadIndex === 1) {
|
if (this.state.typeaheadIndex === 1) {
|
||||||
this.listRef.current.scrollToItem(0); // special case for handling the first group label
|
this.listRef.current.scrollToRow({
|
||||||
|
index: 0,
|
||||||
|
}); // special case for handling the first group label
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.listRef.current.scrollToItem(this.state.typeaheadIndex);
|
this.listRef.current.scrollToRow({
|
||||||
|
index: this.state.typeaheadIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
|
if (isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
|
||||||
@@ -167,22 +171,19 @@ export class Typeahead extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
|
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
|
||||||
<ul role="menu" className={styles.typeahead} data-testid="typeahead">
|
<ul role="menu" className={styles.typeahead} data-testid="typeahead">
|
||||||
<FixedSizeList
|
<List
|
||||||
ref={this.listRef}
|
listRef={this.listRef}
|
||||||
itemCount={allItems.length}
|
rowCount={allItems.length}
|
||||||
itemSize={itemHeight}
|
rowHeight={itemHeight}
|
||||||
itemKey={(index) => {
|
rowProps={{}}
|
||||||
const item = allItems && allItems[index];
|
style={{
|
||||||
const key = item ? `${index}-${item.label}` : `${index}`;
|
width: listWidth,
|
||||||
return key;
|
height: listHeight,
|
||||||
}}
|
}}
|
||||||
width={listWidth}
|
rowComponent={({ index, style }) => {
|
||||||
height={listHeight}
|
|
||||||
>
|
|
||||||
{({ index, style }) => {
|
|
||||||
const item = allItems && allItems[index];
|
const item = allItems && allItems[index];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return null;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,7 +198,7 @@ export class Typeahead extends PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</FixedSizeList>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />}
|
{showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useCallback, useId, useMemo, useRef } from 'react';
|
import { useCallback, useId, useRef } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { List, type RowComponentProps } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import { useInfiniteLoader } from 'react-window-infinite-loader';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Trans } from '@grafana/i18n';
|
import { Trans } from '@grafana/i18n';
|
||||||
@@ -47,32 +47,8 @@ export function NestedFolderList({
|
|||||||
requestLoadMore,
|
requestLoadMore,
|
||||||
emptyFolders,
|
emptyFolders,
|
||||||
}: NestedFolderListProps) {
|
}: NestedFolderListProps) {
|
||||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const virtualData = useMemo(
|
|
||||||
(): VirtualData => ({
|
|
||||||
items,
|
|
||||||
focusedItemIndex,
|
|
||||||
foldersAreOpenable,
|
|
||||||
selectedFolder,
|
|
||||||
onFolderExpand,
|
|
||||||
onFolderSelect,
|
|
||||||
idPrefix,
|
|
||||||
emptyFolders,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
items,
|
|
||||||
focusedItemIndex,
|
|
||||||
foldersAreOpenable,
|
|
||||||
selectedFolder,
|
|
||||||
onFolderExpand,
|
|
||||||
onFolderSelect,
|
|
||||||
idPrefix,
|
|
||||||
emptyFolders,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIsItemLoaded = useCallback(
|
const handleIsItemLoaded = useCallback(
|
||||||
(itemIndex: number) => {
|
(itemIndex: number) => {
|
||||||
return isItemLoaded(itemIndex);
|
return isItemLoaded(itemIndex);
|
||||||
@@ -81,36 +57,42 @@ export function NestedFolderList({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(
|
const handleLoadMore = useCallback(
|
||||||
(startIndex: number, endIndex: number) => {
|
async (startIndex: number, endIndex: number) => {
|
||||||
const { parentUID } = items[startIndex];
|
const { parentUID } = items[startIndex];
|
||||||
requestLoadMore(parentUID);
|
requestLoadMore(parentUID);
|
||||||
},
|
},
|
||||||
[requestLoadMore, items]
|
[requestLoadMore, items]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onRowsRendered = useInfiniteLoader({
|
||||||
|
rowCount: items.length,
|
||||||
|
isRowLoaded: handleIsItemLoaded,
|
||||||
|
loadMoreRows: handleLoadMore,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.table} role="tree">
|
<div className={styles.table} role="tree">
|
||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
<InfiniteLoader
|
<List
|
||||||
ref={infiniteLoaderRef}
|
onRowsRendered={onRowsRendered}
|
||||||
itemCount={items.length}
|
rowProps={{
|
||||||
isItemLoaded={handleIsItemLoaded}
|
items,
|
||||||
loadMoreItems={handleLoadMore}
|
focusedItemIndex,
|
||||||
>
|
foldersAreOpenable,
|
||||||
{({ onItemsRendered, ref }) => (
|
selectedFolder,
|
||||||
<List
|
onFolderExpand,
|
||||||
ref={ref}
|
onFolderSelect,
|
||||||
height={ROW_HEIGHT * Math.min(6.5, items.length)}
|
idPrefix,
|
||||||
width="100%"
|
emptyFolders,
|
||||||
itemData={virtualData}
|
}}
|
||||||
itemSize={ROW_HEIGHT}
|
rowComponent={Row}
|
||||||
itemCount={items.length}
|
rowCount={items.length}
|
||||||
onItemsRendered={onItemsRendered}
|
rowHeight={ROW_HEIGHT}
|
||||||
>
|
style={{
|
||||||
{Row}
|
height: ROW_HEIGHT * Math.min(6.5, items.length),
|
||||||
</List>
|
width: '100%',
|
||||||
)}
|
}}
|
||||||
</InfiniteLoader>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.emptyMessage}>
|
<div className={styles.emptyMessage}>
|
||||||
<Trans i18nKey="browse-dashboards.folder-picker.empty-message">No folders found</Trans>
|
<Trans i18nKey="browse-dashboards.folder-picker.empty-message">No folders found</Trans>
|
||||||
@@ -122,25 +104,20 @@ export function NestedFolderList({
|
|||||||
|
|
||||||
interface VirtualData extends Omit<NestedFolderListProps, 'isItemLoaded' | 'requestLoadMore'> {}
|
interface VirtualData extends Omit<NestedFolderListProps, 'isItemLoaded' | 'requestLoadMore'> {}
|
||||||
|
|
||||||
interface RowProps {
|
|
||||||
index: number;
|
|
||||||
style: React.CSSProperties;
|
|
||||||
data: VirtualData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SKELETON_WIDTHS = [100, 200, 130, 160, 150];
|
const SKELETON_WIDTHS = [100, 200, 130, 160, 150];
|
||||||
|
|
||||||
function Row({ index, style: virtualStyles, data }: RowProps) {
|
function Row({
|
||||||
const {
|
index,
|
||||||
items,
|
style: virtualStyles,
|
||||||
focusedItemIndex,
|
items,
|
||||||
foldersAreOpenable,
|
focusedItemIndex,
|
||||||
selectedFolder,
|
foldersAreOpenable,
|
||||||
onFolderExpand,
|
selectedFolder,
|
||||||
onFolderSelect,
|
onFolderExpand,
|
||||||
idPrefix,
|
onFolderSelect,
|
||||||
emptyFolders,
|
idPrefix,
|
||||||
} = data;
|
emptyFolders,
|
||||||
|
}: RowComponentProps<VirtualData>) {
|
||||||
const { item, isOpen, level, parentUID } = items[index];
|
const { item, isOpen, level, parentUID } = items[index];
|
||||||
const rowRef = useRef<HTMLDivElement>(null);
|
const rowRef = useRef<HTMLDivElement>(null);
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
@@ -190,7 +167,9 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
|
|||||||
Non-folder {{ itemKind }} {{ itemUID }}
|
Non-folder {{ itemKind }} {{ itemUID }}
|
||||||
</Trans>
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have a direct value of whether things are coming from user searching but this seems to be a good
|
// We don't have a direct value of whether things are coming from user searching but this seems to be a good
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { CSSProperties, useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List, type RowComponentProps } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
@@ -95,10 +95,7 @@ export function AlertInstanceModalSelector({
|
|||||||
|
|
||||||
const filteredRulesKeys = Object.keys(filteredRules || []);
|
const filteredRulesKeys = Object.keys(filteredRules || []);
|
||||||
|
|
||||||
const RuleRow = ({ index, style }: { index: number; style?: CSSProperties }) => {
|
const RuleRow = ({ index, style }: RowComponentProps) => {
|
||||||
if (!filteredRules) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ruleName = filteredRulesKeys[index];
|
const ruleName = filteredRulesKeys[index];
|
||||||
|
|
||||||
const isSelected = ruleName === selectedRule;
|
const isSelected = ruleName === selectedRule;
|
||||||
@@ -133,7 +130,7 @@ export function AlertInstanceModalSelector({
|
|||||||
return tags;
|
return tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InstanceRow = ({ index, style }: { index: number; style: CSSProperties }) => {
|
const InstanceRow = ({ index, style }: RowComponentProps) => {
|
||||||
const alerts = useMemo(() => (selectedRule ? rulesWithInstances[selectedRule] : []), []);
|
const alerts = useMemo(() => (selectedRule ? rulesWithInstances[selectedRule] : []), []);
|
||||||
const alert = alerts[index];
|
const alert = alerts[index];
|
||||||
const isSelected = selectedInstances?.includes(alert);
|
const isSelected = selectedInstances?.includes(alert);
|
||||||
@@ -236,9 +233,16 @@ export function AlertInstanceModalSelector({
|
|||||||
{!loading && (
|
{!loading && (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
<FixedSizeList itemSize={50} height={height} width={width} itemCount={filteredRulesKeys.length}>
|
<List
|
||||||
{RuleRow}
|
rowComponent={RuleRow}
|
||||||
</FixedSizeList>
|
rowCount={filteredRulesKeys.length}
|
||||||
|
rowHeight={50}
|
||||||
|
rowProps={{}}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
)}
|
)}
|
||||||
@@ -264,14 +268,16 @@ export function AlertInstanceModalSelector({
|
|||||||
{selectedRule && rulesWithInstances[selectedRule].length && !loading && (
|
{selectedRule && rulesWithInstances[selectedRule].length && !loading && (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<FixedSizeList
|
<List
|
||||||
itemSize={32}
|
rowComponent={InstanceRow}
|
||||||
height={height}
|
rowCount={rulesWithInstances[selectedRule].length || 0}
|
||||||
width={width}
|
rowHeight={32}
|
||||||
itemCount={rulesWithInstances[selectedRule].length || 0}
|
rowProps={{}}
|
||||||
>
|
style={{
|
||||||
{InstanceRow}
|
height,
|
||||||
</FixedSizeList>
|
width,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import { CSSProperties, useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useAsync, useDebounce } from 'react-use';
|
import { useAsync, useDebounce } from 'react-use';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List, type ListImperativeAPI, type RowComponentProps } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
@@ -104,11 +104,13 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
const selectedDashboardIsInPageResult = selectedDashboardIndex >= 0;
|
const selectedDashboardIsInPageResult = selectedDashboardIndex >= 0;
|
||||||
|
|
||||||
const scrollToItem = useCallback(
|
const scrollToItem = useCallback(
|
||||||
(node: FixedSizeList) => {
|
(list: ListImperativeAPI) => {
|
||||||
const canScroll = selectedDashboardIndex >= 0;
|
const canScroll = selectedDashboardIndex >= 0;
|
||||||
|
|
||||||
if (isDefaultSelection && canScroll) {
|
if (isDefaultSelection && canScroll) {
|
||||||
node?.scrollToItem(selectedDashboardIndex, 'smart');
|
list?.scrollToRow({
|
||||||
|
index: selectedDashboardIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isDefaultSelection, selectedDashboardIndex]
|
[isDefaultSelection, selectedDashboardIndex]
|
||||||
@@ -122,7 +124,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
[dashboardFilter]
|
[dashboardFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const DashboardRow = ({ index, style }: { index: number; style?: CSSProperties }) => {
|
const DashboardRow = ({ index, style }: RowComponentProps) => {
|
||||||
const dashboard = filteredDashboards[index];
|
const dashboard = filteredDashboards[index];
|
||||||
const isSelected = selectedDashboardUid === dashboard.uid;
|
const isSelected = selectedDashboardUid === dashboard.uid;
|
||||||
const folderTitle = locationInfo?.[dashboard.location]?.name ?? 'Dashboards';
|
const folderTitle = locationInfo?.[dashboard.location]?.name ?? 'Dashboards';
|
||||||
@@ -143,7 +145,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => {
|
const PanelRow = ({ index, style }: RowComponentProps) => {
|
||||||
const panel = filteredPanels[index];
|
const panel = filteredPanels[index];
|
||||||
const panelTitle = panel.title || '<No title>';
|
const panelTitle = panel.title || '<No title>';
|
||||||
const isSelected = Boolean(panel.id) && selectedPanelId === panel.id;
|
const isSelected = Boolean(panel.id) && selectedPanelId === panel.id;
|
||||||
@@ -258,15 +260,18 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
{!isDashSearchFetching && (
|
{!isDashSearchFetching && (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
<FixedSizeList
|
<List
|
||||||
ref={scrollToItem}
|
listRef={scrollToItem}
|
||||||
itemSize={50}
|
rowHeight={50}
|
||||||
height={height}
|
rowCount={filteredDashboards.length}
|
||||||
width={width}
|
rowComponent={DashboardRow}
|
||||||
itemCount={filteredDashboards.length}
|
rowProps={{}}
|
||||||
>
|
style={{
|
||||||
{DashboardRow}
|
height,
|
||||||
</FixedSizeList>
|
maxHeight: height,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
)}
|
)}
|
||||||
@@ -292,9 +297,17 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
{selectedDashboardUid && !isDashboardFetching && (
|
{selectedDashboardUid && !isDashboardFetching && (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<FixedSizeList itemSize={32} height={height} width={width} itemCount={filteredPanels.length}>
|
<List
|
||||||
{PanelRow}
|
rowHeight={32}
|
||||||
</FixedSizeList>
|
rowCount={filteredPanels.length}
|
||||||
|
rowComponent={PanelRow}
|
||||||
|
rowProps={{}}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
maxHeight: height,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
import { useCallback, useId, useMemo } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
import { TableInstance, useTable } from 'react-table';
|
import { TableInstance, useTable } from 'react-table';
|
||||||
import { VariableSizeList as List } from 'react-window';
|
import { List, type RowComponentProps } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import { useInfiniteLoader } from 'react-window-infinite-loader';
|
||||||
|
|
||||||
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@@ -45,6 +44,15 @@ const HEADER_HEIGHT = 36;
|
|||||||
const ROW_HEIGHT = 36;
|
const ROW_HEIGHT = 36;
|
||||||
const DIVIDER_HEIGHT = 0; // Yes - make it appear as a border on the row rather than a row itself
|
const DIVIDER_HEIGHT = 0; // Yes - make it appear as a border on the row rather than a row itself
|
||||||
|
|
||||||
|
function getRowHeight(rowIndex: number, { items }: VirtualListRowProps) {
|
||||||
|
const row = items[rowIndex];
|
||||||
|
if (row.item.kind === 'ui' && row.item.uiKind === 'divider') {
|
||||||
|
return DIVIDER_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ROW_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardsTree({
|
export function DashboardsTree({
|
||||||
items,
|
items,
|
||||||
width,
|
width,
|
||||||
@@ -59,23 +67,21 @@ export function DashboardsTree({
|
|||||||
permissions,
|
permissions,
|
||||||
}: DashboardsTreeProps) {
|
}: DashboardsTreeProps) {
|
||||||
const treeID = useId();
|
const treeID = useId();
|
||||||
|
|
||||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
|
||||||
const listRef = useRef<List | null>(null);
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
useEffect(() => {
|
// TODO verify if we need this with v2
|
||||||
// If the tree changed identity, then some indexes that were previously loaded may now be unloaded,
|
// useEffect(() => {
|
||||||
// especially after a refetch after a move/delete.
|
// // If the tree changed identity, then some indexes that were previously loaded may now be unloaded,
|
||||||
// Clear that cache, and check if we need to trigger another load
|
// // especially after a refetch after a move/delete.
|
||||||
if (infiniteLoaderRef.current) {
|
// // Clear that cache, and check if we need to trigger another load
|
||||||
infiniteLoaderRef.current.resetloadMoreItemsCache(true);
|
// if (infiniteLoaderRef.current) {
|
||||||
}
|
// infiniteLoaderRef.current.resetloadMoreItemsCache(true);
|
||||||
|
// }
|
||||||
|
|
||||||
if (listRef.current) {
|
// if (listRef.current) {
|
||||||
listRef.current.resetAfterIndex(0);
|
// listRef.current.resetAfterIndex(0);
|
||||||
}
|
// }
|
||||||
}, [items]);
|
// }, [items]);
|
||||||
|
|
||||||
const tableColumns = useMemo(() => {
|
const tableColumns = useMemo(() => {
|
||||||
const checkboxColumn: DashboardsTreeColumn = {
|
const checkboxColumn: DashboardsTreeColumn = {
|
||||||
@@ -111,20 +117,6 @@ export function DashboardsTree({
|
|||||||
const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout);
|
const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout);
|
||||||
const { getTableProps, getTableBodyProps, headerGroups } = table;
|
const { getTableProps, getTableBodyProps, headerGroups } = table;
|
||||||
|
|
||||||
const virtualData = useMemo(
|
|
||||||
() => ({
|
|
||||||
table,
|
|
||||||
isSelected,
|
|
||||||
onAllSelectionChange,
|
|
||||||
onItemSelectionChange,
|
|
||||||
treeID,
|
|
||||||
permissions,
|
|
||||||
}),
|
|
||||||
// we need this to rerender if items changes
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID, permissions]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIsItemLoaded = useCallback(
|
const handleIsItemLoaded = useCallback(
|
||||||
(itemIndex: number) => {
|
(itemIndex: number) => {
|
||||||
return isItemLoaded(itemIndex);
|
return isItemLoaded(itemIndex);
|
||||||
@@ -133,24 +125,18 @@ export function DashboardsTree({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(
|
const handleLoadMore = useCallback(
|
||||||
(startIndex: number, endIndex: number) => {
|
async (startIndex: number, endIndex: number) => {
|
||||||
const { parentUID } = items[startIndex];
|
const { parentUID } = items[startIndex];
|
||||||
requestLoadMore(parentUID);
|
requestLoadMore(parentUID);
|
||||||
},
|
},
|
||||||
[requestLoadMore, items]
|
[requestLoadMore, items]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRowHeight = useCallback(
|
const onRowsRendered = useInfiniteLoader({
|
||||||
(rowIndex: number) => {
|
rowCount: items.length,
|
||||||
const row = items[rowIndex];
|
isRowLoaded: handleIsItemLoaded,
|
||||||
if (row.item.kind === 'ui' && row.item.uiKind === 'divider') {
|
loadMoreRows: handleLoadMore,
|
||||||
return DIVIDER_HEIGHT;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return ROW_HEIGHT;
|
|
||||||
},
|
|
||||||
[items]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...getTableProps()} role="table">
|
<div {...getTableProps()} role="table">
|
||||||
@@ -175,51 +161,50 @@ export function DashboardsTree({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
<div {...getTableBodyProps()} data-testid={selectors.pages.BrowseDashboards.table.body}>
|
<div {...getTableBodyProps()} data-testid={selectors.pages.BrowseDashboards.table.body}>
|
||||||
<InfiniteLoader
|
<List
|
||||||
ref={infiniteLoaderRef}
|
rowComponent={VirtualListRow}
|
||||||
itemCount={items.length}
|
rowCount={items.length}
|
||||||
isItemLoaded={handleIsItemLoaded}
|
rowHeight={getRowHeight}
|
||||||
loadMoreItems={handleLoadMore}
|
rowProps={{
|
||||||
>
|
table,
|
||||||
{({ onItemsRendered, ref }) => (
|
isSelected,
|
||||||
<List
|
onAllSelectionChange,
|
||||||
ref={(elem) => {
|
onItemSelectionChange,
|
||||||
ref(elem);
|
treeID,
|
||||||
listRef.current = elem;
|
permissions,
|
||||||
}}
|
items,
|
||||||
height={height - HEADER_HEIGHT}
|
}}
|
||||||
width={width}
|
onRowsRendered={onRowsRendered}
|
||||||
itemCount={items.length}
|
style={{
|
||||||
itemData={virtualData}
|
height: height - HEADER_HEIGHT,
|
||||||
estimatedItemSize={ROW_HEIGHT}
|
width,
|
||||||
itemSize={getRowHeight}
|
}}
|
||||||
onItemsRendered={onItemsRendered}
|
/>
|
||||||
>
|
|
||||||
{VirtualListRow}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VirtualListRowProps {
|
interface VirtualListRowProps {
|
||||||
index: number;
|
items: DashboardsTreeItem[];
|
||||||
style: React.CSSProperties;
|
table: TableInstance<DashboardsTreeItem>;
|
||||||
data: {
|
isSelected: DashboardsTreeCellProps['isSelected'];
|
||||||
table: TableInstance<DashboardsTreeItem>;
|
onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];
|
||||||
isSelected: DashboardsTreeCellProps['isSelected'];
|
onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];
|
||||||
onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];
|
treeID: string;
|
||||||
onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];
|
permissions: BrowseDashboardsPermissions;
|
||||||
treeID: string;
|
|
||||||
permissions: BrowseDashboardsPermissions;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function VirtualListRow({ index, style, data }: VirtualListRowProps) {
|
function VirtualListRow({
|
||||||
|
index,
|
||||||
|
style,
|
||||||
|
table,
|
||||||
|
isSelected,
|
||||||
|
onItemSelectionChange,
|
||||||
|
treeID,
|
||||||
|
permissions,
|
||||||
|
}: RowComponentProps<VirtualListRowProps>) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { table, isSelected, onItemSelectionChange, treeID, permissions } = data;
|
|
||||||
const { rows, prepareRow } = table;
|
const { rows, prepareRow } = table;
|
||||||
|
|
||||||
const row = rows[index];
|
const row = rows[index];
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { memo, CSSProperties } from 'react';
|
import { type KeyboardEvent } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { areEqual, FixedSizeGrid as Grid } from 'react-window';
|
import { type CellComponentProps, Grid } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
@@ -11,20 +10,14 @@ import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
|||||||
import { ResourceItem } from './FolderPickerTab';
|
import { ResourceItem } from './FolderPickerTab';
|
||||||
|
|
||||||
interface CellProps {
|
interface CellProps {
|
||||||
columnIndex: number;
|
cards: ResourceItem[];
|
||||||
rowIndex: number;
|
columnCount: number;
|
||||||
style: CSSProperties;
|
onChange: (value: string) => void;
|
||||||
data: {
|
selected?: string;
|
||||||
cards: ResourceItem[];
|
|
||||||
columnCount: number;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
selected?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoizedCell = memo(function Cell(props: CellProps) {
|
function Cell(props: CellComponentProps<CellProps>) {
|
||||||
const { columnIndex, rowIndex, style, data } = props;
|
const { columnIndex, rowIndex, style, cards, columnCount, onChange, selected } = props;
|
||||||
const { cards, columnCount, onChange, selected } = data;
|
|
||||||
const singleColumnIndex = columnIndex + rowIndex * columnCount;
|
const singleColumnIndex = columnIndex + rowIndex * columnCount;
|
||||||
const card = cards[singleColumnIndex];
|
const card = cards[singleColumnIndex];
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@@ -36,7 +29,7 @@ const MemoizedCell = memo(function Cell(props: CellProps) {
|
|||||||
key={card.value}
|
key={card.value}
|
||||||
className={selected === card.value ? cx(styles.card, styles.selected) : styles.card}
|
className={selected === card.value ? cx(styles.card, styles.selected) : styles.card}
|
||||||
onClick={() => onChange(card.value)}
|
onClick={() => onChange(card.value)}
|
||||||
onKeyDown={(e: React.KeyboardEvent) => {
|
onKeyDown={(e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
onChange(card.value);
|
onChange(card.value);
|
||||||
}
|
}
|
||||||
@@ -54,7 +47,7 @@ const MemoizedCell = memo(function Cell(props: CellProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, areEqual);
|
}
|
||||||
|
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
@@ -75,17 +68,20 @@ export const ResourceCards = (props: CardProps) => {
|
|||||||
const rowCount = Math.ceil(cards.length / columnCount);
|
const rowCount = Math.ceil(cards.length / columnCount);
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
width={width}
|
style={{
|
||||||
height={height}
|
height,
|
||||||
|
maxHeight: height,
|
||||||
|
maxWidth: width,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
columnCount={columnCount}
|
columnCount={columnCount}
|
||||||
columnWidth={cardWidth}
|
columnWidth={cardWidth}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
rowHeight={cardHeight}
|
rowHeight={cardHeight}
|
||||||
itemData={{ cards, columnCount, onChange, selected: value }}
|
cellProps={{ cards, columnCount, onChange, selected: value }}
|
||||||
className={styles.grid}
|
className={styles.grid}
|
||||||
>
|
cellComponent={Cell}
|
||||||
{MemoizedCell}
|
/>
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useEffect, useId, useRef, useState } from 'react';
|
import { useId, useState } from 'react';
|
||||||
import { useWindowSize } from 'react-use';
|
import { useWindowSize } from 'react-use';
|
||||||
import { VariableSizeList as List } from 'react-window';
|
import { List } from 'react-window';
|
||||||
|
|
||||||
import { DataFrame, Field as DataFrameField } from '@grafana/data';
|
import { DataFrame, Field as DataFrameField } from '@grafana/data';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
@@ -59,6 +59,24 @@ const styles = {
|
|||||||
const mobileWidthThreshold = 480;
|
const mobileWidthThreshold = 480;
|
||||||
const numberOfColumnsBeforeExpandedViewIsDefault = 2;
|
const numberOfColumnsBeforeExpandedViewIsDefault = 2;
|
||||||
|
|
||||||
|
interface RowProps {
|
||||||
|
isExpandedView: boolean;
|
||||||
|
valueLabels: DataFrameField[];
|
||||||
|
items: instantQueryRawVirtualizedListData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListItemHeight(itemIndex: number, { isExpandedView, items, valueLabels }: RowProps) {
|
||||||
|
const singleLineHeight = 32;
|
||||||
|
const additionalLineHeight = 22;
|
||||||
|
if (!isExpandedView) {
|
||||||
|
return singleLineHeight;
|
||||||
|
}
|
||||||
|
const item = items[itemIndex];
|
||||||
|
|
||||||
|
// Height of 1.5 lines, plus the number of non-value attributes times the height of additional lines
|
||||||
|
return 1.5 * singleLineHeight + (Object.keys(item).length - valueLabels.length) * additionalLineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The container that provides the virtualized list to the child components
|
* The container that provides the virtualized list to the child components
|
||||||
* @param props
|
* @param props
|
||||||
@@ -67,7 +85,6 @@ const numberOfColumnsBeforeExpandedViewIsDefault = 2;
|
|||||||
const RawListContainer = (props: RawListContainerProps) => {
|
const RawListContainer = (props: RawListContainerProps) => {
|
||||||
const { tableResult } = props;
|
const { tableResult } = props;
|
||||||
const dataFrame = cloneDeep(tableResult);
|
const dataFrame = cloneDeep(tableResult);
|
||||||
const listRef = useRef<List | null>(null);
|
|
||||||
|
|
||||||
const valueLabels = dataFrame.fields.filter((field) => field.name.includes('Value'));
|
const valueLabels = dataFrame.fields.filter((field) => field.name.includes('Value'));
|
||||||
const items = getRawPrometheusListItemsFromDataFrame(dataFrame);
|
const items = getRawPrometheusListItemsFromDataFrame(dataFrame);
|
||||||
@@ -84,11 +101,6 @@ const RawListContainer = (props: RawListContainerProps) => {
|
|||||||
reportInteraction('grafana_explore_prometheus_instant_query_ui_raw_toggle_expand', props);
|
reportInteraction('grafana_explore_prometheus_instant_query_ui_raw_toggle_expand', props);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// After the expanded view has updated, tell the list to re-render
|
|
||||||
listRef.current?.resetAfterIndex(0, true);
|
|
||||||
}, [isExpandedView]);
|
|
||||||
|
|
||||||
const calculateInitialHeight = (length: number): number => {
|
const calculateInitialHeight = (length: number): number => {
|
||||||
const maxListHeight = 600;
|
const maxListHeight = 600;
|
||||||
const shortListLength = 10;
|
const shortListLength = 10;
|
||||||
@@ -96,7 +108,11 @@ const RawListContainer = (props: RawListContainerProps) => {
|
|||||||
if (length < shortListLength) {
|
if (length < shortListLength) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
sum += getListItemHeight(i, true);
|
sum += getListItemHeight(i, {
|
||||||
|
isExpandedView: true,
|
||||||
|
items,
|
||||||
|
valueLabels,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(maxListHeight, sum);
|
return Math.min(maxListHeight, sum);
|
||||||
@@ -105,18 +121,6 @@ const RawListContainer = (props: RawListContainerProps) => {
|
|||||||
return maxListHeight;
|
return maxListHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getListItemHeight = (itemIndex: number, isExpandedView: boolean) => {
|
|
||||||
const singleLineHeight = 32;
|
|
||||||
const additionalLineHeight = 22;
|
|
||||||
if (!isExpandedView) {
|
|
||||||
return singleLineHeight;
|
|
||||||
}
|
|
||||||
const item = items[itemIndex];
|
|
||||||
|
|
||||||
// Height of 1.5 lines, plus the number of non-value attributes times the height of additional lines
|
|
||||||
return 1.5 * singleLineHeight + (Object.keys(item).length - valueLabels.length) * additionalLineHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchId = `isExpandedView ${useId()}`;
|
const switchId = `isExpandedView ${useId()}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -153,14 +157,19 @@ const RawListContainer = (props: RawListContainerProps) => {
|
|||||||
<ItemLabels valueLabels={valueLabels} expanded={isExpandedView} />
|
<ItemLabels valueLabels={valueLabels} expanded={isExpandedView} />
|
||||||
)}
|
)}
|
||||||
<List
|
<List
|
||||||
ref={listRef}
|
|
||||||
itemCount={items.length}
|
|
||||||
className={styles.wrapper}
|
className={styles.wrapper}
|
||||||
itemSize={(index) => getListItemHeight(index, isExpandedView)}
|
style={{
|
||||||
height={calculateInitialHeight(items.length)}
|
height: calculateInitialHeight(items.length),
|
||||||
width="100%"
|
width: '100%',
|
||||||
>
|
}}
|
||||||
{({ index, style }) => {
|
rowCount={items.length}
|
||||||
|
rowHeight={getListItemHeight}
|
||||||
|
rowProps={{
|
||||||
|
isExpandedView,
|
||||||
|
valueLabels,
|
||||||
|
items,
|
||||||
|
}}
|
||||||
|
rowComponent={({ index, style }) => {
|
||||||
let filteredValueLabels: DataFrameField[] | undefined;
|
let filteredValueLabels: DataFrameField[] | undefined;
|
||||||
if (isExpandedView) {
|
if (isExpandedView) {
|
||||||
filteredValueLabels = valueLabels.filter((valueLabel) => {
|
filteredValueLabels = valueLabels.filter((valueLabel) => {
|
||||||
@@ -181,7 +190,7 @@ const RawListContainer = (props: RawListContainerProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</List>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
import { VariableSizeList } from 'react-window';
|
import { List } from 'react-window';
|
||||||
|
|
||||||
import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data';
|
import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data';
|
||||||
import { LogsSortOrder } from '@grafana/schema';
|
import { LogsSortOrder } from '@grafana/schema';
|
||||||
@@ -89,19 +89,19 @@ function setup(
|
|||||||
loadMore={loadMoreMock}
|
loadMore={loadMoreMock}
|
||||||
infiniteScrollMode={infiniteScrollMode}
|
infiniteScrollMode={infiniteScrollMode}
|
||||||
>
|
>
|
||||||
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
|
{({ itemCount, onItemsRendered, Renderer }) => (
|
||||||
<VariableSizeList
|
<List
|
||||||
height={100}
|
rowComponent={Renderer}
|
||||||
itemCount={itemCount}
|
rowCount={itemCount}
|
||||||
itemSize={() => virtualization.getLineHeight()}
|
rowProps={{}}
|
||||||
itemKey={getItemKey}
|
rowHeight={() => virtualization.getLineHeight()}
|
||||||
layout="vertical"
|
onRowsRendered={onItemsRendered}
|
||||||
onItemsRendered={onItemsRendered}
|
style={{
|
||||||
style={{ overflow: 'scroll' }}
|
overflow: 'scroll',
|
||||||
width="100%"
|
height: 100,
|
||||||
>
|
width: '100%',
|
||||||
{Renderer}
|
}}
|
||||||
</VariableSizeList>
|
/>
|
||||||
)}
|
)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent } from 'react';
|
import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent, type JSX } from 'react';
|
||||||
import { usePrevious } from 'react-use';
|
import { usePrevious } from 'react-use';
|
||||||
import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window';
|
import { type RowComponentProps, type ListProps } from 'react-window';
|
||||||
|
|
||||||
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
|
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
@@ -17,8 +17,8 @@ import { LogLineVirtualization } from './virtualization';
|
|||||||
interface ChildrenProps {
|
interface ChildrenProps {
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
getItemKey: (index: number) => string;
|
getItemKey: (index: number) => string;
|
||||||
onItemsRendered: (props: ListOnItemsRenderedProps) => void;
|
onItemsRendered: ListProps<{}>['onRowsRendered'];
|
||||||
Renderer: (props: ListChildComponentProps) => ReactNode;
|
Renderer: (props: RowComponentProps) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -200,7 +200,7 @@ export const InfiniteScroll = ({
|
|||||||
}, [onLoadMore]);
|
}, [onLoadMore]);
|
||||||
|
|
||||||
const Renderer = useCallback(
|
const Renderer = useCallback(
|
||||||
({ index, style }: ListChildComponentProps) => {
|
({ index, style }: RowComponentProps) => {
|
||||||
if (!logs[index] && infiniteLoaderState !== 'idle') {
|
if (!logs[index] && infiniteLoaderState !== 'idle') {
|
||||||
return (
|
return (
|
||||||
<LogLineMessage
|
<LogLineMessage
|
||||||
@@ -248,12 +248,12 @@ export const InfiniteScroll = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onItemsRendered = useCallback(
|
const onItemsRendered = useCallback<NonNullable<ListProps<{}>['onRowsRendered']>>(
|
||||||
(props: ListOnItemsRenderedProps) => {
|
(props) => {
|
||||||
if (!scrollElement) {
|
if (!scrollElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props.visibleStartIndex === 0) {
|
if (props.startIndex === 0) {
|
||||||
noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight;
|
noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight;
|
||||||
}
|
}
|
||||||
if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
|
if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
|
||||||
@@ -261,9 +261,9 @@ export const InfiniteScroll = ({
|
|||||||
}
|
}
|
||||||
const lastLogIndex = logs.length - 1;
|
const lastLogIndex = logs.length - 1;
|
||||||
const preScrollIndex = logs.length - 2;
|
const preScrollIndex = logs.length - 2;
|
||||||
if (props.visibleStopIndex >= lastLogIndex) {
|
if (props.stopIndex >= lastLogIndex) {
|
||||||
setInfiniteLoaderState('pre-scroll-bottom');
|
setInfiniteLoaderState('pre-scroll-bottom');
|
||||||
} else if (props.visibleStartIndex < preScrollIndex) {
|
} else if (props.startIndex < preScrollIndex) {
|
||||||
setInfiniteLoaderState('idle');
|
setInfiniteLoaderState('idle');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { Grammar } from 'prismjs';
|
import { Grammar } from 'prismjs';
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react';
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react';
|
||||||
import { Align, VariableSizeList } from 'react-window';
|
import { Align, List, ListImperativeAPI, useListRef } from 'react-window';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CoreApp,
|
CoreApp,
|
||||||
@@ -282,10 +282,9 @@ const LogListComponent = ({
|
|||||||
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
||||||
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
|
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const listRef = useRef<VariableSizeList | null>(null);
|
const listRef = useListRef(null);
|
||||||
const widthRef = useRef(containerElement.clientWidth);
|
const widthRef = useRef(containerElement.clientWidth);
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]);
|
const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]);
|
||||||
const dimensions = useMemo(
|
const dimensions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -329,25 +328,29 @@ const LogListComponent = ({
|
|||||||
|
|
||||||
// When log lines report size discrepancies, we debounce the calculation reset to give time to
|
// When log lines report size discrepancies, we debounce the calculation reset to give time to
|
||||||
// use the smallest log index to reset the heights.
|
// use the smallest log index to reset the heights.
|
||||||
const debouncedResetAfterIndex = useMemo(() => {
|
// TODO do we need this?
|
||||||
return debounce((index: number) => {
|
// const debouncedResetAfterIndex = useMemo(() => {
|
||||||
listRef.current?.resetAfterIndex(index);
|
// return debounce((index: number) => {
|
||||||
overflowIndexRef.current = Infinity;
|
// listRef.current?.resetAfterIndex(index);
|
||||||
}, 0);
|
// overflowIndexRef.current = Infinity;
|
||||||
}, []);
|
// }, 0);
|
||||||
|
// }, []);
|
||||||
|
|
||||||
const debouncedScrollToItem = useMemo(() => {
|
const debouncedScrollToItem = useMemo(() => {
|
||||||
return debounce((index: number, align?: Align) => {
|
return debounce((index: number, align?: Align) => {
|
||||||
listRef.current?.scrollToItem(index, align);
|
listRef.current?.scrollToRow({
|
||||||
|
index,
|
||||||
|
align,
|
||||||
|
});
|
||||||
}, 250);
|
}, 250);
|
||||||
}, []);
|
}, [listRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
||||||
handleScrollToEvent(e, filteredLogs, listRef.current)
|
handleScrollToEvent(e, filteredLogs, listRef.current)
|
||||||
);
|
);
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [eventBus, filteredLogs]);
|
}, [eventBus, filteredLogs, listRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProcessedLogs(
|
setProcessedLogs(
|
||||||
@@ -366,11 +369,13 @@ const LogListComponent = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
virtualization.resetLogLineSizes();
|
virtualization.resetLogLineSizes();
|
||||||
listRef.current?.resetAfterIndex(0);
|
// TODO do we need this?
|
||||||
|
// listRef.current?.resetAfterIndex(0);
|
||||||
}, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]);
|
}, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listRef.current?.resetAfterIndex(0);
|
// TODO do we need this?
|
||||||
|
// listRef.current?.resetAfterIndex(0);
|
||||||
}, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]);
|
}, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -399,9 +404,10 @@ const LogListComponent = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current;
|
overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current;
|
||||||
debouncedResetAfterIndex(overflowIndexRef.current);
|
// TODO do we need this?
|
||||||
|
// debouncedResetAfterIndex(overflowIndexRef.current);
|
||||||
},
|
},
|
||||||
[debouncedResetAfterIndex, virtualization, widthContainer]
|
[virtualization, widthContainer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleScrollPosition = useCallback(
|
const handleScrollPosition = useCallback(
|
||||||
@@ -410,13 +416,13 @@ const LogListComponent = ({
|
|||||||
if (scrollToUID) {
|
if (scrollToUID) {
|
||||||
const index = processedLogs.findIndex((log) => log.uid === scrollToUID);
|
const index = processedLogs.findIndex((log) => log.uid === scrollToUID);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
listRef.current?.scrollToItem(index, 'start');
|
listRef.current?.scrollToRow({ index, align: 'start' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : processedLogs.length - 1);
|
listRef.current?.scrollToRow({ index: initialScrollPosition === 'top' ? 0 : processedLogs.length - 1 });
|
||||||
},
|
},
|
||||||
[initialScrollPosition, permalinkedLogId, processedLogs]
|
[initialScrollPosition, permalinkedLogId, processedLogs, listRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLogLineClick = useCallback(
|
const handleLogLineClick = useCallback(
|
||||||
@@ -503,7 +509,7 @@ const LogListComponent = ({
|
|||||||
logs={filteredLogs}
|
logs={filteredLogs}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
onClick={handleLogLineClick}
|
onClick={handleLogLineClick}
|
||||||
scrollElement={scrollRef.current}
|
scrollElement={listRef.current?.element ?? null}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
@@ -512,12 +518,25 @@ const LogListComponent = ({
|
|||||||
virtualization={virtualization}
|
virtualization={virtualization}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
>
|
>
|
||||||
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
|
{({ itemCount, onItemsRendered, Renderer }) => (
|
||||||
<VariableSizeList
|
<List
|
||||||
className={styles.logList}
|
className={styles.logList}
|
||||||
height={listHeight}
|
style={
|
||||||
itemCount={itemCount}
|
wrapLogMessage
|
||||||
itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, {
|
? {
|
||||||
|
height: listHeight,
|
||||||
|
width: '100%',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
height: listHeight,
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'scroll',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowProps={{}}
|
||||||
|
rowCount={itemCount}
|
||||||
|
rowHeight={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, {
|
||||||
detailsMode,
|
detailsMode,
|
||||||
hasLogsWithErrors,
|
hasLogsWithErrors,
|
||||||
hasSampledLogs,
|
hasSampledLogs,
|
||||||
@@ -526,17 +545,11 @@ const LogListComponent = ({
|
|||||||
showTime,
|
showTime,
|
||||||
wrap: wrapLogMessage,
|
wrap: wrapLogMessage,
|
||||||
})}
|
})}
|
||||||
itemKey={getItemKey}
|
rowComponent={Renderer}
|
||||||
layout="vertical"
|
onRowsRendered={onItemsRendered}
|
||||||
onItemsRendered={onItemsRendered}
|
|
||||||
outerRef={scrollRef}
|
|
||||||
overscanCount={5}
|
overscanCount={5}
|
||||||
ref={listRef}
|
listRef={listRef}
|
||||||
style={wrapLogMessage ? { overflowY: 'scroll' } : { overflow: 'scroll' }}
|
/>
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
{Renderer}
|
|
||||||
</VariableSizeList>
|
|
||||||
)}
|
)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
</div>
|
</div>
|
||||||
@@ -586,16 +599,23 @@ function getStyles(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: VariableSizeList | null) {
|
function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: ListImperativeAPI | null) {
|
||||||
if (event.payload.scrollTo === 'top') {
|
if (event.payload.scrollTo === 'top') {
|
||||||
list?.scrollTo(0);
|
list?.scrollToRow({
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
} else if (event.payload.scrollTo === 'bottom') {
|
} else if (event.payload.scrollTo === 'bottom') {
|
||||||
list?.scrollToItem(logs.length - 1);
|
list?.scrollToRow({
|
||||||
|
index: logs.length - 1,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// uid
|
// uid
|
||||||
const index = logs.findIndex((log) => log.uid === event.payload.scrollTo);
|
const index = logs.findIndex((log) => log.uid === event.payload.scrollTo);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
list?.scrollToItem(index, 'center');
|
list?.scrollToRow({
|
||||||
|
index,
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ChangeEvent, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { ChangeEvent, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { VariableSizeList } from 'react-window';
|
import { type ListImperativeAPI } from 'react-window';
|
||||||
|
|
||||||
import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data';
|
import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
@@ -12,7 +12,7 @@ import { useLogListSearchContext } from './LogListSearchContext';
|
|||||||
import { LogListModel } from './processing';
|
import { LogListModel } from './processing';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
listRef: VariableSizeList | null;
|
listRef: ListImperativeAPI | null;
|
||||||
logs: LogListModel[];
|
logs: LogListModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
|||||||
}
|
}
|
||||||
const prev = currentResult > 0 ? currentResult - 1 : matches.length - 1;
|
const prev = currentResult > 0 ? currentResult - 1 : matches.length - 1;
|
||||||
setCurrentResult(prev);
|
setCurrentResult(prev);
|
||||||
listRef?.scrollToItem(logs.indexOf(matches[prev]), 'center');
|
listRef?.scrollToRow({
|
||||||
|
index: logs.indexOf(matches[prev]),
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
}, [currentResult, listRef, logs, matches]);
|
}, [currentResult, listRef, logs, matches]);
|
||||||
|
|
||||||
const nextResult = useCallback(() => {
|
const nextResult = useCallback(() => {
|
||||||
@@ -70,7 +73,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
|||||||
}
|
}
|
||||||
const next = currentResult < matches.length - 1 ? currentResult + 1 : 0;
|
const next = currentResult < matches.length - 1 ? currentResult + 1 : 0;
|
||||||
setCurrentResult(next);
|
setCurrentResult(next);
|
||||||
listRef?.scrollToItem(logs.indexOf(matches[next]), 'center');
|
listRef?.scrollToRow({
|
||||||
|
index: logs.indexOf(matches[next]),
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
}, [currentResult, listRef, logs, matches]);
|
}, [currentResult, listRef, logs, matches]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -80,7 +86,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => {
|
|||||||
}
|
}
|
||||||
if (!currentResult) {
|
if (!currentResult) {
|
||||||
setCurrentResult(0);
|
setCurrentResult(0);
|
||||||
listRef?.scrollToItem(logs.indexOf(matches[0]), 'center');
|
listRef?.scrollToRow({
|
||||||
|
index: logs.indexOf(matches[0]),
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentResult, listRef, logs, matches]);
|
}, [currentResult, listRef, logs, matches]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useEffect, useMemo, useRef, useCallback, useState, CSSProperties } from 'react';
|
import { useMemo, useCallback, useEffect } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useTable, Column, TableOptions, Cell } from 'react-table';
|
import { useTable, Column, TableOptions, Cell } from 'react-table';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List, useListRef, type RowComponentProps } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import { useInfiniteLoader } from 'react-window-infinite-loader';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { Field, GrafanaTheme2 } from '@grafana/data';
|
import { Field, GrafanaTheme2 } from '@grafana/data';
|
||||||
@@ -57,8 +57,7 @@ export const SearchResultsTable = React.memo(
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const columnStyles = useStyles2(getColumnStyles);
|
const columnStyles = useStyles2(getColumnStyles);
|
||||||
const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Sm);
|
const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Sm);
|
||||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
const listRef = useListRef(null);
|
||||||
const [listEl, setListEl] = useState<FixedSizeList | null>(null);
|
|
||||||
const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);
|
const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);
|
||||||
|
|
||||||
const memoizedData = useMemo(() => {
|
const memoizedData = useMemo(() => {
|
||||||
@@ -72,15 +71,13 @@ export const SearchResultsTable = React.memo(
|
|||||||
return Array(response.totalRows).fill(0);
|
return Array(response.totalRows).fill(0);
|
||||||
}, [response]);
|
}, [response]);
|
||||||
|
|
||||||
// Scroll to the top and clear loader cache when the query results change
|
// Scroll to the top when the query results change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (infiniteLoaderRef.current) {
|
const list = listRef.current;
|
||||||
infiniteLoaderRef.current.resetloadMoreItemsCache();
|
list?.scrollToRow({
|
||||||
}
|
index: 0,
|
||||||
if (listEl) {
|
});
|
||||||
listEl.scrollTo(0);
|
}, [listRef, memoizedData]);
|
||||||
}
|
|
||||||
}, [memoizedData, listEl]);
|
|
||||||
|
|
||||||
// React-table column definitions
|
// React-table column definitions
|
||||||
const memoizedColumns = useMemo(() => {
|
const memoizedColumns = useMemo(() => {
|
||||||
@@ -129,8 +126,14 @@ export const SearchResultsTable = React.memo(
|
|||||||
[response, selection, selectionToggle]
|
[response, selection, selectionToggle]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onRowsRendered = useInfiniteLoader({
|
||||||
|
rowCount: rows.length,
|
||||||
|
isRowLoaded: response.isItemLoaded,
|
||||||
|
loadMoreRows: handleLoadMore,
|
||||||
|
});
|
||||||
|
|
||||||
const RenderRow = useCallback(
|
const RenderRow = useCallback(
|
||||||
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
|
({ index: rowIndex, style }: RowComponentProps) => {
|
||||||
const row = rows[rowIndex];
|
const row = rows[rowIndex];
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
|
|
||||||
@@ -225,29 +228,19 @@ export const SearchResultsTable = React.memo(
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
<div {...getTableBodyProps()}>
|
<div {...getTableBodyProps()}>
|
||||||
<InfiniteLoader
|
<List
|
||||||
ref={infiniteLoaderRef}
|
rowProps={{}}
|
||||||
isItemLoaded={response.isItemLoaded}
|
listRef={listRef}
|
||||||
itemCount={rows.length}
|
rowComponent={RenderRow}
|
||||||
loadMoreItems={handleLoadMore}
|
rowCount={rows.length}
|
||||||
>
|
onRowsRendered={onRowsRendered}
|
||||||
{({ onItemsRendered, ref }) => (
|
rowHeight={tableStyles.rowHeight}
|
||||||
<FixedSizeList
|
style={{
|
||||||
ref={(innerRef) => {
|
height: height - ROW_HEIGHT,
|
||||||
ref(innerRef);
|
width: width,
|
||||||
setListEl(innerRef);
|
overflow: 'hidden auto',
|
||||||
}}
|
}}
|
||||||
onItemsRendered={onItemsRendered}
|
/>
|
||||||
height={height - ROW_HEIGHT}
|
|
||||||
itemCount={rows.length}
|
|
||||||
itemSize={tableStyles.rowHeight}
|
|
||||||
width={width}
|
|
||||||
style={{ overflow: 'hidden auto' }}
|
|
||||||
>
|
|
||||||
{RenderRow}
|
|
||||||
</FixedSizeList>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-select": "5.10.2",
|
"react-select": "5.10.2",
|
||||||
"react-window": "1.8.11",
|
"react-window": "2.2.3",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.18",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react-window": "1.8.8",
|
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
|
|||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { List } from 'react-window';
|
||||||
|
|
||||||
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
|
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
@@ -505,18 +505,19 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
|
|||||||
onClick={this.onClickLabel}
|
onClick={this.onClickLabel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FixedSizeList
|
<List
|
||||||
height={200}
|
rowCount={label.values?.length || 0}
|
||||||
itemCount={label.values?.length || 0}
|
rowHeight={28}
|
||||||
itemSize={28}
|
rowProps={{}}
|
||||||
itemKey={(i) => label.values?.[i].name ?? i}
|
style={{
|
||||||
width={200}
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
}}
|
||||||
className={styles.valueList}
|
className={styles.valueList}
|
||||||
>
|
rowComponent={({ index, style }) => {
|
||||||
{({ index, style }) => {
|
|
||||||
const value = label.values?.[index];
|
const value = label.values?.[index];
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return <></>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
@@ -531,7 +532,7 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</FixedSizeList>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
70
yarn.lock
70
yarn.lock
@@ -1473,7 +1473,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7":
|
"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7":
|
||||||
version: 7.28.4
|
version: 7.28.4
|
||||||
resolution: "@babel/runtime@npm:7.28.4"
|
resolution: "@babel/runtime@npm:7.28.4"
|
||||||
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
|
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
|
||||||
@@ -2654,7 +2654,6 @@ __metadata:
|
|||||||
"@types/node": "npm:24.10.1"
|
"@types/node": "npm:24.10.1"
|
||||||
"@types/react": "npm:18.3.18"
|
"@types/react": "npm:18.3.18"
|
||||||
"@types/react-dom": "npm:18.3.5"
|
"@types/react-dom": "npm:18.3.5"
|
||||||
"@types/react-window": "npm:1.8.8"
|
|
||||||
"@types/uuid": "npm:10.0.0"
|
"@types/uuid": "npm:10.0.0"
|
||||||
jest: "npm:29.7.0"
|
jest: "npm:29.7.0"
|
||||||
lodash: "npm:4.17.21"
|
lodash: "npm:4.17.21"
|
||||||
@@ -2662,7 +2661,7 @@ __metadata:
|
|||||||
react: "npm:18.3.1"
|
react: "npm:18.3.1"
|
||||||
react-dom: "npm:18.3.1"
|
react-dom: "npm:18.3.1"
|
||||||
react-select: "npm:5.10.2"
|
react-select: "npm:5.10.2"
|
||||||
react-window: "npm:1.8.11"
|
react-window: "npm:2.2.3"
|
||||||
rxjs: "npm:7.8.2"
|
rxjs: "npm:7.8.2"
|
||||||
stream-browserify: "npm:3.0.0"
|
stream-browserify: "npm:3.0.0"
|
||||||
ts-node: "npm:10.9.2"
|
ts-node: "npm:10.9.2"
|
||||||
@@ -3522,7 +3521,6 @@ __metadata:
|
|||||||
"@types/react": "npm:18.3.18"
|
"@types/react": "npm:18.3.18"
|
||||||
"@types/react-dom": "npm:18.3.5"
|
"@types/react-dom": "npm:18.3.5"
|
||||||
"@types/react-highlight-words": "npm:0.20.0"
|
"@types/react-highlight-words": "npm:0.20.0"
|
||||||
"@types/react-window": "npm:1.8.8"
|
|
||||||
"@types/semver": "npm:7.7.1"
|
"@types/semver": "npm:7.7.1"
|
||||||
"@types/uuid": "npm:10.0.0"
|
"@types/uuid": "npm:10.0.0"
|
||||||
debounce-promise: "npm:3.1.2"
|
debounce-promise: "npm:3.1.2"
|
||||||
@@ -3542,7 +3540,7 @@ __metadata:
|
|||||||
react-highlight-words: "npm:0.21.0"
|
react-highlight-words: "npm:0.21.0"
|
||||||
react-select-event: "npm:5.5.1"
|
react-select-event: "npm:5.5.1"
|
||||||
react-use: "npm:17.6.0"
|
react-use: "npm:17.6.0"
|
||||||
react-window: "npm:1.8.11"
|
react-window: "npm:2.2.3"
|
||||||
rimraf: "npm:6.0.1"
|
rimraf: "npm:6.0.1"
|
||||||
rollup: "npm:^4.22.4"
|
rollup: "npm:^4.22.4"
|
||||||
rollup-plugin-esbuild: "npm:6.2.1"
|
rollup-plugin-esbuild: "npm:6.2.1"
|
||||||
@@ -3825,7 +3823,6 @@ __metadata:
|
|||||||
"@types/react-highlight-words": "npm:0.20.0"
|
"@types/react-highlight-words": "npm:0.20.0"
|
||||||
"@types/react-table": "npm:7.7.20"
|
"@types/react-table": "npm:7.7.20"
|
||||||
"@types/react-transition-group": "npm:4.4.12"
|
"@types/react-transition-group": "npm:4.4.12"
|
||||||
"@types/react-window": "npm:1.8.8"
|
|
||||||
"@types/slate": "npm:0.47.11"
|
"@types/slate": "npm:0.47.11"
|
||||||
"@types/slate-plain-serializer": "npm:0.7.5"
|
"@types/slate-plain-serializer": "npm:0.7.5"
|
||||||
"@types/slate-react": "npm:0.22.9"
|
"@types/slate-react": "npm:0.22.9"
|
||||||
@@ -3885,7 +3882,7 @@ __metadata:
|
|||||||
react-table: "npm:7.8.0"
|
react-table: "npm:7.8.0"
|
||||||
react-transition-group: "npm:4.4.5"
|
react-transition-group: "npm:4.4.5"
|
||||||
react-use: "npm:17.6.0"
|
react-use: "npm:17.6.0"
|
||||||
react-window: "npm:1.8.11"
|
react-window: "npm:2.2.3"
|
||||||
rimraf: "npm:6.0.1"
|
rimraf: "npm:6.0.1"
|
||||||
rollup: "npm:^4.22.4"
|
rollup: "npm:^4.22.4"
|
||||||
rollup-plugin-copy: "npm:3.5.0"
|
rollup-plugin-copy: "npm:3.5.0"
|
||||||
@@ -10727,25 +10724,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-window-infinite-loader@npm:^1":
|
|
||||||
version: 1.0.9
|
|
||||||
resolution: "@types/react-window-infinite-loader@npm:1.0.9"
|
|
||||||
dependencies:
|
|
||||||
"@types/react": "npm:*"
|
|
||||||
"@types/react-window": "npm:*"
|
|
||||||
checksum: 10/9f2c27f24bfa726ceaef6612a4adbda745f3455c877193f68dfa48591274c670a6df4fa6870785cff5f948e289ceb9a247fb7cbf67e3cd555ab16d11866fd63f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react-window@npm:*, @types/react-window@npm:1.8.8":
|
|
||||||
version: 1.8.8
|
|
||||||
resolution: "@types/react-window@npm:1.8.8"
|
|
||||||
dependencies:
|
|
||||||
"@types/react": "npm:*"
|
|
||||||
checksum: 10/79b70b7c33161efb14bf69115792843de8e038594136a8373cfbbcc4066c49fd611dd2d3592a9a81d19d21c075bf14e5e73a64f4d9ad32e45d4d5493f5f53918
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react@npm:*, @types/react@npm:18.3.18":
|
"@types/react@npm:*, @types/react@npm:18.3.18":
|
||||||
version: 18.3.18
|
version: 18.3.18
|
||||||
resolution: "@types/react@npm:18.3.18"
|
resolution: "@types/react@npm:18.3.18"
|
||||||
@@ -19319,8 +19297,6 @@ __metadata:
|
|||||||
"@types/react-table": "npm:7.7.20"
|
"@types/react-table": "npm:7.7.20"
|
||||||
"@types/react-transition-group": "npm:4.4.12"
|
"@types/react-transition-group": "npm:4.4.12"
|
||||||
"@types/react-virtualized-auto-sizer": "npm:1.0.8"
|
"@types/react-virtualized-auto-sizer": "npm:1.0.8"
|
||||||
"@types/react-window": "npm:1.8.8"
|
|
||||||
"@types/react-window-infinite-loader": "npm:^1"
|
|
||||||
"@types/redux-mock-store": "npm:1.5.0"
|
"@types/redux-mock-store": "npm:1.5.0"
|
||||||
"@types/semver": "npm:7.7.1"
|
"@types/semver": "npm:7.7.1"
|
||||||
"@types/slate": "npm:0.47.11"
|
"@types/slate": "npm:0.47.11"
|
||||||
@@ -19500,8 +19476,8 @@ __metadata:
|
|||||||
react-use: "npm:17.6.0"
|
react-use: "npm:17.6.0"
|
||||||
react-virtual: "npm:2.10.4"
|
react-virtual: "npm:2.10.4"
|
||||||
react-virtualized-auto-sizer: "npm:1.0.26"
|
react-virtualized-auto-sizer: "npm:1.0.26"
|
||||||
react-window: "npm:1.8.11"
|
react-window: "npm:2.2.3"
|
||||||
react-window-infinite-loader: "npm:1.0.10"
|
react-window-infinite-loader: "npm:2.0.0"
|
||||||
reduce-reducers: "npm:^1.0.4"
|
reduce-reducers: "npm:^1.0.4"
|
||||||
redux: "npm:5.0.1"
|
redux: "npm:5.0.1"
|
||||||
redux-mock-store: "npm:1.5.5"
|
redux-mock-store: "npm:1.5.5"
|
||||||
@@ -23894,13 +23870,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"memoize-one@npm:>=3.1.1 <6":
|
|
||||||
version: 5.2.1
|
|
||||||
resolution: "memoize-one@npm:5.2.1"
|
|
||||||
checksum: 10/b7141dc148b5c6fdd51e77ecf0421fd2581681eb8756e0b3dfbd4fe765b5e2b5a6bc90214bb6f19a96b6aed44de17eda3407142a7be9e24ccd0774bbd9874d1b
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"memoize-one@npm:^4.0.0":
|
"memoize-one@npm:^4.0.0":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "memoize-one@npm:4.0.3"
|
resolution: "memoize-one@npm:4.0.3"
|
||||||
@@ -28964,26 +28933,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-window-infinite-loader@npm:1.0.10":
|
"react-window-infinite-loader@npm:2.0.0":
|
||||||
version: 1.0.10
|
version: 2.0.0
|
||||||
resolution: "react-window-infinite-loader@npm:1.0.10"
|
resolution: "react-window-infinite-loader@npm:2.0.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
checksum: 10/4f4c097a2948f8da71d13199289d85c89720e41888ea50039d2b8e6d7cf160300e97f66421c53420d8cb95b3ece74940c07ebd23bddd7a705a679d7e2dc2957e
|
checksum: 10/a67ba08dbdb557a46390bf1056ad01d09202fc76492d27818429ac8f9e893792c3476a584fadfc7ae3f2766e1986468658356105000d7b31034287012e7a89dc
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-window@npm:1.8.11":
|
"react-window@npm:2.2.3":
|
||||||
version: 1.8.11
|
version: 2.2.3
|
||||||
resolution: "react-window@npm:1.8.11"
|
resolution: "react-window@npm:2.2.3"
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": "npm:^7.0.0"
|
|
||||||
memoize-one: "npm:>=3.1.1 <6"
|
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
checksum: 10/bdbac2b664c5a799443b97a32b2f60a00cc13cc14ca8a8b1e81e2dc7dd00d8d54f05743113972fe1a641b57ada5d874b59c3cbe7e8a07a88c6713a0fb65d60f6
|
checksum: 10/f676883c6cd16a0f56690b6d9c929a3da8b7d0ccf78dd5247f474b65ead38aae32a328fcd24c95956d1541383b774fca2b6decae65b6991697a90168222de86d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user