diff --git a/public/app/features/dashboard/components/Inspector/InspectContent.tsx b/public/app/features/dashboard/components/Inspector/InspectContent.tsx index 618a0959d37..311059c79d0 100644 --- a/public/app/features/dashboard/components/Inspector/InspectContent.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectContent.tsx @@ -82,6 +82,7 @@ export const InspectContent: React.FC = ({ isLoading={isDataLoading} options={dataOptions} onOptionsChange={onDataOptionsChange} + timeZone={dashboard.timezone} /> )} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index ff1ee759646..3f341a8298e 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -232,13 +232,14 @@ export class Explore extends React.PureComponent { } renderTablePanel(width: number) { - const { exploreId, datasourceInstance } = this.props; + const { exploreId, datasourceInstance, timeZone } = this.props; return ( ); } @@ -303,6 +304,7 @@ export class Explore extends React.PureComponent { showLogs, showTrace, showNodeGraph, + timeZone, } = this.props; const { openDrawer } = this.state; const styles = getStyles(theme); @@ -364,6 +366,7 @@ export class Explore extends React.PureComponent { exploreId={exploreId} width={width} onClose={this.toggleShowQueryInspector} + timeZone={timeZone} /> )} diff --git a/public/app/features/explore/ExploreGraph.tsx b/public/app/features/explore/ExploreGraph.tsx index 3802e771382..8081253ffe3 100644 --- a/public/app/features/explore/ExploreGraph.tsx +++ b/public/app/features/explore/ExploreGraph.tsx @@ -162,6 +162,7 @@ export function ExploreGraph({ width={width} height={height} onChangeTimeRange={onChangeTime} + timeZone={timeZone} options={ { tooltip: { mode: tooltipDisplayMode }, diff --git a/public/app/features/explore/ExploreQueryInspector.test.tsx b/public/app/features/explore/ExploreQueryInspector.test.tsx index acceab4a13e..f439608e31f 100644 --- a/public/app/features/explore/ExploreQueryInspector.test.tsx +++ b/public/app/features/explore/ExploreQueryInspector.test.tsx @@ -1,7 +1,7 @@ import React, { ComponentProps } from 'react'; import { Observable } from 'rxjs'; import { render, screen, fireEvent } from '@testing-library/react'; -import { TimeRange, LoadingState } from '@grafana/data'; +import { TimeRange, LoadingState, InternalTimeZones } from '@grafana/data'; import { ExploreId } from 'app/types'; import { ExploreQueryInspector } from './ExploreQueryInspector'; @@ -33,6 +33,7 @@ const setup = (propOverrides = {}) => { width: 100, exploreId: ExploreId.left, onClose: jest.fn(), + timeZone: InternalTimeZones.utc, queryResponse: { state: LoadingState.Done, series: [], diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 82ff34fa0e9..bda8f197cf8 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -14,13 +14,14 @@ import { InspectErrorTab } from 'app/features/inspector/InspectErrorTab'; interface DispatchProps { width: number; exploreId: ExploreId; + timeZone: TimeZone; onClose: () => void; } type Props = DispatchProps & ConnectedProps; export function ExploreQueryInspector(props: Props) { - const { loading, width, onClose, queryResponse } = props; + const { loading, width, onClose, queryResponse, timeZone } = props; const dataFrames = queryResponse?.series || []; const error = queryResponse?.error; @@ -47,6 +48,7 @@ export function ExploreQueryInspector(props: Props) { data={dataFrames} isLoading={loading} options={{ withTransforms: false, withFieldConfig: false }} + timeZone={timeZone} /> ), }; diff --git a/public/app/features/explore/TableContainer.test.tsx b/public/app/features/explore/TableContainer.test.tsx index e2ac1083f0e..471abc061d1 100644 --- a/public/app/features/explore/TableContainer.test.tsx +++ b/public/app/features/explore/TableContainer.test.tsx @@ -1,41 +1,102 @@ import React from 'react'; -import { render, shallow } from 'enzyme'; +import { screen, render, within } from '@testing-library/react'; import { TableContainer } from './TableContainer'; -import { DataFrame } from '@grafana/data'; +import { DataFrame, toDataFrame, FieldType, InternalTimeZones } from '@grafana/data'; import { ExploreId } from 'app/types/explore'; +function getTable(): HTMLElement { + return screen.getAllByRole('table')[0]; +} + +function getRowsData(rows: HTMLElement[]): Object[] { + let content = []; + for (let i = 1; i < rows.length; i++) { + content.push({ + time: within(rows[i]).getByText(/2021*/).textContent, + text: within(rows[i]).getByText(/test_string_*/).textContent, + }); + } + return content; +} + +const dataFrame = toDataFrame({ + name: 'A', + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609470000000, 1609462800000, 1609466400000], + config: { + custom: { + filterable: false, + }, + }, + }, + { + name: 'text', + type: FieldType.string, + values: ['test_string_1', 'test_string_2', 'test_string_3', 'test_string_4'], + config: { + custom: { + filterable: false, + }, + }, + }, + ], +}); + +const defaultProps = { + exploreId: ExploreId.left as ExploreId, + loading: false, + width: 800, + onCellFilterAdded: jest.fn(), + tableResult: dataFrame, + splitOpen: (() => {}) as any, + range: {} as any, + timeZone: InternalTimeZones.utc, +}; + describe('TableContainer', () => { it('should render component', () => { - const props = { - exploreId: ExploreId.left as ExploreId, - loading: false, - width: 800, - onCellFilterAdded: jest.fn(), - tableResult: {} as DataFrame, - splitOpen: (() => {}) as any, - range: {} as any, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + render(); + expect(getTable()).toBeInTheDocument(); + const rows = within(getTable()).getAllByRole('row'); + expect(rows).toHaveLength(5); + expect(getRowsData(rows)).toEqual([ + { time: '2021-01-01 00:00:00', text: 'test_string_1' }, + { time: '2021-01-01 03:00:00', text: 'test_string_2' }, + { time: '2021-01-01 01:00:00', text: 'test_string_3' }, + { time: '2021-01-01 02:00:00', text: 'test_string_4' }, + ]); }); it('should render 0 series returned on no items', () => { - const props = { - exploreId: ExploreId.left as ExploreId, - loading: false, - width: 800, - onCellFilterAdded: jest.fn(), - tableResult: { - name: 'TableResultName', - fields: [], - length: 0, - } as DataFrame, - splitOpen: (() => {}) as any, - range: {} as any, - }; - - const wrapper = render(); - expect(wrapper.find('0 series returned')).toBeTruthy(); + const emptyFrames = { + name: 'TableResultName', + fields: [], + length: 0, + } as DataFrame; + render(); + expect(screen.getByText('0 series returned')).toBeInTheDocument(); + }); + + it('should update time when timezone changes', () => { + const { rerender } = render(); + const rowsBeforeChange = within(getTable()).getAllByRole('row'); + expect(getRowsData(rowsBeforeChange)).toEqual([ + { time: '2021-01-01 00:00:00', text: 'test_string_1' }, + { time: '2021-01-01 03:00:00', text: 'test_string_2' }, + { time: '2021-01-01 01:00:00', text: 'test_string_3' }, + { time: '2021-01-01 02:00:00', text: 'test_string_4' }, + ]); + + rerender(); + const rowsAfterChange = within(getTable()).getAllByRole('row'); + expect(getRowsData(rowsAfterChange)).toEqual([ + { time: '2020-12-31 19:00:00', text: 'test_string_1' }, + { time: '2020-12-31 22:00:00', text: 'test_string_2' }, + { time: '2020-12-31 20:00:00', text: 'test_string_3' }, + { time: '2020-12-31 21:00:00', text: 'test_string_4' }, + ]); }); }); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index 506c9e8771f..29f7c85b6ad 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { ValueLinkConfig } from '@grafana/data'; +import { ValueLinkConfig, applyFieldOverrides, TimeZone } from '@grafana/data'; import { Collapse, Table } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; @@ -15,6 +15,7 @@ interface TableContainerProps { ariaLabel?: string; exploreId: ExploreId; width: number; + timeZone: TimeZone; onCellFilterAdded?: (filter: FilterItem) => void; } @@ -48,17 +49,27 @@ export class TableContainer extends PureComponent { } render() { - const { loading, onCellFilterAdded, tableResult, width, splitOpen, range, ariaLabel } = this.props; - + const { loading, onCellFilterAdded, tableResult, width, splitOpen, range, ariaLabel, timeZone } = this.props; const height = this.getTableHeight(); const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER; - const hasTableResult = tableResult?.length; - if (tableResult && tableResult.length) { + let dataFrame = tableResult; + + if (dataFrame?.length) { + dataFrame = applyFieldOverrides({ + data: [dataFrame], + timeZone, + theme: config.theme2, + replaceVariables: (v: string) => v, + fieldConfig: { + defaults: {}, + overrides: [], + }, + })[0]; // Bit of code smell here. We need to add links here to the frame modifying the frame on every render. // Should work fine in essence but still not the ideal way to pass props. In logs container we do this // differently and sidestep this getLinks API on a dataframe - for (const field of tableResult.fields) { + for (const field of dataFrame.fields) { field.getLinks = (config: ValueLinkConfig) => { return getFieldLinksForExplore({ field, rowIndex: config.valueRowIndex!, splitOpenFn: splitOpen, range }); }; @@ -67,10 +78,10 @@ export class TableContainer extends PureComponent { return ( - {hasTableResult ? ( + {dataFrame?.length ? ( - - -`; diff --git a/public/app/features/inspector/InspectDataTab.tsx b/public/app/features/inspector/InspectDataTab.tsx index 0dbf2c8b566..624955c6226 100644 --- a/public/app/features/inspector/InspectDataTab.tsx +++ b/public/app/features/inspector/InspectDataTab.tsx @@ -12,6 +12,7 @@ import { SelectableValue, toCSV, transformDataFrame, + TimeZone, } from '@grafana/data'; import { Button, Container, Spinner, Table } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; @@ -30,6 +31,7 @@ import { transformToOTLP } from 'app/plugins/datasource/tempo/resultTransformer' interface Props { isLoading: boolean; options: GetDataOptions; + timeZone: TimeZone; data?: DataFrame[]; panel?: PanelModel; onOptionsChange?: (options: GetDataOptions) => void; @@ -184,7 +186,7 @@ export class InspectDataTab extends PureComponent { }; getProcessedData(): DataFrame[] { - const { options, panel } = this.props; + const { options, panel, timeZone } = this.props; const data = this.state.transformedData; if (!options.withFieldConfig || !panel) { @@ -197,6 +199,7 @@ export class InspectDataTab extends PureComponent { data, theme: config.theme2, fieldConfig: panel.fieldConfig, + timeZone, replaceVariables: (value: string) => { return value; },