Explore: Limit number of columns shown in Explore table (#98726)

pull/98768/head^2
Piotr Jamróz 6 months ago committed by GitHub
parent e38bab43db
commit f7e0710f53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 50
      public/app/features/explore/Graph/GraphContainer.tsx
  2. 48
      public/app/features/explore/LimitedDataDisclaimer.tsx
  3. 47
      public/app/features/explore/Table/TableContainer.tsx
  4. 5
      public/app/features/explore/state/query.ts
  5. 6
      public/locales/en-US/grafana.json
  6. 6
      public/locales/pseudo-LOCALE/grafana.json

@ -1,4 +1,3 @@
import { css } from '@emotion/css';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useToggle } from 'react-use'; import { useToggle } from 'react-use';
@ -10,21 +9,13 @@ import {
SplitOpen, SplitOpen,
LoadingState, LoadingState,
ThresholdsConfig, ThresholdsConfig,
GrafanaTheme2,
TimeRange, TimeRange,
} from '@grafana/data'; } from '@grafana/data';
import { import { GraphThresholdsStyleConfig, PanelChrome, PanelChromeProps } from '@grafana/ui';
GraphThresholdsStyleConfig,
PanelChrome,
PanelChromeProps,
Icon,
Button,
useStyles2,
Tooltip,
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { ExploreGraphStyle } from 'app/types'; import { ExploreGraphStyle } from 'app/types';
import { LimitedDataDisclaimer } from '../LimitedDataDisclaimer';
import { storeGraphStyle } from '../state/utils'; import { storeGraphStyle } from '../state/utils';
import { ExploreGraph } from './ExploreGraph'; import { ExploreGraph } from './ExploreGraph';
@ -65,7 +56,6 @@ export const GraphContainer = ({
}: Props) => { }: Props) => {
const [showAllSeries, toggleShowAllSeries] = useToggle(false); const [showAllSeries, toggleShowAllSeries] = useToggle(false);
const [graphStyle, setGraphStyle] = useState(loadGraphStyle); const [graphStyle, setGraphStyle] = useState(loadGraphStyle);
const styles = useStyles2(getStyles);
const onGraphStyleChange = useCallback((graphStyle: ExploreGraphStyle) => { const onGraphStyleChange = useCallback((graphStyle: ExploreGraphStyle) => {
storeGraphStyle(graphStyle); storeGraphStyle(graphStyle);
@ -81,24 +71,20 @@ export const GraphContainer = ({
title={t('graph.container.title', 'Graph')} title={t('graph.container.title', 'Graph')}
titleItems={[ titleItems={[
!showAllSeries && MAX_NUMBER_OF_TIME_SERIES < data.length && ( !showAllSeries && MAX_NUMBER_OF_TIME_SERIES < data.length && (
<div key="disclaimer" className={styles.timeSeriesDisclaimer}> <LimitedDataDisclaimer
<span className={styles.warningMessage}> key="disclaimer"
<Icon name="exclamation-triangle" aria-hidden="true" /> toggleShowAllSeries={toggleShowAllSeries}
info={
<Trans i18nKey={'graph.container.show-only-series'}> <Trans i18nKey={'graph.container.show-only-series'}>
Showing only {{ MAX_NUMBER_OF_TIME_SERIES }} series Showing only {{ MAX_NUMBER_OF_TIME_SERIES }} series
</Trans> </Trans>
</span> }
<Tooltip buttonLabel={<Trans i18nKey={'graph.container.show-all-series'}>Show all {{ length: data.length }}</Trans>}
content={t( tooltip={t(
'graph.container.content', 'graph.container.content',
'Rendering too many series in a single panel may impact performance and make data harder to read. Consider refining your queries.' 'Rendering too many series in a single panel may impact performance and make data harder to read. Consider refining your queries.'
)} )}
> />
<Button variant="secondary" size="sm" onClick={toggleShowAllSeries}>
<Trans i18nKey={'graph.container.show-all-series'}>Show all {{ length: data.length }}</Trans>
</Button>
</Tooltip>
</div>
), ),
].filter(Boolean)} ].filter(Boolean)}
width={width} width={width}
@ -127,19 +113,3 @@ export const GraphContainer = ({
</PanelChrome> </PanelChrome>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({
timeSeriesDisclaimer: css({
label: 'time-series-disclaimer',
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
warningMessage: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
color: theme.colors.warning.main,
fontSize: theme.typography.bodySmall.fontSize,
}),
});

@ -0,0 +1,48 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Icon, Tooltip, useStyles2 } from '@grafana/ui';
type Props = {
toggleShowAllSeries: () => void;
info: React.ReactNode;
tooltip: string;
buttonLabel: React.ReactNode;
};
export function LimitedDataDisclaimer(props: Props) {
const { toggleShowAllSeries, info, tooltip, buttonLabel } = props;
const styles = useStyles2(getStyles);
return (
<div key="disclaimer" className={styles.disclaimer}>
<span className={styles.warningMessage}>
<Icon name="exclamation-triangle" aria-hidden="true" />
{info}
</span>
<Tooltip content={tooltip}>
<Button variant="secondary" size="sm" onClick={toggleShowAllSeries}>
{buttonLabel}
</Button>
</Tooltip>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
disclaimer: css({
label: 'series-disclaimer',
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
warningMessage: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
color: theme.colors.warning.main,
fontSize: theme.typography.bodySmall.fontSize,
}),
});

@ -7,7 +7,7 @@ import { getTemplateSrv } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema'; import { TimeZone } from '@grafana/schema';
import { Table, AdHocFilterItem, PanelChrome, withTheme2, Themeable2 } from '@grafana/ui'; import { Table, AdHocFilterItem, PanelChrome, withTheme2, Themeable2 } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { t } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { import {
hasDeprecatedParentRowIndex, hasDeprecatedParentRowIndex,
migrateFromParentRowIndexToNestedFrames, migrateFromParentRowIndexToNestedFrames,
@ -15,10 +15,13 @@ import {
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { ExploreItemState } from 'app/types/explore'; import { ExploreItemState } from 'app/types/explore';
import { LimitedDataDisclaimer } from '../LimitedDataDisclaimer';
import { MetaInfoText } from '../MetaInfoText'; import { MetaInfoText } from '../MetaInfoText';
import { selectIsWaitingForData } from '../state/query'; import { selectIsWaitingForData } from '../state/query';
import { exploreDataLinkPostProcessorFactory } from '../utils/links'; import { exploreDataLinkPostProcessorFactory } from '../utils/links';
const MAX_NUMBER_OF_COLUMNS = 20;
interface TableContainerProps extends Themeable2 { interface TableContainerProps extends Themeable2 {
ariaLabel?: string; ariaLabel?: string;
exploreId: string; exploreId: string;
@ -40,8 +43,13 @@ function mapStateToProps(state: StoreState, { exploreId }: TableContainerProps)
const connector = connect(mapStateToProps, {}); const connector = connect(mapStateToProps, {});
type Props = TableContainerProps & ConnectedProps<typeof connector>; type Props = TableContainerProps & ConnectedProps<typeof connector>;
type State = {
showAll: boolean;
};
export class TableContainer extends PureComponent<Props, State> {
state = { showAll: false };
export class TableContainer extends PureComponent<Props> {
hasSubFrames = (data: DataFrame) => data.fields.some((f) => f.type === FieldType.nestedFrames); hasSubFrames = (data: DataFrame) => data.fields.some((f) => f.type === FieldType.nestedFrames);
getTableHeight(rowCount: number, hasSubFrames: boolean) { getTableHeight(rowCount: number, hasSubFrames: boolean) {
@ -64,16 +72,35 @@ export class TableContainer extends PureComponent<Props> {
: t('explore.table.title', 'Table'); : t('explore.table.title', 'Table');
} }
showAll() {
this.setState({
showAll: true,
});
}
render() { render() {
const { loading, onCellFilterAdded, tableResult, width, splitOpenFn, range, ariaLabel, timeZone, theme } = const { loading, onCellFilterAdded, tableResult, width, splitOpenFn, range, ariaLabel, timeZone, theme } =
this.props; this.props;
const { showAll } = this.state;
let dataFrames = hasDeprecatedParentRowIndex(tableResult) let dataFrames = hasDeprecatedParentRowIndex(tableResult)
? migrateFromParentRowIndexToNestedFrames(tableResult) ? migrateFromParentRowIndexToNestedFrames(tableResult)
: tableResult; : tableResult;
const dataLinkPostProcessor = exploreDataLinkPostProcessorFactory(splitOpenFn, range); const dataLinkPostProcessor = exploreDataLinkPostProcessorFactory(splitOpenFn, range);
let dataLimited = false;
if (dataFrames?.length) { if (dataFrames?.length) {
dataFrames = dataFrames.map((frame) => {
frame.fields.forEach((field, index) => {
const hidden = showAll ? false : index >= MAX_NUMBER_OF_COLUMNS;
field.config.custom = { hidden };
dataLimited = dataLimited || hidden;
});
return frame;
});
dataFrames = applyFieldOverrides({ dataFrames = applyFieldOverrides({
data: dataFrames, data: dataFrames,
timeZone, timeZone,
@ -104,6 +131,22 @@ export class TableContainer extends PureComponent<Props> {
<PanelChrome <PanelChrome
key={data.refId || `table-${i}`} key={data.refId || `table-${i}`}
title={this.getTableTitle(dataFrames, data, i)} title={this.getTableTitle(dataFrames, data, i)}
titleItems={[
!showAll && dataLimited && (
<LimitedDataDisclaimer
toggleShowAllSeries={() => this.showAll()}
info={
<Trans i18nKey={'table.container.show-only-series'}>
Showing only {{ MAX_NUMBER_OF_COLUMNS }} columns
</Trans>
}
tooltip={
'Showing too many columns in a single table may impact performance and make data harder to read. Consider refining your queries.'
}
buttonLabel={<Trans i18nKey={'table.container.show-all-series'}>Show all columns</Trans>}
/>
),
]}
width={width} width={width}
height={this.getTableHeight(data.length, this.hasSubFrames(data))} height={this.getTableHeight(data.length, this.hasSubFrames(data))}
loadingState={loading ? LoadingState.Loading : undefined} loadingState={loading ? LoadingState.Loading : undefined}

@ -1303,10 +1303,7 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return state; return state;
}; };
export const processQueryResponse = ( const processQueryResponse = (state: ExploreItemState, action: PayloadAction<QueryEndedPayload>): ExploreItemState => {
state: ExploreItemState,
action: PayloadAction<QueryEndedPayload>
): ExploreItemState => {
const { response } = action.payload; const { response } = action.payload;
const { const {
request, request,

@ -3216,6 +3216,12 @@
"url-column-header": "Snapshot url", "url-column-header": "Snapshot url",
"view-button": "View" "view-button": "View"
}, },
"table": {
"container": {
"show-all-series": "Show all columns",
"show-only-series": "Showing only {{MAX_NUMBER_OF_COLUMNS}} columns"
}
},
"tag-filter": { "tag-filter": {
"clear-button": "Clear tags", "clear-button": "Clear tags",
"loading": "Loading...", "loading": "Loading...",

@ -3216,6 +3216,12 @@
"url-column-header": "Ŝʼnäpşĥőŧ ūřľ", "url-column-header": "Ŝʼnäpşĥőŧ ūřľ",
"view-button": "Vįęŵ" "view-button": "Vįęŵ"
}, },
"table": {
"container": {
"show-all-series": "Ŝĥőŵ äľľ čőľūmʼnş",
"show-only-series": "Ŝĥőŵįʼnģ őʼnľy {{MAX_NUMBER_OF_COLUMNS}} čőľūmʼnş"
}
},
"tag-filter": { "tag-filter": {
"clear-button": "Cľęäř ŧäģş", "clear-button": "Cľęäř ŧäģş",
"loading": "Ŀőäđįʼnģ...", "loading": "Ŀőäđįʼnģ...",

Loading…
Cancel
Save