Commit 65942efb by Torkel Ödegaard Committed by GitHub

Panels: Add support for panels with no padding (#20012)

* Panels: Added support to set panel padding to zero

* WIP: fullChromeControl work

* Tweaks to header position

* Reverted some overlay mechanic and now back to no title only

* Fixed test

* Fixed transparent flag

* Added show title

* Added font weight to value

* Reverted back to no padding option

* Fixed issue with border and width and height
parent a4a97152
...@@ -20,8 +20,8 @@ datasources: ...@@ -20,8 +20,8 @@ datasources:
url: http://localhost:3011 url: http://localhost:3011
- name: gdev-testdata - name: gdev-testdata
type: testdata
isDefault: true isDefault: true
type: testdata
- name: gdev-influxdb - name: gdev-influxdb
type: influxdb type: influxdb
......
...@@ -74,6 +74,7 @@ export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> ...@@ -74,6 +74,7 @@ export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta>
defaults?: TOptions; defaults?: TOptions;
onPanelMigration?: PanelMigrationHandler<TOptions>; onPanelMigration?: PanelMigrationHandler<TOptions>;
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>; onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
noPadding?: boolean;
/** /**
* Legacy angular ctrl. If this exists it will be used instead of the panel * Legacy angular ctrl. If this exists it will be used instead of the panel
...@@ -95,6 +96,11 @@ export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> ...@@ -95,6 +96,11 @@ export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta>
return this; return this;
} }
setNoPadding() {
this.noPadding = true;
return this;
}
/** /**
* This function is called before the panel first loads if * This function is called before the panel first loads if
* the current version is different than the version that was saved. * the current version is different than the version that was saved.
......
...@@ -160,7 +160,7 @@ export function calculateLayout(props: Props): LayoutResult { ...@@ -160,7 +160,7 @@ export function calculateLayout(props: Props): LayoutResult {
export function getTitleStyles(layout: LayoutResult) { export function getTitleStyles(layout: LayoutResult) {
const styles: CSSProperties = { const styles: CSSProperties = {
fontSize: `${layout.titleFontSize}px`, fontSize: `${layout.titleFontSize}px`,
textShadow: '#333 1px 1px 5px', textShadow: '#333 0px 0px 1px',
color: '#EEE', color: '#EEE',
}; };
...@@ -175,7 +175,8 @@ export function getValueStyles(layout: LayoutResult) { ...@@ -175,7 +175,8 @@ export function getValueStyles(layout: LayoutResult) {
const styles: CSSProperties = { const styles: CSSProperties = {
fontSize: `${layout.valueFontSize}px`, fontSize: `${layout.valueFontSize}px`,
color: '#EEE', color: '#EEE',
textShadow: '#333 1px 1px 5px', textShadow: '#333 0px 0px 1px',
fontWeight: 500,
lineHeight: LINE_HEIGHT, lineHeight: LINE_HEIGHT,
}; };
...@@ -347,7 +348,7 @@ function renderLineGeom(layout: LayoutResult) { ...@@ -347,7 +348,7 @@ function renderLineGeom(layout: LayoutResult) {
const lineStyle: any = { const lineStyle: any = {
stroke: '#CCC', stroke: '#CCC',
lineWidth: 2, lineWidth: 2,
shadowBlur: 15, shadowBlur: 10,
shadowColor: '#444', shadowColor: '#444',
shadowOffsetY: 7, shadowOffsetY: 7,
}; };
...@@ -359,7 +360,7 @@ function renderVibrant2Geom(layout: LayoutResult) { ...@@ -359,7 +360,7 @@ function renderVibrant2Geom(layout: LayoutResult) {
const lineStyle: any = { const lineStyle: any = {
stroke: '#CCC', stroke: '#CCC',
lineWidth: 2, lineWidth: 2,
shadowBlur: 15, shadowBlur: 10,
shadowColor: '#444', shadowColor: '#444',
shadowOffsetY: -5, shadowOffsetY: -5,
}; };
......
import React, { PureComponent } from 'react'; import React, { PureComponent, CSSProperties } from 'react';
import { VizOrientation } from '@grafana/data'; import { VizOrientation } from '@grafana/data';
interface Props<V, D> { interface Props<V, D> {
...@@ -87,12 +87,13 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>> ...@@ -87,12 +87,13 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
repeaterStyle.flexDirection = 'column'; repeaterStyle.flexDirection = 'column';
itemStyles.marginBottom = `${itemSpacing}px`; itemStyles.marginBottom = `${itemSpacing}px`;
vizWidth = width; vizWidth = width;
vizHeight = height / values.length - itemSpacing; vizHeight = height / values.length - itemSpacing + itemSpacing / values.length;
} else { } else {
repeaterStyle.flexDirection = 'row'; repeaterStyle.flexDirection = 'row';
repeaterStyle.justifyContent = 'space-between';
itemStyles.marginRight = `${itemSpacing}px`; itemStyles.marginRight = `${itemSpacing}px`;
vizHeight = height; vizHeight = height;
vizWidth = width / values.length - itemSpacing; vizWidth = width / values.length - itemSpacing + itemSpacing / values.length;
} }
itemStyles.width = `${vizWidth}px`; itemStyles.width = `${vizWidth}px`;
...@@ -103,7 +104,7 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>> ...@@ -103,7 +104,7 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
<div style={repeaterStyle}> <div style={repeaterStyle}>
{values.map((value, index) => { {values.map((value, index) => {
return ( return (
<div key={index} style={itemStyles}> <div key={index} style={getItemStylesForIndex(itemStyles, index, values.length)}>
{renderValue(value, vizWidth, vizHeight, dims)} {renderValue(value, vizWidth, vizHeight, dims)}
</div> </div>
); );
...@@ -112,3 +113,17 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>> ...@@ -112,3 +113,17 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
); );
} }
} }
/*
* Removes any padding on the last item
*/
function getItemStylesForIndex(itemStyles: CSSProperties, index: number, length: number): CSSProperties {
if (index === length - 1) {
return {
...itemStyles,
marginRight: 0,
marginBottom: 0,
};
}
return itemStyles;
}
...@@ -7,13 +7,14 @@ import { PanelHeader } from './PanelHeader/PanelHeader'; ...@@ -7,13 +7,14 @@ import { PanelHeader } from './PanelHeader/PanelHeader';
import { ErrorBoundary } from '@grafana/ui'; import { ErrorBoundary } from '@grafana/ui';
// Utils & Services // Utils & Services
import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
import { applyPanelTimeOverrides, calculateInnerPanelHeight } from 'app/features/dashboard/utils/panel'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { profiler } from 'app/core/profiler'; import { profiler } from 'app/core/profiler';
import { getProcessedDataFrames } from '../state/runRequest'; import { getProcessedDataFrames } from '../state/runRequest';
import templateSrv from 'app/features/templating/template_srv'; import templateSrv from 'app/features/templating/template_srv';
import config from 'app/core/config'; import config from 'app/core/config';
// Types // Types
import { DashboardModel, PanelModel } from '../state'; import { DashboardModel, PanelModel } from '../state';
import { PANEL_BORDER } from 'app/core/constants';
import { import {
LoadingState, LoadingState,
ScopedVars, ScopedVars,
...@@ -260,13 +261,22 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -260,13 +261,22 @@ export class PanelChrome extends PureComponent<Props, State> {
} }
const PanelComponent = plugin.panel; const PanelComponent = plugin.panel;
const innerPanelHeight = calculateInnerPanelHeight(panel, height);
const timeRange = data.timeRange || this.timeSrv.timeRange(); const timeRange = data.timeRange || this.timeSrv.timeRange();
const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight;
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
const panelWidth = width - chromePadding * 2 - PANEL_BORDER;
const innerPanelHeight = height - headerHeight - chromePadding * 2 - PANEL_BORDER;
const panelContentClassNames = classNames({
'panel-content': true,
'panel-content--no-padding': plugin.noPadding,
});
return ( return (
<> <>
{loading === LoadingState.Loading && this.renderLoadingState()} {loading === LoadingState.Loading && this.renderLoadingState()}
<div className="panel-content"> <div className={panelContentClassNames}>
<PanelComponent <PanelComponent
id={panel.id} id={panel.id}
data={data} data={data}
...@@ -274,7 +284,7 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -274,7 +284,7 @@ export class PanelChrome extends PureComponent<Props, State> {
timeZone={this.props.dashboard.getTimezone()} timeZone={this.props.dashboard.getTimezone()}
options={panel.getOptions()} options={panel.getOptions()}
transparent={panel.transparent} transparent={panel.transparent}
width={width - theme.panelPadding * 2} width={panelWidth}
height={innerPanelHeight} height={innerPanelHeight}
renderCounter={renderCounter} renderCounter={renderCounter}
replaceVariables={this.replaceVariables} replaceVariables={this.replaceVariables}
...@@ -294,6 +304,23 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -294,6 +304,23 @@ export class PanelChrome extends PureComponent<Props, State> {
); );
} }
hasOverlayHeader() {
const { panel } = this.props;
const { errorMessage, data } = this.state;
// always show normal header if we have an error message
if (errorMessage) {
return false;
}
// always show normal header if we have time override
if (data.request && data.request.timeInfo) {
return false;
}
return !panel.hasTitle();
}
render() { render() {
const { dashboard, panel, isFullscreen, width, height } = this.props; const { dashboard, panel, isFullscreen, width, height } = this.props;
const { errorMessage, data } = this.state; const { errorMessage, data } = this.state;
...@@ -302,8 +329,8 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -302,8 +329,8 @@ export class PanelChrome extends PureComponent<Props, State> {
const containerClassNames = classNames({ const containerClassNames = classNames({
'panel-container': true, 'panel-container': true,
'panel-container--absolute': true, 'panel-container--absolute': true,
'panel-container--no-title': !panel.hasTitle(), 'panel-container--transparent': transparent,
'panel-transparent': transparent, 'panel-container--no-title': this.hasOverlayHeader(),
}); });
return ( return (
......
...@@ -103,13 +103,11 @@ export class PanelHeader extends Component<Props, State> { ...@@ -103,13 +103,11 @@ export class PanelHeader extends Component<Props, State> {
<span className="panel-title-text"> <span className="panel-title-text">
{title} <span className="fa fa-caret-down panel-menu-toggle" /> {title} <span className="fa fa-caret-down panel-menu-toggle" />
</span> </span>
{this.state.panelMenuOpen && ( {this.state.panelMenuOpen && (
<ClickOutsideWrapper onClick={this.closeMenu}> <ClickOutsideWrapper onClick={this.closeMenu}>
<PanelHeaderMenu panel={panel} dashboard={dashboard} /> <PanelHeaderMenu panel={panel} dashboard={dashboard} />
</ClickOutsideWrapper> </ClickOutsideWrapper>
)} )}
{timeInfo && ( {timeInfo && (
<span className="panel-time-info"> <span className="panel-time-info">
<i className="fa fa-clock-o" /> {timeInfo} <i className="fa fa-clock-o" /> {timeInfo}
......
import { TimeRange } from '@grafana/data'; import { TimeRange } from '@grafana/data';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { applyPanelTimeOverrides, calculateInnerPanelHeight } from 'app/features/dashboard/utils/panel';
import { advanceTo, clear } from 'jest-date-mock'; import { advanceTo, clear } from 'jest-date-mock';
import { dateTime, DateTime } from '@grafana/data'; import { dateTime, DateTime } from '@grafana/data';
import { PanelModel } from '../state';
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
const dashboardTimeRange: TimeRange = { const dashboardTimeRange: TimeRange = {
from: dateTime([2019, 1, 11, 12, 0]), from: dateTime([2019, 1, 11, 12, 0]),
...@@ -71,4 +73,19 @@ describe('applyPanelTimeOverrides', () => { ...@@ -71,4 +73,19 @@ describe('applyPanelTimeOverrides', () => {
expect((overrides.timeRange.raw.from as DateTime).toISOString()).toEqual(expectedFromDate.toISOString()); expect((overrides.timeRange.raw.from as DateTime).toISOString()).toEqual(expectedFromDate.toISOString());
expect((overrides.timeRange.raw.to as DateTime).toISOString()).toEqual(expectedToDate.toISOString()); expect((overrides.timeRange.raw.to as DateTime).toISOString()).toEqual(expectedToDate.toISOString());
}); });
it('Calculate panel height', () => {
const panelModel = new PanelModel({});
const height = calculateInnerPanelHeight(panelModel, 100);
expect(height).toBe(82);
});
it('Calculate panel height with panel plugin zeroChromePadding', () => {
const panelModel = new PanelModel({});
panelModel.pluginLoaded(getPanelPlugin({ id: 'table' }, null, null).setNoPadding());
const height = calculateInnerPanelHeight(panelModel, 100);
expect(height).toBe(98);
});
}); });
...@@ -173,10 +173,7 @@ export function getResolution(panel: PanelModel): number { ...@@ -173,10 +173,7 @@ export function getResolution(panel: PanelModel): number {
} }
export function calculateInnerPanelHeight(panel: PanelModel, containerHeight: number): number { export function calculateInnerPanelHeight(panel: PanelModel, containerHeight: number): number {
return ( const chromePadding = panel.plugin && panel.plugin.noPadding ? 0 : config.theme.panelPadding * 2;
containerHeight - const headerHeight = panel.hasTitle() ? config.theme.panelHeaderHeight : 0;
(panel.hasTitle() ? config.theme.panelHeaderHeight : 0) - return containerHeight - headerHeight - chromePadding - PANEL_BORDER;
config.theme.panelPadding * 2 -
PANEL_BORDER
);
} }
...@@ -15,7 +15,7 @@ import { ...@@ -15,7 +15,7 @@ import {
import { Threshold, ValueMapping, FieldConfig, DataLink, PanelEditorProps, FieldDisplayOptions } from '@grafana/data'; import { Threshold, ValueMapping, FieldConfig, DataLink, PanelEditorProps, FieldDisplayOptions } from '@grafana/data';
import { SingleStatOptions, SparklineOptions, displayModes, colorModes } from './types'; import { SingleStatOptions, SparklineOptions, displayModes } from './types';
import { SparklineEditor } from './SparklineEditor'; import { SparklineEditor } from './SparklineEditor';
import { import {
getDataLinksVariableSuggestions, getDataLinksVariableSuggestions,
...@@ -52,7 +52,6 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO ...@@ -52,7 +52,6 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
}); });
onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value }); onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
onColorModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, colorMode: value });
onDefaultsChange = (field: FieldConfig) => { onDefaultsChange = (field: FieldConfig) => {
this.onDisplayOptionsChanged({ this.onDisplayOptionsChanged({
...@@ -91,21 +90,16 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO ...@@ -91,21 +90,16 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
value={displayModes.find(item => item.value === options.displayMode)} value={displayModes.find(item => item.value === options.displayMode)}
/> />
</div> </div>
<div className="form-field">
<FormLabel width={8}>Color by</FormLabel>
<Select
width={12}
options={colorModes}
defaultValue={colorModes[0]}
onChange={this.onColorModeChange}
value={colorModes.find(item => item.value === options.colorMode)}
/>
</div>
<SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} /> <SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
</PanelOptionsGroup> </PanelOptionsGroup>
<PanelOptionsGroup title="Field (default)"> <PanelOptionsGroup title="Field">
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} /> <FieldPropertiesEditor
showMinMax={true}
onChange={this.onDefaultsChange}
value={defaults}
showTitle={true}
/>
</PanelOptionsGroup> </PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} /> <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
......
...@@ -7,5 +7,6 @@ import { SingleStatEditor } from './SingleStatEditor'; ...@@ -7,5 +7,6 @@ import { SingleStatEditor } from './SingleStatEditor';
export const plugin = new PanelPlugin<SingleStatOptions>(SingleStatPanel) export const plugin = new PanelPlugin<SingleStatOptions>(SingleStatPanel)
.setDefaults(defaults) .setDefaults(defaults)
.setEditor(SingleStatEditor) .setEditor(SingleStatEditor)
.setNoPadding()
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler) .setPanelChangeHandler(sharedSingleStatPanelChangedHandler)
.setMigrationHandler(sharedSingleStatMigrationHandler); .setMigrationHandler(sharedSingleStatMigrationHandler);
...@@ -8,27 +8,15 @@ export interface SparklineOptions { ...@@ -8,27 +8,15 @@ export interface SparklineOptions {
// Structure copied from angular // Structure copied from angular
export interface SingleStatOptions extends SingleStatBaseOptions { export interface SingleStatOptions extends SingleStatBaseOptions {
sparkline: SparklineOptions; sparkline: SparklineOptions;
colorMode: ColorMode;
displayMode: SingleStatDisplayMode; displayMode: SingleStatDisplayMode;
} }
export const displayModes: Array<SelectableValue<SingleStatDisplayMode>> = [ export const displayModes: Array<SelectableValue<SingleStatDisplayMode>> = [
{ value: SingleStatDisplayMode.Classic, label: 'Classic' }, { value: SingleStatDisplayMode.Classic, label: 'Classic' },
{ value: SingleStatDisplayMode.Classic2, label: 'Classic 2' },
{ value: SingleStatDisplayMode.Vibrant, label: 'Vibrant' }, { value: SingleStatDisplayMode.Vibrant, label: 'Vibrant' },
{ value: SingleStatDisplayMode.Vibrant2, label: 'Vibrant 2' }, { value: SingleStatDisplayMode.Vibrant2, label: 'Vibrant 2' },
]; ];
export enum ColorMode {
Thresholds,
Series,
}
export const colorModes: Array<SelectableValue<ColorMode>> = [
{ value: ColorMode.Thresholds, label: 'Thresholds' },
{ value: ColorMode.Series, label: 'Series' },
];
export const standardFieldDisplayOptions: FieldDisplayOptions = { export const standardFieldDisplayOptions: FieldDisplayOptions = {
values: false, values: false,
calcs: [ReducerID.mean], calcs: [ReducerID.mean],
...@@ -48,7 +36,6 @@ export const defaults: SingleStatOptions = { ...@@ -48,7 +36,6 @@ export const defaults: SingleStatOptions = {
sparkline: { sparkline: {
show: true, show: true,
}, },
colorMode: ColorMode.Thresholds,
displayMode: SingleStatDisplayMode.Vibrant, displayMode: SingleStatDisplayMode.Vibrant,
fieldOptions: standardFieldDisplayOptions, fieldOptions: standardFieldDisplayOptions,
orientation: VizOrientation.Auto, orientation: VizOrientation.Auto,
......
...@@ -55,8 +55,8 @@ div.flot-text { ...@@ -55,8 +55,8 @@ div.flot-text {
height: 100%; height: 100%;
width: 100%; width: 100%;
&.panel-transparent { &--transparent {
background-color: transparent; background-color: $page-bg;
border: none; border: none;
} }
...@@ -82,8 +82,11 @@ div.flot-text { ...@@ -82,8 +82,11 @@ div.flot-text {
height: calc(100% - #{$panel-header-height}); height: calc(100% - #{$panel-header-height});
width: 100%; width: 100%;
position: relative; position: relative;
// Fixes scrolling on mobile devices
overflow: auto; overflow: auto;
&--no-padding {
padding: 0;
}
} }
// For larger screens, set back to hidden to avoid double scroll bars // For larger screens, set back to hidden to avoid double scroll bars
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment