mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
a4a9715237
commit
65942efb95
@ -20,8 +20,8 @@ datasources:
|
||||
url: http://localhost:3011
|
||||
|
||||
- name: gdev-testdata
|
||||
type: testdata
|
||||
isDefault: true
|
||||
type: testdata
|
||||
|
||||
- name: gdev-influxdb
|
||||
type: influxdb
|
||||
|
@ -74,6 +74,7 @@ export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta>
|
||||
defaults?: TOptions;
|
||||
onPanelMigration?: PanelMigrationHandler<TOptions>;
|
||||
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
|
||||
noPadding?: boolean;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
return this;
|
||||
}
|
||||
|
||||
setNoPadding() {
|
||||
this.noPadding = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called before the panel first loads if
|
||||
* the current version is different than the version that was saved.
|
||||
|
@ -160,7 +160,7 @@ export function calculateLayout(props: Props): LayoutResult {
|
||||
export function getTitleStyles(layout: LayoutResult) {
|
||||
const styles: CSSProperties = {
|
||||
fontSize: `${layout.titleFontSize}px`,
|
||||
textShadow: '#333 1px 1px 5px',
|
||||
textShadow: '#333 0px 0px 1px',
|
||||
color: '#EEE',
|
||||
};
|
||||
|
||||
@ -175,7 +175,8 @@ export function getValueStyles(layout: LayoutResult) {
|
||||
const styles: CSSProperties = {
|
||||
fontSize: `${layout.valueFontSize}px`,
|
||||
color: '#EEE',
|
||||
textShadow: '#333 1px 1px 5px',
|
||||
textShadow: '#333 0px 0px 1px',
|
||||
fontWeight: 500,
|
||||
lineHeight: LINE_HEIGHT,
|
||||
};
|
||||
|
||||
@ -347,7 +348,7 @@ function renderLineGeom(layout: LayoutResult) {
|
||||
const lineStyle: any = {
|
||||
stroke: '#CCC',
|
||||
lineWidth: 2,
|
||||
shadowBlur: 15,
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#444',
|
||||
shadowOffsetY: 7,
|
||||
};
|
||||
@ -359,7 +360,7 @@ function renderVibrant2Geom(layout: LayoutResult) {
|
||||
const lineStyle: any = {
|
||||
stroke: '#CCC',
|
||||
lineWidth: 2,
|
||||
shadowBlur: 15,
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#444',
|
||||
shadowOffsetY: -5,
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
|
||||
interface Props<V, D> {
|
||||
@ -87,12 +87,13 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
|
||||
repeaterStyle.flexDirection = 'column';
|
||||
itemStyles.marginBottom = `${itemSpacing}px`;
|
||||
vizWidth = width;
|
||||
vizHeight = height / values.length - itemSpacing;
|
||||
vizHeight = height / values.length - itemSpacing + itemSpacing / values.length;
|
||||
} else {
|
||||
repeaterStyle.flexDirection = 'row';
|
||||
repeaterStyle.justifyContent = 'space-between';
|
||||
itemStyles.marginRight = `${itemSpacing}px`;
|
||||
vizHeight = height;
|
||||
vizWidth = width / values.length - itemSpacing;
|
||||
vizWidth = width / values.length - itemSpacing + itemSpacing / values.length;
|
||||
}
|
||||
|
||||
itemStyles.width = `${vizWidth}px`;
|
||||
@ -103,7 +104,7 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
|
||||
<div style={repeaterStyle}>
|
||||
{values.map((value, index) => {
|
||||
return (
|
||||
<div key={index} style={itemStyles}>
|
||||
<div key={index} style={getItemStylesForIndex(itemStyles, index, values.length)}>
|
||||
{renderValue(value, vizWidth, vizHeight, dims)}
|
||||
</div>
|
||||
);
|
||||
@ -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';
|
||||
import { ErrorBoundary } from '@grafana/ui';
|
||||
// Utils & Services
|
||||
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 { getProcessedDataFrames } from '../state/runRequest';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import config from 'app/core/config';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import {
|
||||
LoadingState,
|
||||
ScopedVars,
|
||||
@ -260,13 +261,22 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const PanelComponent = plugin.panel;
|
||||
const innerPanelHeight = calculateInnerPanelHeight(panel, height);
|
||||
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 (
|
||||
<>
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
<div className="panel-content">
|
||||
<div className={panelContentClassNames}>
|
||||
<PanelComponent
|
||||
id={panel.id}
|
||||
data={data}
|
||||
@ -274,7 +284,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
timeZone={this.props.dashboard.getTimezone()}
|
||||
options={panel.getOptions()}
|
||||
transparent={panel.transparent}
|
||||
width={width - theme.panelPadding * 2}
|
||||
width={panelWidth}
|
||||
height={innerPanelHeight}
|
||||
renderCounter={renderCounter}
|
||||
replaceVariables={this.replaceVariables}
|
||||
@ -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() {
|
||||
const { dashboard, panel, isFullscreen, width, height } = this.props;
|
||||
const { errorMessage, data } = this.state;
|
||||
@ -302,8 +329,8 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
const containerClassNames = classNames({
|
||||
'panel-container': true,
|
||||
'panel-container--absolute': true,
|
||||
'panel-container--no-title': !panel.hasTitle(),
|
||||
'panel-transparent': transparent,
|
||||
'panel-container--transparent': transparent,
|
||||
'panel-container--no-title': this.hasOverlayHeader(),
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -103,13 +103,11 @@ export class PanelHeader extends Component<Props, State> {
|
||||
<span className="panel-title-text">
|
||||
{title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||
</span>
|
||||
|
||||
{this.state.panelMenuOpen && (
|
||||
<ClickOutsideWrapper onClick={this.closeMenu}>
|
||||
<PanelHeaderMenu panel={panel} dashboard={dashboard} />
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
|
||||
{timeInfo && (
|
||||
<span className="panel-time-info">
|
||||
<i className="fa fa-clock-o" /> {timeInfo}
|
||||
|
@ -1,7 +1,9 @@
|
||||
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 { dateTime, DateTime } from '@grafana/data';
|
||||
import { PanelModel } from '../state';
|
||||
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
|
||||
|
||||
const dashboardTimeRange: TimeRange = {
|
||||
from: dateTime([2019, 1, 11, 12, 0]),
|
||||
@ -71,4 +73,19 @@ describe('applyPanelTimeOverrides', () => {
|
||||
expect((overrides.timeRange.raw.from as DateTime).toISOString()).toEqual(expectedFromDate.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 {
|
||||
}
|
||||
|
||||
export function calculateInnerPanelHeight(panel: PanelModel, containerHeight: number): number {
|
||||
return (
|
||||
containerHeight -
|
||||
(panel.hasTitle() ? config.theme.panelHeaderHeight : 0) -
|
||||
config.theme.panelPadding * 2 -
|
||||
PANEL_BORDER
|
||||
);
|
||||
const chromePadding = panel.plugin && panel.plugin.noPadding ? 0 : config.theme.panelPadding * 2;
|
||||
const headerHeight = panel.hasTitle() ? config.theme.panelHeaderHeight : 0;
|
||||
return containerHeight - headerHeight - chromePadding - PANEL_BORDER;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
|
||||
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 {
|
||||
getDataLinksVariableSuggestions,
|
||||
@ -52,7 +52,6 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
||||
});
|
||||
|
||||
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) => {
|
||||
this.onDisplayOptionsChanged({
|
||||
@ -91,21 +90,16 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
||||
value={displayModes.find(item => item.value === options.displayMode)}
|
||||
/>
|
||||
</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} />
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<PanelOptionsGroup title="Field (default)">
|
||||
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
|
||||
<PanelOptionsGroup title="Field">
|
||||
<FieldPropertiesEditor
|
||||
showMinMax={true}
|
||||
onChange={this.onDefaultsChange}
|
||||
value={defaults}
|
||||
showTitle={true}
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
|
@ -7,5 +7,6 @@ import { SingleStatEditor } from './SingleStatEditor';
|
||||
export const plugin = new PanelPlugin<SingleStatOptions>(SingleStatPanel)
|
||||
.setDefaults(defaults)
|
||||
.setEditor(SingleStatEditor)
|
||||
.setNoPadding()
|
||||
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler)
|
||||
.setMigrationHandler(sharedSingleStatMigrationHandler);
|
||||
|
@ -8,27 +8,15 @@ export interface SparklineOptions {
|
||||
// Structure copied from angular
|
||||
export interface SingleStatOptions extends SingleStatBaseOptions {
|
||||
sparkline: SparklineOptions;
|
||||
colorMode: ColorMode;
|
||||
displayMode: SingleStatDisplayMode;
|
||||
}
|
||||
|
||||
export const displayModes: Array<SelectableValue<SingleStatDisplayMode>> = [
|
||||
{ value: SingleStatDisplayMode.Classic, label: 'Classic' },
|
||||
{ value: SingleStatDisplayMode.Classic2, label: 'Classic 2' },
|
||||
{ value: SingleStatDisplayMode.Vibrant, label: 'Vibrant' },
|
||||
{ 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 = {
|
||||
values: false,
|
||||
calcs: [ReducerID.mean],
|
||||
@ -48,7 +36,6 @@ export const defaults: SingleStatOptions = {
|
||||
sparkline: {
|
||||
show: true,
|
||||
},
|
||||
colorMode: ColorMode.Thresholds,
|
||||
displayMode: SingleStatDisplayMode.Vibrant,
|
||||
fieldOptions: standardFieldDisplayOptions,
|
||||
orientation: VizOrientation.Auto,
|
||||
|
@ -55,8 +55,8 @@ div.flot-text {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.panel-transparent {
|
||||
background-color: transparent;
|
||||
&--transparent {
|
||||
background-color: $page-bg;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@ -82,8 +82,11 @@ div.flot-text {
|
||||
height: calc(100% - #{$panel-header-height});
|
||||
width: 100%;
|
||||
position: relative;
|
||||
// Fixes scrolling on mobile devices
|
||||
overflow: auto;
|
||||
|
||||
&--no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For larger screens, set back to hidden to avoid double scroll bars
|
||||
|
Loading…
Reference in New Issue
Block a user