diff --git a/docs/sources/visualizations/table/_index.md b/docs/sources/visualizations/table/_index.md index 57d8589d59a..6d74d495128 100644 --- a/docs/sources/visualizations/table/_index.md +++ b/docs/sources/visualizations/table/_index.md @@ -105,3 +105,7 @@ Enables value inspection from table cell. The raw value is presented in a modal ## Column filter You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "./filter-table-columns.md" >}}). + +## Pagination + +Use this option to enable or disable pagination. It is a front-end option that does not affect queries. When enabled, the page size automatically adjusts to the height of the table. diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts index 121111ec9d8..1436fc457ac 100644 --- a/e2e/panels-suite/panelEdit_base.spec.ts +++ b/e2e/panels-suite/panelEdit_base.spec.ts @@ -102,7 +102,7 @@ e2e.scenario({ e2e.components.PanelEditor.DataPane.content().should('be.visible'); // Field & Overrides tabs (need to switch to React based vis, i.e. Table) - e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show header').should('be.visible'); + e2e.components.PanelEditor.OptionsPane.fieldLabel('Header and footer Show header').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); }, }); diff --git a/packages/grafana-runtime/tsconfig.json b/packages/grafana-runtime/tsconfig.json index 515c02e0262..444da147886 100644 --- a/packages/grafana-runtime/tsconfig.json +++ b/packages/grafana-runtime/tsconfig.json @@ -9,5 +9,10 @@ }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"] + "include": [ + "src/**/*.ts*", + "../../public/app/types/jquery/*.ts", + "../../public/app/types/*.d.ts", + "../grafana-ui/src/types/*.d.ts" + ] } diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx new file mode 100644 index 00000000000..305e25050a3 --- /dev/null +++ b/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Pagination } from './Pagination'; + +describe('Pagination component', () => { + it('should render only 10 buttons when number of pages is higher than 8', () => { + render( {}} />); + expect(screen.getAllByRole('button')).toHaveLength(10); + }); + it('should only show 3 buttons when showSmallVersion is true', () => { + render( {}} showSmallVersion />); + expect(screen.getAllByRole('button')).toHaveLength(4); + }); +}); diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.tsx index 34b30e07f08..a15b7a1fe24 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.tsx @@ -1,77 +1,94 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { css } from '@emotion/css'; -import { stylesFactory } from '../../themes'; +import { useStyles2 } from '../../themes'; import { Button, ButtonVariant } from '../Button'; import { Icon } from '../Icon/Icon'; -const PAGE_LENGTH_TO_CONDENSE = 8; - export interface Props { - /** The current page index being shown. */ + /** The current page index being shown. */ currentPage: number; - /** Number of total pages. */ + /** Number of total pages. */ numberOfPages: number; - /** Callback function for fetching the selected page */ + /** Callback function for fetching the selected page. */ onNavigate: (toPage: number) => void; - /** When set to true and the pagination result is only one page it will not render the pagination at all */ + /** When set to true and the pagination result is only one page it will not render the pagination at all. */ hideWhenSinglePage?: boolean; + /** Small version only shows the current page and the navigation buttons. */ + showSmallVersion?: boolean; } -export const Pagination: React.FC = ({ currentPage, numberOfPages, onNavigate, hideWhenSinglePage }) => { - const styles = getStyles(); - const pages = [...new Array(numberOfPages).keys()]; +export const Pagination: React.FC = ({ + currentPage, + numberOfPages, + onNavigate, + hideWhenSinglePage, + showSmallVersion, +}) => { + const styles = useStyles2(getStyles); + const pageLengthToCondense = showSmallVersion ? 1 : 8; - const condensePages = numberOfPages > PAGE_LENGTH_TO_CONDENSE; - const getListItem = (page: number, variant: 'primary' | 'secondary') => ( -
  • - -
  • - ); + const pageButtons = useMemo(() => { + const pages = [...new Array(numberOfPages).keys()]; + + const condensePages = numberOfPages > pageLengthToCondense; + const getListItem = (page: number, variant: 'primary' | 'secondary') => ( +
  • + +
  • + ); - const pageButtons = pages.reduce((pagesToRender, pageIndex) => { - const page = pageIndex + 1; - const variant: ButtonVariant = page === currentPage ? 'primary' : 'secondary'; + return pages.reduce((pagesToRender, pageIndex) => { + const page = pageIndex + 1; + const variant: ButtonVariant = page === currentPage ? 'primary' : 'secondary'; - // The indexes at which to start and stop condensing pages - const lowerBoundIndex = PAGE_LENGTH_TO_CONDENSE; - const upperBoundIndex = numberOfPages - PAGE_LENGTH_TO_CONDENSE + 1; - // When the indexes overlap one another this number is negative - const differenceOfBounds = upperBoundIndex - lowerBoundIndex; + // The indexes at which to start and stop condensing pages + const lowerBoundIndex = pageLengthToCondense; + const upperBoundIndex = numberOfPages - pageLengthToCondense + 1; + // When the indexes overlap one another this number is negative + const differenceOfBounds = upperBoundIndex - lowerBoundIndex; - const isFirstOrLastPage = page === 1 || page === numberOfPages; - // This handles when the lowerBoundIndex < currentPage < upperBoundIndex - const currentPageIsBetweenBounds = - differenceOfBounds > -1 && currentPage >= lowerBoundIndex && currentPage <= upperBoundIndex; + const isFirstOrLastPage = page === 1 || page === numberOfPages; + // This handles when the lowerBoundIndex < currentPage < upperBoundIndex + const currentPageIsBetweenBounds = + differenceOfBounds > -1 && currentPage >= lowerBoundIndex && currentPage <= upperBoundIndex; - if (condensePages) { - if ( - isFirstOrLastPage || - (currentPage < lowerBoundIndex && page < lowerBoundIndex) || - (differenceOfBounds >= 0 && currentPage > upperBoundIndex && page > upperBoundIndex) || - (differenceOfBounds < 0 && currentPage >= lowerBoundIndex && page > upperBoundIndex) || - (currentPageIsBetweenBounds && page >= currentPage - 2 && page <= currentPage + 2) - ) { - // Renders a button for the page + // Show ellipsis after that many pages + const ellipsisOffset = showSmallVersion ? 1 : 3; + + // The offset to show more pages when currentPageIsBetweenBounds + const pageOffset = showSmallVersion ? 0 : 2; + + if (condensePages) { + if ( + isFirstOrLastPage || + (currentPage < lowerBoundIndex && page < lowerBoundIndex) || + (differenceOfBounds >= 0 && currentPage > upperBoundIndex && page > upperBoundIndex) || + (differenceOfBounds < 0 && currentPage >= lowerBoundIndex && page > upperBoundIndex) || + (currentPageIsBetweenBounds && page >= currentPage - pageOffset && page <= currentPage + pageOffset) + ) { + // Renders a button for the page + pagesToRender.push(getListItem(page, variant)); + } else if ( + (page === lowerBoundIndex && currentPage < lowerBoundIndex) || + (page === upperBoundIndex && currentPage > upperBoundIndex) || + (currentPageIsBetweenBounds && + (page === currentPage - ellipsisOffset || page === currentPage + ellipsisOffset)) + ) { + // Renders and ellipsis to represent condensed pages + pagesToRender.push( +
  • + +
  • + ); + } + } else { pagesToRender.push(getListItem(page, variant)); - } else if ( - (page === lowerBoundIndex && currentPage < lowerBoundIndex) || - (page === upperBoundIndex && currentPage > upperBoundIndex) || - (currentPageIsBetweenBounds && (page === currentPage - 3 || page === currentPage + 3)) - ) { - // Renders and ellipsis to represent condensed pages - pagesToRender.push( -
  • - -
  • - ); } - } else { - pagesToRender.push(getListItem(page, variant)); - } - return pagesToRender; - }, []); + return pagesToRender; + }, []); + }, [currentPage, numberOfPages, onNavigate, pageLengthToCondense, showSmallVersion, styles.ellipsis, styles.item]); if (hideWhenSinglePage && numberOfPages <= 1) { return null; @@ -108,7 +125,7 @@ export const Pagination: React.FC = ({ currentPage, numberOfPages, onNavi ); }; -const getStyles = stylesFactory(() => { +const getStyles = () => { return { container: css` float: right; @@ -122,4 +139,4 @@ const getStyles = stylesFactory(() => { transform: rotate(90deg); `, }; -}); +}; diff --git a/packages/grafana-ui/src/components/Table/FooterRow.tsx b/packages/grafana-ui/src/components/Table/FooterRow.tsx index 5bdc13b8b61..c032c3d3597 100644 --- a/packages/grafana-ui/src/components/Table/FooterRow.tsx +++ b/packages/grafana-ui/src/components/Table/FooterRow.tsx @@ -10,18 +10,19 @@ export interface FooterRowProps { totalColumnsWidth: number; footerGroups: HeaderGroup[]; footerValues: FooterItem[]; + isPaginationVisible: boolean; height: number; } export const FooterRow = (props: FooterRowProps) => { - const { totalColumnsWidth, footerGroups, height } = props; + const { totalColumnsWidth, footerGroups, height, isPaginationVisible } = props; const e2eSelectorsTable = selectors.components.Panels.Visualization.Table; const tableStyles = useStyles2(getTableStyles); return ( { ); }; + +export const Pagination: Story = (args) => ; +Pagination.args = { + pageSize: 10, +}; diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index a558a80514b..1366744fd91 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -1,15 +1,14 @@ -import React, { FC, memo, useCallback, useMemo } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo } from 'react'; import { DataFrame, getFieldDisplayName } from '@grafana/data'; import { Cell, Column, + TableState, useAbsoluteLayout, useFilters, - UseFiltersState, + usePagination, useResizeColumns, - UseResizeColumnsState, useSortBy, - UseSortByState, useTable, } from 'react-table'; import { FixedSizeList } from 'react-window'; @@ -27,6 +26,7 @@ import { TableCell } from './TableCell'; import { useStyles2 } from '../../themes'; import { FooterRow } from './FooterRow'; import { HeaderRow } from './HeaderRow'; +import { Pagination } from '../Pagination/Pagination'; const COLUMN_MIN_WIDTH = 150; @@ -45,13 +45,12 @@ export interface Props { onSortByChange?: TableSortByActionCallback; onCellFilterAdded?: TableFilterActionCallback; footerValues?: FooterItem[]; + enablePagination?: boolean; } -interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}>, UseFiltersState<{}> {} - function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) { return useCallback( - (newState: ReactTableInternalState, action: any) => { + (newState: TableState, action: any) => { switch (action.type) { case 'columnDoneResizing': if (onColumnResize) { @@ -95,8 +94,8 @@ function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) { ); } -function getInitialState(initialSortBy: Props['initialSortBy'], columns: Column[]): Partial { - const state: Partial = {}; +function getInitialState(initialSortBy: Props['initialSortBy'], columns: Column[]): Partial { + const state: Partial = {}; if (initialSortBy) { state.sortBy = []; @@ -126,6 +125,7 @@ export const Table: FC = memo((props: Props) => { initialSortBy, footerValues, showTypeIcons, + enablePagination, } = props; const tableStyles = useStyles2(getTableStyles); @@ -188,17 +188,40 @@ export const Table: FC = memo((props: Props) => { [initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer] ); - const { getTableProps, headerGroups, rows, prepareRow, totalColumnsWidth, footerGroups } = useTable( - options, - useFilters, - useSortBy, - useAbsoluteLayout, - useResizeColumns - ); + const { + getTableProps, + headerGroups, + rows, + prepareRow, + totalColumnsWidth, + footerGroups, + page, + state, + gotoPage, + setPageSize, + pageOptions, + } = useTable(options, useFilters, useSortBy, usePagination, useAbsoluteLayout, useResizeColumns); + + let listHeight = height - (headerHeight + footerHeight); + if (enablePagination) { + listHeight -= tableStyles.cellHeight; + } + const pageSize = Math.round(listHeight / tableStyles.cellHeight) - 1; + + useEffect(() => { + // Don't update the page size if it is less than 1 + if (pageSize <= 0) { + return; + } + setPageSize(pageSize); + }, [pageSize, setPageSize]); const RenderRow = React.useCallback( ({ index: rowIndex, style }) => { - const row = rows[rowIndex]; + let row = rows[rowIndex]; + if (enablePagination) { + row = page[rowIndex]; + } prepareRow(row); return (
    @@ -215,20 +238,52 @@ export const Table: FC = memo((props: Props) => {
    ); }, - [onCellFilterAdded, prepareRow, rows, tableStyles] + [onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles] ); - const listHeight = height - (headerHeight + footerHeight); + const onNavigate = useCallback( + (toPage: number) => { + gotoPage(toPage - 1); + }, + [gotoPage] + ); + const itemCount = enablePagination ? page.length : data.length; + let paginationEl = null; + if (enablePagination) { + const itemsRangeStart = state.pageIndex * state.pageSize + 1; + let itemsRangeEnd = itemsRangeStart + state.pageSize - 1; + const isSmall = width < 500; + if (itemsRangeEnd > data.length) { + itemsRangeEnd = data.length; + } + paginationEl = ( +
    +
    + +
    + {isSmall ? null : ( +
    + {itemsRangeStart} - {itemsRangeEnd} of {data.length} rows +
    + )} +
    + ); + } return (
    -
    +
    {!noHeader && } - {rows.length > 0 ? ( + {itemCount > 0 ? ( = memo((props: Props) => { {footerValues && ( )} + {paginationEl}
    diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index d11a186dd58..c70f0c9e199 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -170,6 +170,32 @@ export const getTableStyles = (theme: GrafanaTheme2) => { label: headerFilter; cursor: pointer; `, + paginationWrapper: css` + display: flex; + background: ${headerBg}; + height: ${cellHeight}px; + justify-content: center; + align-items: center; + width: 100%; + border-top: 1px solid ${theme.colors.border.weak}; + li { + margin-bottom: 0; + } + div:not(:only-child):first-child { + flex-grow: 0.6; + } + `, + paginationSummary: css` + color: ${theme.colors.text.secondary}; + font-size: ${theme.typography.bodySmall.fontSize}; + margin-left: auto; + `, + + tableContentWrapper: (totalColumnsWidth: number) => css` + width: ${totalColumnsWidth ?? '100%'}; + display: flex; + flex-direction: column; + `, row: css` label: row; border-bottom: 1px solid ${borderColor}; diff --git a/packages/grafana-ui/src/types/react-table-config.d.ts b/packages/grafana-ui/src/types/react-table-config.d.ts new file mode 100644 index 00000000000..b0e872a7c31 --- /dev/null +++ b/packages/grafana-ui/src/types/react-table-config.d.ts @@ -0,0 +1,111 @@ +import type { + UseColumnOrderInstanceProps, + UseColumnOrderState, + UseExpandedHooks, + UseExpandedInstanceProps, + UseExpandedOptions, + UseExpandedRowProps, + UseExpandedState, + UseFiltersColumnOptions, + UseFiltersColumnProps, + UseFiltersInstanceProps, + UseFiltersOptions, + UseFiltersState, + UseGlobalFiltersColumnOptions, + UseGlobalFiltersInstanceProps, + UseGlobalFiltersOptions, + UseGlobalFiltersState, + UseGroupByCellProps, + UseGroupByColumnOptions, + UseGroupByColumnProps, + UseGroupByHooks, + UseGroupByInstanceProps, + UseGroupByOptions, + UseGroupByRowProps, + UseGroupByState, + UsePaginationInstanceProps, + UsePaginationOptions, + UsePaginationState, + UseResizeColumnsColumnOptions, + UseResizeColumnsColumnProps, + UseResizeColumnsOptions, + UseResizeColumnsState, + UseRowSelectHooks, + UseRowSelectInstanceProps, + UseRowSelectOptions, + UseRowSelectRowProps, + UseRowSelectState, + UseRowStateCellProps, + UseRowStateInstanceProps, + UseRowStateOptions, + UseRowStateRowProps, + UseRowStateState, + UseSortByColumnOptions, + UseSortByColumnProps, + UseSortByHooks, + UseSortByInstanceProps, + UseSortByOptions, + UseSortByState, +} from 'react-table'; + +declare module 'react-table' { + export interface TableOptions> + extends UseExpandedOptions, + UseFiltersOptions, + UseGlobalFiltersOptions, + UseGroupByOptions, + UsePaginationOptions, + UseResizeColumnsOptions, + UseRowSelectOptions, + UseRowStateOptions, + UseSortByOptions, + // note that having Record here allows you to add anything to the options, this matches the spirit of the + // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your + // feature set, this is a safe default. + Record {} + + export interface Hooks = Record> + extends UseExpandedHooks, + UseGroupByHooks, + UseRowSelectHooks, + UseSortByHooks {} + + export interface TableInstance = Record> + extends UseColumnOrderInstanceProps, + UseExpandedInstanceProps, + UseFiltersInstanceProps, + UseGlobalFiltersInstanceProps, + UseGroupByInstanceProps, + UsePaginationInstanceProps, + UseRowSelectInstanceProps, + UseRowStateInstanceProps, + UseSortByInstanceProps {} + + export interface TableState = Record> + extends UseColumnOrderState, + UseExpandedState, + UseFiltersState, + UseGlobalFiltersState, + UseGroupByState, + UsePaginationState, + UseResizeColumnsState, + UseRowSelectState, + UseRowStateState, + UseSortByState {} + + export interface ColumnInterface = Record> + extends UseGlobalFiltersColumnOptions, + UseGroupByColumnOptions, + UseResizeColumnsColumnOptions, + UseSortByColumnOptions {} + + export interface ColumnInstance = Record> + extends UseFiltersColumnProps, + UseGroupByColumnProps, + UseResizeColumnsColumnProps, + UseSortByColumnProps {} + + export interface Cell = Record, V = any> + extends UseGroupByCellProps, + UseRowStateCellProps {} +} diff --git a/packages/jaeger-ui-components/tsconfig.json b/packages/jaeger-ui-components/tsconfig.json index 0f75321d0df..83f4d88d032 100644 --- a/packages/jaeger-ui-components/tsconfig.json +++ b/packages/jaeger-ui-components/tsconfig.json @@ -6,5 +6,11 @@ }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "typings", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"] + "include": [ + "src/**/*.ts*", + "typings", + "../../public/app/types/jquery/*.ts", + "../../public/app/types/*.d.ts", + "../grafana-ui/src/types/*.d.ts" + ] } diff --git a/public/app/plugins/panel/table/PaginationEditor.tsx b/public/app/plugins/panel/table/PaginationEditor.tsx new file mode 100644 index 00000000000..241d3137de1 --- /dev/null +++ b/public/app/plugins/panel/table/PaginationEditor.tsx @@ -0,0 +1,14 @@ +import { StandardEditorProps } from '@grafana/data'; +import { Switch } from '@grafana/ui'; +import React from 'react'; + +export function PaginationEditor({ onChange, value, context }: StandardEditorProps) { + const changeValue = (event: React.FormEvent | undefined) => { + if (event?.currentTarget.checked) { + context.options.footer.show = false; + } + onChange(event?.currentTarget.checked); + }; + + return ; +} diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index edf4f0b5862..852b0bbaeeb 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -108,6 +108,7 @@ export class TablePanel extends Component { onColumnResize={this.onColumnResize} onCellFilterAdded={this.onCellFilterAdded} footerValues={footerValues} + enablePagination={options.footer?.enablePagination} /> ); } diff --git a/public/app/plugins/panel/table/models.gen.ts b/public/app/plugins/panel/table/models.gen.ts index 413ccdf20e6..18c02f6cecd 100644 --- a/public/app/plugins/panel/table/models.gen.ts +++ b/public/app/plugins/panel/table/models.gen.ts @@ -24,6 +24,7 @@ export interface TableFooterCalc { show: boolean; reducer: string[]; // actually 1 value fields?: string[]; + enablePagination?: boolean; } export const defaultPanelOptions: PanelOptions = { diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index f6cbc78a82f..bfc3dfdccd0 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -12,6 +12,7 @@ import { TableFieldOptions } from '@grafana/schema'; import { tableMigrationHandler, tablePanelChangedHandler } from './migrations'; import { TableCellDisplayMode } from '@grafana/ui'; import { TableSuggestionsSupplier } from './suggestions'; +import { PaginationEditor } from './PaginationEditor'; export const plugin = new PanelPlugin(TablePanel) .setPanelChangeHandler(tablePanelChangedHandler) @@ -108,18 +109,21 @@ export const plugin = new PanelPlugin(TablePane builder .addBooleanSwitch({ path: 'showHeader', + category: ['Header and footer'], name: 'Show header', description: "To display table's header or not to display", defaultValue: defaultPanelOptions.showHeader, }) .addBooleanSwitch({ path: 'footer.show', + category: ['Header and footer'], name: 'Show Footer', description: "To display table's footer or not to display", defaultValue: defaultPanelOptions.footer?.show, }) .addCustomEditor({ id: 'footer.reducer', + category: ['Header and footer'], path: 'footer.reducer', name: 'Calculation', description: 'Choose a reducer function / calculation', @@ -129,6 +133,7 @@ export const plugin = new PanelPlugin(TablePane }) .addMultiSelect({ path: 'footer.fields', + category: ['Header and footer'], name: 'Fields', description: 'Select the fields that should be calculated', settings: { @@ -152,6 +157,13 @@ export const plugin = new PanelPlugin(TablePane }, defaultValue: '', showIf: (cfg) => cfg.footer?.show, + }) + .addCustomEditor({ + id: 'footer.enablePagination', + category: ['Header and footer'], + path: 'footer.enablePagination', + name: 'Enable pagination', + editor: PaginationEditor, }); }) .setSuggestionsSupplier(new TableSuggestionsSupplier()); diff --git a/tsconfig.json b/tsconfig.json index 224a1dfbccb..db195a464c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "public/test/**/*.ts", "public/vendor/**/*.ts", "packages/jaeger-ui-components/typings", - "packages/grafana-data/typings" + "packages/grafana-data/typings", + "packages/grafana-ui/src/types" ] }