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}