From 724cbcd745129934a4524bc038b765e21b42eb5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 May 2021 17:09:06 +0200 Subject: [PATCH] PanelEdit: Adds a table view toggle to quickly view data in table form (#33753) * PanelEdit: Adds raw data toggle to quickly be able to view data in table form * With transforms * Updated name for toggle * refactoring and added e2e test * Support options * fixing e2e --- e2e/suite1/specs/panelEdit_base.spec.ts | 8 ++++ .../grafana-data/src/field/fieldState.test.ts | 40 ++++++++++++++++++- packages/grafana-data/src/field/fieldState.ts | 8 +++- .../src/selectors/components.ts | 4 ++ .../src/components/PanelRenderer.tsx | 2 +- .../Forms/RadioButtonGroup/RadioButton.tsx | 3 +- .../components/PanelChrome/PanelChrome.tsx | 2 +- .../src/components/Switch/Switch.story.tsx | 6 +++ .../src/components/Switch/Switch.tsx | 39 +++++++++++++----- .../grafana-ui/src/components/Table/Table.tsx | 9 ++++- .../components/PanelEditor/PanelEditor.tsx | 27 ++++++++++++- .../PanelEditor/PanelEditorTableView.tsx | 40 +++++++++++++++++++ .../PanelEditor/getPanelFrameOptions.tsx | 1 + .../components/PanelEditor/state/reducers.ts | 7 ++++ .../dashboard/components/PanelEditor/types.ts | 17 +++++++- public/app/features/panel/PanelRenderer.tsx | 8 ++-- 16 files changed, 196 insertions(+), 25 deletions(-) create mode 100644 public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx diff --git a/e2e/suite1/specs/panelEdit_base.spec.ts b/e2e/suite1/specs/panelEdit_base.spec.ts index 000e7608bdf..2f6f5517d51 100644 --- a/e2e/suite1/specs/panelEdit_base.spec.ts +++ b/e2e/suite1/specs/panelEdit_base.spec.ts @@ -69,6 +69,14 @@ e2e.scenario({ e2e.components.PluginVisualization.item('Time series').should('be.visible'); e2e.components.PluginVisualization.current().should((e) => expect(e).to.contain('Time series')); + // Check that table view works + e2e.components.PanelEditor.toggleTableView().click({ force: true }); + e2e.components.Panels.Visualization.Table.header() + .should('be.visible') + .within(() => { + cy.contains('A-series').should('be.visible'); + }); + // Change to Text panel e2e.components.PluginVisualization.item('Text').scrollIntoView().should('be.visible').click(); e2e.components.PanelEditor.toggleVizPicker().should((e) => expect(e).to.contain('Text')); diff --git a/packages/grafana-data/src/field/fieldState.test.ts b/packages/grafana-data/src/field/fieldState.test.ts index 713fd8b7a9e..656016fb9b3 100644 --- a/packages/grafana-data/src/field/fieldState.test.ts +++ b/packages/grafana-data/src/field/fieldState.test.ts @@ -1,5 +1,5 @@ import { DataFrame, TIME_SERIES_VALUE_FIELD_NAME, FieldType } from '../types'; -import { getFieldDisplayName } from './fieldState'; +import { getFieldDisplayName, getFrameDisplayName } from './fieldState'; import { toDataFrame } from '../dataframe'; interface TitleScenario { @@ -14,6 +14,44 @@ function checkScenario(scenario: TitleScenario): string { return getFieldDisplayName(field, frame, scenario.frames); } +describe('getFrameDisplayName', () => { + it('Should return frame name if set', () => { + const frame = toDataFrame({ + name: 'Series A', + fields: [{ name: 'Field 1' }], + }); + expect(getFrameDisplayName(frame)).toBe('Series A'); + }); + + it('Should return field name', () => { + const frame = toDataFrame({ + fields: [{ name: 'Field 1' }], + }); + expect(getFrameDisplayName(frame)).toBe('Field 1'); + }); + + it('Should return all field names', () => { + const frame = toDataFrame({ + fields: [{ name: 'Field A' }, { name: 'Field B' }], + }); + expect(getFrameDisplayName(frame)).toBe('Field A, Field B'); + }); + + it('Should return labels if single field with labels', () => { + const frame = toDataFrame({ + fields: [{ name: 'value', labels: { server: 'A' } }], + }); + expect(getFrameDisplayName(frame)).toBe('{server="A"}'); + }); + + it('Should return field names when labels object exist but has no keys', () => { + const frame = toDataFrame({ + fields: [{ name: 'value', labels: {} }], + }); + expect(getFrameDisplayName(frame)).toBe('value'); + }); +}); + describe('Check field state calculations (displayName and id)', () => { it('should use field name if no frame name', () => { const title = checkScenario({ diff --git a/packages/grafana-data/src/field/fieldState.ts b/packages/grafana-data/src/field/fieldState.ts index 255e2ddb91c..0e88d5be15e 100644 --- a/packages/grafana-data/src/field/fieldState.ts +++ b/packages/grafana-data/src/field/fieldState.ts @@ -10,7 +10,13 @@ export function getFrameDisplayName(frame: DataFrame, index?: number) { } // Single field with tags - const valuesWithLabels = frame.fields.filter((f) => f.labels !== undefined); + const valuesWithLabels: Field[] = []; + for (const field of frame.fields) { + if (field.labels && Object.keys(field.labels).length > 0) { + valuesWithLabels.push(field); + } + } + if (valuesWithLabels.length === 1) { return formatLabels(valuesWithLabels[0].labels!); } diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 90e3b92861c..fcfd0c4524c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -50,6 +50,9 @@ export const Components = { Text: { container: () => '.markdown-html', }, + Table: { + header: 'table header', + }, }, }, VizLegend: { @@ -80,6 +83,7 @@ export const Components = { applyButton: 'panel editor apply', toggleVizPicker: 'toggle-viz-picker', toggleVizOptions: 'toggle-viz-options', + toggleTableView: 'toggle-table-view', }, PanelInspector: { Data: { diff --git a/packages/grafana-runtime/src/components/PanelRenderer.tsx b/packages/grafana-runtime/src/components/PanelRenderer.tsx index eb6713f973e..1d178836483 100644 --- a/packages/grafana-runtime/src/components/PanelRenderer.tsx +++ b/packages/grafana-runtime/src/components/PanelRenderer.tsx @@ -15,7 +15,7 @@ export interface PanelRendererProps

; options?: P; - onOptionsChange: (options: P) => void; + onOptionsChange?: (options: P) => void; onChangeTimeRange?: (timeRange: AbsoluteTimeRange) => void; timeZone?: string; width: number; diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx index e96ebfc0a5a..cacece751f1 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -57,7 +57,6 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme2, size: RadioBut const textColor = theme.colors.text.secondary; const textColorHover = theme.colors.text.primary; - const bg = theme.colors.background.primary; // remove the group inner padding (set on RadioButtonGroup) const labelHeight = height * theme.spacing.gridSize - 4 - 2; @@ -99,7 +98,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme2, size: RadioBut color: ${textColor}; padding: ${theme.spacing(0, padding)}; border-radius: ${theme.shape.borderRadius()}; - background: ${bg}; + background: transparent; cursor: pointer; z-index: 1; flex: ${fullWidth ? `1 0 0` : 'none'}; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index e13dc1a9687..a32c0953a4f 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -37,7 +37,7 @@ export const PanelChrome: React.FC = ({ const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height); const headerStyles: CSSProperties = { - height: theme.panelHeaderHeight, + height: headerHeight, }; const containerStyles: CSSProperties = { width, height }; diff --git a/packages/grafana-ui/src/components/Switch/Switch.story.tsx b/packages/grafana-ui/src/components/Switch/Switch.story.tsx index 1cbbc94c760..d9a9bd5b3cf 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.story.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.story.tsx @@ -37,6 +37,12 @@ export const Controlled = () => { +

+
just inline switch with show label
+ + + +
); }; diff --git a/packages/grafana-ui/src/components/Switch/Switch.tsx b/packages/grafana-ui/src/components/Switch/Switch.tsx index 63a54dded46..2379cb82ab6 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.tsx @@ -42,16 +42,27 @@ export const Switch = React.forwardRef( Switch.displayName = 'Switch'; -export const InlineSwitch = React.forwardRef(({ transparent, ...props }, ref) => { - const theme = useTheme2(); - const styles = getSwitchStyles(theme, transparent); +export interface InlineSwitchProps extends Props { + showLabel?: boolean; +} - return ( -
- -
- ); -}); +export const InlineSwitch = React.forwardRef( + ({ transparent, showLabel, label, value, id, ...props }, ref) => { + const theme = useTheme2(); + const styles = getSwitchStyles(theme, transparent); + + return ( +
+ {showLabel && ( + + )} + +
+ ); + } +); InlineSwitch.displayName = 'Switch'; @@ -129,11 +140,19 @@ const getSwitchStyles = stylesFactory((theme: GrafanaTheme2, transparent?: boole inlineContainer: css` padding: ${theme.spacing(0, 1)}; height: ${theme.spacing(theme.components.height.md)}; - display: flex; + display: inline-flex; align-items: center; background: ${transparent ? 'transparent' : theme.components.input.background}; border: 1px solid ${transparent ? 'transparent' : theme.components.input.borderColor}; border-radius: ${theme.shape.borderRadius()}; `, + inlineLabel: css` + cursor: pointer; + padding-right: ${theme.spacing(1)}; + color: ${theme.colors.text.secondary}; + `, + inlineLabelEnabled: css` + color: ${theme.colors.text.primary}; + `, }; }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 1f4623a96bb..1950a9aaf93 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -27,8 +27,10 @@ import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; import { Filter } from './Filter'; import { TableCell } from './TableCell'; import { useStyles2 } from '../../themes'; +import { selectors } from '@grafana/e2e-selectors'; const COLUMN_MIN_WIDTH = 150; +const e2eSelectorsTable = selectors.components.Panels.Visualization.Table; export interface Props { ariaLabel?: string; @@ -202,7 +204,12 @@ export const Table: FC = memo((props: Props) => { {headerGroups.map((headerGroup: HeaderGroup) => { const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps(); return ( -
+
{headerGroup.headers.map((column: Column, index: number) => renderHeaderCell(column, tableStyles, data.fields[index]) )} diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 4129d6a5a4f..77e03416fb7 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -9,6 +9,7 @@ import { FieldConfigSource, GrafanaTheme } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { HorizontalGroup, + InlineSwitch, ModalsController, PageToolbar, RadioButtonGroup, @@ -38,6 +39,7 @@ import { } from './state/actions'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; +import { toggleTableView } from './state/reducers'; import { getPanelEditorTabs } from './state/selectors'; import { getPanelStateById } from '../../state/selectors'; @@ -59,6 +61,7 @@ import { saveAndRefreshLibraryPanel, } from '../../../library-panels/utils'; import { notifyApp } from '../../../../core/actions'; +import { PanelEditorTableView } from './PanelEditorTableView'; interface OwnProps { dashboard: DashboardModel; @@ -75,6 +78,7 @@ const mapStateToProps = (state: StoreState) => { panel, initDone: state.panelEditor.initDone, uiState: state.panelEditor.ui, + tableViewEnabled: state.panelEditor.tableViewEnabled, variables: getVariables(state), }; }; @@ -87,6 +91,7 @@ const mapDispatchToProps = { discardPanelChanges, updatePanelEditorUIState, updateTimeZoneForSession, + toggleTableView, notifyApp, }; @@ -218,13 +223,17 @@ export class PanelEditorUnconnected extends PureComponent { }); }; + onToggleTableView = () => { + this.props.toggleTableView(); + }; + onTogglePanelOptions = () => { const { uiState, updatePanelEditorUIState } = this.props; updatePanelEditorUIState({ isPanelOptionsVisible: !uiState.isPanelOptionsVisible }); }; renderPanel = (styles: EditorStyles) => { - const { dashboard, panel, uiState, plugin, tab } = this.props; + const { dashboard, panel, uiState, plugin, tab, tableViewEnabled } = this.props; const tabs = getPanelEditorTabs(tab, plugin); return ( @@ -236,6 +245,11 @@ export class PanelEditorUnconnected extends PureComponent { if (width < 3 || height < 3) { return null; } + + if (tableViewEnabled) { + return ; + } + return (
@@ -290,12 +304,21 @@ export class PanelEditorUnconnected extends PureComponent { } renderPanelToolbar(styles: EditorStyles) { - const { dashboard, uiState, variables, updateTimeZoneForSession, panel } = this.props; + const { dashboard, uiState, variables, updateTimeZoneForSession, panel, tableViewEnabled } = this.props; + return (
0 ? 'space-between' : 'flex-end'} align="flex-start"> {this.renderTemplateVariables(styles)} + {!uiState.isPanelOptionsVisible && } diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx new file mode 100644 index 00000000000..42ecc77740b --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx @@ -0,0 +1,40 @@ +import { PanelChrome } from '@grafana/ui'; +import { PanelRenderer } from 'app/features/panel/PanelRenderer'; +import React, { useState } from 'react'; +import { PanelModel } from '../../state'; +import { usePanelLatestData } from './usePanelLatestData'; +import { PanelOptions } from 'app/plugins/panel/table/models.gen'; + +interface Props { + width: number; + height: number; + panel: PanelModel; +} + +export function PanelEditorTableView({ width, height, panel }: Props) { + const { data } = usePanelLatestData(panel, { withTransforms: true, withFieldConfig: false }, true); + const [options, setOptions] = useState({ + frameIndex: 0, + showHeader: true, + }); + + if (!data) { + return null; + } + + return ( + + {(innerWidth, innerHeight) => ( + + )} + + ); +} diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index ad56b99439c..c45a8f422c8 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -66,6 +66,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane return ( onPanelConfigChange('transparent', e.currentTarget.checked)} /> ); diff --git a/public/app/features/dashboard/components/PanelEditor/state/reducers.ts b/public/app/features/dashboard/components/PanelEditor/state/reducers.ts index 239b3b840a4..2d912ee4413 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/reducers.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/reducers.ts @@ -35,6 +35,7 @@ export interface PanelEditorState { isOpen: boolean; ui: PanelEditorUIState; isVizPickerOpen: boolean; + tableViewEnabled: boolean; } export const initialState = (): PanelEditorState => { @@ -58,6 +59,7 @@ export const initialState = (): PanelEditorState => { shouldDiscardChanges: false, isOpen: false, isVizPickerOpen: false, + tableViewEnabled: false, ui: { ...DEFAULT_PANEL_EDITOR_UI_STATE, ...migratedState, @@ -101,10 +103,14 @@ const pluginsSlice = createSlice({ state.ui.isPanelOptionsVisible = true; } }, + toggleTableView(state) { + state.tableViewEnabled = !state.tableViewEnabled; + }, closeCompleted: (state) => { state.isOpen = false; state.initDone = false; state.isVizPickerOpen = false; + state.tableViewEnabled = false; }, }, }); @@ -116,6 +122,7 @@ export const { closeCompleted, setPanelEditorUIState, toggleVizPicker, + toggleTableView, } = pluginsSlice.actions; export const panelEditorReducer = pluginsSlice.reducer; diff --git a/public/app/features/dashboard/components/PanelEditor/types.ts b/public/app/features/dashboard/components/PanelEditor/types.ts index 4bccde08024..b4017c62423 100644 --- a/public/app/features/dashboard/components/PanelEditor/types.ts +++ b/public/app/features/dashboard/components/PanelEditor/types.ts @@ -21,10 +21,23 @@ export enum DisplayMode { Exact = 2, } +export enum PanelEditTableToggle { + Off = 0, + Table = 1, +} + export const displayModes = [ { value: DisplayMode.Fill, label: 'Fill', description: 'Use all available space' }, - { value: DisplayMode.Fit, label: 'Fit', description: 'Fit in the space keeping ratio' }, - { value: DisplayMode.Exact, label: 'Exact', description: 'Make same size as the dashboard' }, + { value: DisplayMode.Exact, label: 'Actual', description: 'Make same size as on the dashboard' }, +]; + +export const panelEditTableModes = [ + { + value: PanelEditTableToggle.Off, + label: 'Visualization', + description: 'Show using selected visualization', + }, + { value: PanelEditTableToggle.Table, label: 'Table', description: 'Show raw data in table form' }, ]; /** @internal */ diff --git a/public/app/features/panel/PanelRenderer.tsx b/public/app/features/panel/PanelRenderer.tsx index 4d9430cb505..7e0bb242293 100644 --- a/public/app/features/panel/PanelRenderer.tsx +++ b/public/app/features/panel/PanelRenderer.tsx @@ -16,15 +16,15 @@ export function PanelRenderer

(pr width, height, title, - onOptionsChange, + onOptionsChange = () => {}, onChangeTimeRange = () => {}, fieldConfig: config = { defaults: {}, overrides: [] }, } = props; const [fieldConfig, setFieldConfig] = useState(config); const { value: plugin, error, loading } = useAsync(() => importPanelPlugin(pluginId), [pluginId]); - const defaultOptions = useOptionDefaults(plugin, options, fieldConfig); - const dataWithOverrides = useFieldOverrides(plugin, defaultOptions, data, timeZone); + const optionsWithDefaults = useOptionDefaults(plugin, options, fieldConfig); + const dataWithOverrides = useFieldOverrides(plugin, optionsWithDefaults, data, timeZone); if (error) { return

Failed to load plugin: {error.message}
; @@ -51,7 +51,7 @@ export function PanelRenderer

(pr title={title} timeRange={dataWithOverrides.timeRange} timeZone={timeZone} - options={options} + options={optionsWithDefaults!.options} fieldConfig={fieldConfig} transparent={false} width={width}