@grafana/ui
Version:
Grafana Components Library
389 lines (386 loc) • 13.8 kB
JavaScript
import { useState, useMemo, useLayoutEffect, useRef, useCallback, useEffect } from 'react';
import { formattedValueToString, compareArrayValues, ReducerID, FieldType, reduceField } from '@grafana/data';
import { TABLE } from './constants.mjs';
import { getDisplayName, processNestedTableRows, getColumnTypes, applySort, buildHeaderHeightMeasurers, getRowHeight, buildCellHeightMeasurers, IS_SAFARI_26, computeColWidths } from './utils.mjs';
;
const getDisplayedValue = (row, key, fields) => {
const field = fields.find((field2) => getDisplayName(field2) === key);
if (!field || !field.display) {
return "";
}
const displayedValue = formattedValueToString(field.display(row[key]));
return displayedValue;
};
function useFilteredRows(rows, fields, { hasNestedFrames }) {
const [filter, setFilter] = useState({});
const filterValues = useMemo(() => Object.entries(filter), [filter]);
const crossFilterOrder = useMemo(
() => Array.from(new Set(filterValues.map(([key]) => key))),
[filterValues]
);
const [filteredRows, crossFilterRows] = useMemo(() => {
const crossFilterRows2 = {};
const filterRows = (row) => {
var _a;
for (const [key, value] of filterValues) {
const displayedValue = getDisplayedValue(row, key, fields);
if (!value.filteredSet.has(displayedValue)) {
return false;
}
crossFilterRows2[key] = (_a = crossFilterRows2[key]) != null ? _a : [];
crossFilterRows2[key].push(row);
}
return true;
};
const filteredRows2 = hasNestedFrames ? processNestedTableRows(rows, (parents) => parents.filter(filterRows)) : rows.filter(filterRows);
return [filteredRows2, crossFilterRows2];
}, [filterValues, rows, fields, hasNestedFrames]);
return {
rows: filteredRows,
filter,
setFilter,
crossFilterOrder,
crossFilterRows
};
}
function useSortedRows(rows, fields, { initialSortBy, hasNestedFrames }) {
const initialSortColumns = useMemo(
() => {
var _a;
return (_a = initialSortBy == null ? void 0 : initialSortBy.flatMap(({ displayName, desc }) => {
if (!fields.some((f) => getDisplayName(f) === displayName)) {
return [];
}
return [
{
columnKey: displayName,
direction: desc ? "DESC" : "ASC"
}
];
})) != null ? _a : [];
},
[]
// eslint-disable-line react-hooks/exhaustive-deps
);
const [sortColumns, setSortColumns] = useState(initialSortColumns);
const columnTypes = useMemo(() => getColumnTypes(fields), [fields]);
const sortedRows = useMemo(
() => applySort(rows, fields, sortColumns, columnTypes, hasNestedFrames),
[rows, fields, sortColumns, hasNestedFrames, columnTypes]
);
return {
rows: sortedRows,
sortColumns,
setSortColumns
};
}
const PAGINATION_HEIGHT = 38;
function usePaginatedRows(rows, { height, width, headerHeight, footerHeight, rowHeight, enabled }) {
const [page, setPage] = useState(0);
const numRows = rows.length;
const avgRowHeight = useMemo(() => {
if (!enabled) {
return 0;
}
if (typeof rowHeight === "number") {
return rowHeight;
}
if (typeof rowHeight === "string") {
return TABLE.MAX_CELL_HEIGHT;
}
return rows.slice(0, 100).reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0);
}, [rows, rowHeight, enabled]);
const smallPagination = useMemo(() => enabled && width < TABLE.PAGINATION_LIMIT, [enabled, width]);
const { numPages, rowsPerPage, pageRangeStart, pageRangeEnd } = useMemo(() => {
if (!enabled) {
return { numPages: 0, rowsPerPage: 0, pageRangeStart: 1, pageRangeEnd: numRows };
}
const rowAreaHeight = height - headerHeight - footerHeight - PAGINATION_HEIGHT;
const heightPerRow = Math.floor(rowAreaHeight / (avgRowHeight || 1));
let rowsPerPage2 = heightPerRow > 1 ? heightPerRow : 1;
const pageRangeStart2 = page * rowsPerPage2 + 1;
let pageRangeEnd2 = pageRangeStart2 + rowsPerPage2 - 1;
if (pageRangeEnd2 > numRows) {
pageRangeEnd2 = numRows;
}
const numPages2 = Math.ceil(numRows / rowsPerPage2);
return {
numPages: numPages2,
rowsPerPage: rowsPerPage2,
pageRangeStart: pageRangeStart2,
pageRangeEnd: pageRangeEnd2
};
}, [height, headerHeight, footerHeight, avgRowHeight, enabled, numRows, page]);
useLayoutEffect(() => {
if (!enabled) {
return;
}
if (page > numPages) {
setPage(numPages - 1);
}
}, [numPages, enabled, page, setPage]);
const paginatedRows = useMemo(() => {
if (!enabled) {
return rows;
}
const pageOffset = page * rowsPerPage;
return rows.slice(pageOffset, pageOffset + rowsPerPage);
}, [page, rowsPerPage, rows, enabled]);
return {
rows: paginatedRows,
page: enabled ? page : -1,
setPage,
numPages,
rowsPerPage,
pageRangeStart,
pageRangeEnd,
smallPagination
};
}
const ICON_WIDTH = 16;
const ICON_GAP = 4;
function useHeaderHeight({
fields,
enabled,
columnWidths,
sortColumns,
typographyCtx,
showTypeIcons = false
}) {
const perIconSpace = ICON_WIDTH + ICON_GAP;
const measurers = useMemo(() => buildHeaderHeightMeasurers(fields, typographyCtx), [fields, typographyCtx]);
const columnAvailableWidths = useMemo(
() => columnWidths.map((c, idx) => {
var _a, _b;
if (idx >= fields.length) {
return 0;
}
let width = c - 2 * TABLE.CELL_PADDING - TABLE.BORDER_RIGHT;
const field = fields[idx];
if ((_b = (_a = field.config) == null ? void 0 : _a.custom) == null ? void 0 : _b.filterable) {
width -= perIconSpace;
}
if (sortColumns.some((col) => col.columnKey === getDisplayName(field))) {
width -= perIconSpace;
}
if (showTypeIcons) {
width -= perIconSpace;
}
return Math.floor(width) - 1;
}),
[fields, columnWidths, sortColumns, showTypeIcons, perIconSpace]
);
const headerHeight = useMemo(() => {
if (!enabled) {
return 0;
}
return getRowHeight(
fields,
-1,
columnAvailableWidths,
TABLE.HEADER_HEIGHT,
measurers,
TABLE.LINE_HEIGHT,
TABLE.CELL_PADDING
);
}, [fields, enabled, columnAvailableWidths, measurers]);
return headerHeight;
}
function useRowHeight({
columnWidths,
fields,
hasNestedFrames,
defaultHeight,
expandedRows,
typographyCtx,
maxHeight
}) {
const measurers = useMemo(
() => buildCellHeightMeasurers(fields, typographyCtx, maxHeight),
[fields, typographyCtx, maxHeight]
);
const hasWrappedCols = useMemo(() => {
var _a;
return (_a = measurers == null ? void 0 : measurers.length) != null ? _a : 0 > 0;
}, [measurers]);
const colWidths = useMemo(() => {
const columnWidthAffordance = 2 * TABLE.CELL_PADDING + TABLE.BORDER_RIGHT;
return columnWidths.map((c) => c - columnWidthAffordance);
}, [columnWidths]);
const rowHeight = useMemo(() => {
if (!hasNestedFrames && !hasWrappedCols || typeof defaultHeight === "string") {
return defaultHeight;
}
const cache = Array(fields[0].values.length);
return (row) => {
var _a, _b, _c, _d, _e;
if (row.__depth > 0) {
if (!expandedRows.has(row.__index)) {
return 0;
}
const rowCount = (_b = (_a = row.data) == null ? void 0 : _a.length) != null ? _b : 0;
if (rowCount === 0) {
return TABLE.NESTED_NO_DATA_HEIGHT + TABLE.CELL_PADDING * 2;
}
const nestedHeaderHeight = ((_e = (_d = (_c = row.data) == null ? void 0 : _c.meta) == null ? void 0 : _d.custom) == null ? void 0 : _e.noHeader) ? 0 : defaultHeight;
return defaultHeight * rowCount + nestedHeaderHeight + TABLE.CELL_PADDING * 2;
}
let result = cache[row.__index];
if (!result) {
result = cache[row.__index] = getRowHeight(fields, row.__index, colWidths, defaultHeight, measurers);
}
return result;
};
}, [hasNestedFrames, hasWrappedCols, defaultHeight, fields, colWidths, measurers, expandedRows]);
return rowHeight;
}
const INITIAL_COL_RESIZE_STATE = Object.freeze({ columnKey: void 0, width: 0 });
function useColumnResize(onColumnResize = () => {
}) {
const colResizeState = useRef({ ...INITIAL_COL_RESIZE_STATE });
const pointerIsDown = useRef(false);
useLayoutEffect(() => {
function pointerDown(_event) {
pointerIsDown.current = true;
}
function pointerUp(_event) {
pointerIsDown.current = false;
}
window.addEventListener("pointerdown", pointerDown);
window.addEventListener("pointerup", pointerUp);
return () => {
window.removeEventListener("pointerdown", pointerDown);
window.removeEventListener("pointerup", pointerUp);
};
});
const dispatchEvent = useCallback(() => {
if (colResizeState.current.columnKey) {
onColumnResize(colResizeState.current.columnKey, Math.floor(colResizeState.current.width));
colResizeState.current = { ...INITIAL_COL_RESIZE_STATE };
}
window.removeEventListener("click", dispatchEvent, { capture: true });
}, [onColumnResize]);
const dataGridResizeHandler = useCallback(
(column, width) => {
if (!colResizeState.current.columnKey) {
window.addEventListener("click", dispatchEvent, { capture: true });
}
colResizeState.current.columnKey = column.key;
colResizeState.current.width = width;
if (!pointerIsDown.current) {
dispatchEvent();
}
},
[dispatchEvent]
);
return dataGridResizeHandler;
}
function useScrollbarWidth(ref, height) {
const [scrollbarWidth, setScrollbarWidth] = useState(0);
useLayoutEffect(() => {
var _a;
const el = (_a = ref.current) == null ? void 0 : _a.element;
if (!el || IS_SAFARI_26) {
return;
}
const updateScrollbarDimensions = () => {
setScrollbarWidth(el.offsetWidth - el.clientWidth);
};
updateScrollbarDimensions();
const resizeObserver = new ResizeObserver(updateScrollbarDimensions);
resizeObserver.observe(el);
return () => {
resizeObserver.disconnect();
};
}, [ref, height]);
return scrollbarWidth;
}
const numIsEqual = (a, b) => a === b;
function useColWidths(visibleFields, availableWidth, frozenColumns) {
const [widths, setWidths] = useState(computeColWidths(visibleFields, availableWidth));
useEffect(() => {
const newWidths = computeColWidths(visibleFields, availableWidth);
if (!compareArrayValues(widths, newWidths, numIsEqual)) {
setWidths(newWidths);
}
}, [availableWidth, widths, visibleFields]);
const numFrozenColsFullyInView = useMemo(() => {
if (!frozenColumns || frozenColumns <= 0) {
return -1;
}
const fullyVisibleCols = widths.reduce(
([count, remainingWidth], nextWidth) => {
if (remainingWidth - nextWidth >= 0) {
return [count + 1, remainingWidth - nextWidth];
}
return [count, 0];
},
[0, availableWidth]
)[0];
return Math.min(fullyVisibleCols, frozenColumns);
}, [widths, availableWidth, frozenColumns]);
return [widths, numFrozenColsFullyInView];
}
const isReducer = (maybeReducer) => maybeReducer in ReducerID;
const nonMathReducers = /* @__PURE__ */ new Set([
ReducerID.allValues,
ReducerID.changeCount,
ReducerID.count,
ReducerID.countAll,
ReducerID.distinctCount,
ReducerID.first,
ReducerID.firstNotNull,
ReducerID.last,
ReducerID.lastNotNull,
ReducerID.uniqueValues
]);
const isNonMathReducer = (reducer) => isReducer(reducer) && nonMathReducers.has(reducer);
const noFormattingReducers = /* @__PURE__ */ new Set([ReducerID.count, ReducerID.countAll]);
const shouldReducerSkipFormatting = (reducer) => isReducer(reducer) && noFormattingReducers.has(reducer);
const useReducerEntries = (field, rows, displayName, colIdx) => {
return useMemo(() => {
var _a, _b, _c;
const reducers = (_c = (_b = (_a = field.config.custom) == null ? void 0 : _a.footer) == null ? void 0 : _b.reducers) != null ? _c : [];
if (reducers.length === 0 || field.type !== FieldType.number && reducers.every((reducerId) => !isNonMathReducer(reducerId))) {
return [];
}
const newState = {
lastProcessedRowCount: 0,
...field.state || {}
// Preserve any existing state properties
};
field.state = newState;
const currentRowCount = rows.length;
const lastRowCount = newState.lastProcessedRowCount;
if (lastRowCount !== currentRowCount) {
if (newState.calcs) {
delete newState.calcs;
}
newState.lastProcessedRowCount = currentRowCount;
}
const results = reduceField({
field: {
...field,
values: rows.map((row) => row[displayName])
},
reducers
});
return reducers.map((reducerId) => {
if (results[reducerId] === void 0 || // For non-number fields, only show special count reducers
field.type !== FieldType.number && !isNonMathReducer(reducerId) || // for countAll, only show the reducer in the first column
reducerId === ReducerID.countAll && colIdx !== 0) {
return [reducerId, null];
}
const value = results[reducerId];
let result = null;
if (!shouldReducerSkipFormatting(reducerId) && field.display) {
result = formattedValueToString(field.display(value));
} else if (value != null) {
result = String(value);
}
return [reducerId, result];
});
}, [field, rows, displayName, colIdx]);
};
export { useColWidths, useColumnResize, useFilteredRows, useHeaderHeight, usePaginatedRows, useReducerEntries, useRowHeight, useScrollbarWidth, useSortedRows };
//# sourceMappingURL=hooks.mjs.map