This commit is contained in:
Hugo Häggmark 2019-01-11 11:41:08 +01:00
commit d97cd450c9
46 changed files with 216 additions and 150 deletions

View File

@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
"list": []
},
"refresh": "5s",
"schemaVersion": 16,
"schemaVersion": 17,
"version": 0,
"links": []
}

View File

@ -292,9 +292,11 @@ The `direction` controls how the panels will be arranged.
By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
panel.
By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
Set `Max per row` to tell grafana how many panels per row you want at most. It defaults to *4* if you don't set anything.
By choosing `vertical` the panels will be arranged from top to bottom in a column. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.

View File

@ -0,0 +1,11 @@
import React, { SFC } from 'react';
interface LoadingPlaceholderProps {
text: string;
}
export const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
<div className="gf-form-group">
{text} <i className="fa fa-spinner fa-spin" />
</div>
);

View File

@ -1,7 +1,10 @@
import React from 'react';
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
// @ts-ignore
import { components } from '@torkelo/react-select';
export const IndicatorsContainer = props => {
export const IndicatorsContainer = (props: any) => {
const isOpen = props.selectProps.menuIsOpen;
return (
<components.IndicatorsContainer {...props}>

View File

@ -1,5 +1,9 @@
import React from 'react';
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
// @ts-ignore
import { components } from '@torkelo/react-select';
// @ts-ignore
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
export interface Props {

View File

@ -1,16 +1,21 @@
// Libraries
import classNames from 'classnames';
import React, { PureComponent } from 'react';
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
// @ts-ignore
import { default as ReactSelect } from '@torkelo/react-select';
// @ts-ignore
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
// @ts-ignore
import { components } from '@torkelo/react-select';
// Components
import { Option, SingleValue } from './PickerOption';
import OptionGroup from './OptionGroup';
import { SelectOption, SingleValue } from './SelectOption';
import SelectOptionGroup from './SelectOptionGroup';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
import ResetStyles from './ResetStyles';
import resetSelectStyles from './resetSelectStyles';
import { CustomScrollbar } from '@grafana/ui';
export interface SelectOptionItem {
@ -53,7 +58,7 @@ interface AsyncProps {
loadingMessage?: () => string;
}
export const MenuList = props => {
export const MenuList = (props: any) => {
return (
<components.MenuList {...props}>
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
classNamePrefix="gf-form-select-box"
className={selectClassNames}
components={{
Option,
Option: SelectOption,
SingleValue,
IndicatorsContainer,
MenuList,
Group: OptionGroup,
Group: SelectOptionGroup,
}}
defaultValue={defaultValue}
value={value}
@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
onChange={onChange}
options={options}
placeholder={placeholder || 'Choose'}
styles={ResetStyles}
styles={resetSelectStyles()}
isDisabled={isDisabled}
isLoading={isLoading}
isClearable={isClearable}
@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
isLoading={isLoading}
defaultOptions={defaultOptions}
placeholder={placeholder || 'Choose'}
styles={ResetStyles}
styles={resetSelectStyles()}
loadingMessage={loadingMessage}
noOptionsMessage={noOptionsMessage}
isDisabled={isDisabled}

View File

@ -1,11 +1,11 @@
import React from 'react';
import renderer from 'react-test-renderer';
import PickerOption from './PickerOption';
import SelectOption from './SelectOption';
import { OptionProps } from 'react-select/lib/components/Option';
const model = {
const model: OptionProps<any> = {
cx: jest.fn(),
clearValue: jest.fn(),
onSelect: jest.fn(),
getStyles: jest.fn(),
getValue: jest.fn(),
hasValue: true,
@ -18,21 +18,31 @@ const model = {
isFocused: false,
isSelected: false,
innerRef: null,
innerProps: null,
label: 'Option label',
type: null,
children: 'Model title',
data: {
title: 'Model title',
imgUrl: 'url/to/avatar',
label: 'User picker label',
innerProps: {
id: '',
key: '',
onClick: jest.fn(),
onMouseOver: jest.fn(),
tabIndex: 1,
},
label: 'Option label',
type: 'option',
children: 'Model title',
className: 'class-for-user-picker',
};
describe('PickerOption', () => {
describe('SelectOption', () => {
it('renders correctly', () => {
const tree = renderer.create(<PickerOption {...model} />).toJSON();
const tree = renderer
.create(
<SelectOption
{...model}
data={{
imgUrl: 'url/to/avatar',
}}
/>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@ -1,4 +1,7 @@
import React from 'react';
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
// @ts-ignore
import { components } from '@torkelo/react-select';
import { OptionProps } from 'react-select/lib/components/Option';
@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> {
};
}
export const Option = (props: ExtendedOptionProps) => {
export const SelectOption = (props: ExtendedOptionProps) => {
const { children, isSelected, data } = props;
return (
@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
};
// was not able to type this without typescript error
export const SingleValue = props => {
export const SingleValue = (props: any) => {
const { children, data } = props;
return (
@ -41,4 +44,4 @@ export const SingleValue = props => {
);
};
export default Option;
export default SelectOption;

View File

@ -9,7 +9,7 @@ interface State {
expanded: boolean;
}
export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
state = {
expanded: false,
};
@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State
}
}
componentDidUpdate(nextProps) {
componentDidUpdate(nextProps: ExtendedGroupProps) {
if (nextProps.selectProps.inputValue !== '') {
this.setState({ expanded: true });
}

View File

@ -1,7 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PickerOption renders correctly 1`] = `
<div>
exports[`SelectOption renders correctly 1`] = `
<div
id=""
onClick={[MockFunction]}
onMouseOver={[MockFunction]}
tabIndex={1}
>
<div
className="gf-form-select-box__desc-option"
>

View File

@ -0,0 +1,27 @@
export default function resetSelectStyles() {
return {
clearIndicator: () => ({}),
container: () => ({}),
control: () => ({}),
dropdownIndicator: () => ({}),
group: () => ({}),
groupHeading: () => ({}),
indicatorsContainer: () => ({}),
indicatorSeparator: () => ({}),
input: () => ({}),
loadingIndicator: () => ({}),
loadingMessage: () => ({}),
menu: () => ({}),
menuList: ({ maxHeight }: { maxHeight: number }) => ({
maxHeight,
}),
multiValue: () => ({}),
multiValueLabel: () => ({}),
multiValueRemove: () => ({}),
noOptionsMessage: () => ({}),
option: () => ({}),
placeholder: () => ({}),
singleValue: () => ({}),
valueContainer: () => ({}),
};
}

View File

@ -2,3 +2,4 @@
@import 'DeleteButton/DeleteButton';
@import 'ThresholdsEditor/ThresholdsEditor';
@import 'Tooltip/Tooltip';
@import 'Select/Select';

View File

@ -2,6 +2,14 @@ export { DeleteButton } from './DeleteButton/DeleteButton';
export { Tooltip } from './Tooltip/Tooltip';
export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
// Select
export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
export { IndicatorsContainer } from './Select/IndicatorsContainer';
export { NoOptionsMessage } from './Select/NoOptionsMessage';
export { default as resetSelectStyles } from './Select/resetSelectStyles';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';

View File

@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard {
func NewDashboardFolder(title string) *Dashboard {
folder := NewDashboard(title)
folder.IsFolder = true
folder.Data.Set("schemaVersion", 16)
folder.Data.Set("schemaVersion", 17)
folder.Data.Set("version", 0)
folder.IsFolder = true
return folder

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker';
import { Select, SelectOptionItem } from 'app/core/components/Select/Select';
import { Select, SelectOptionItem } from '@grafana/ui';
import { User } from 'app/types';
import {
dashboardPermissionLevels,

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import Select from 'app/core/components/Select/Select';
import { Select } from '@grafana/ui';
import { dashboardPermissionLevels } from 'app/types/acl';
export interface Props {

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { Select } from 'app/core/components/Select/Select';
import { Select } from '@grafana/ui';
import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
import { FolderInfo } from 'app/types';

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import _ from 'lodash';
// Components
import Select from './Select';
import { Select } from '@grafana/ui';
// Types
import { DataSourceSelectItem } from 'app/types';

View File

@ -1,25 +0,0 @@
export default {
clearIndicator: () => ({}),
container: () => ({}),
control: () => ({}),
dropdownIndicator: () => ({}),
group: () => ({}),
groupHeading: () => ({}),
indicatorsContainer: () => ({}),
indicatorSeparator: () => ({}),
input: () => ({}),
loadingIndicator: () => ({}),
loadingMessage: () => ({}),
menu: () => ({}),
menuList: ({ maxHeight }: { maxHeight: number }) => ({
maxHeight,
}),
multiValue: () => ({}),
multiValueLabel: () => ({}),
multiValueRemove: () => ({}),
noOptionsMessage: () => ({}),
option: () => ({}),
placeholder: () => ({}),
singleValue: () => ({}),
valueContainer: () => ({}),
};

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import _ from 'lodash';
import { AsyncSelect } from './Select';
import { AsyncSelect } from '@grafana/ui';
import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import Select from './Select';
import { Select } from '@grafana/ui';
import kbn from 'app/core/utils/kbn';
interface Props {

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import _ from 'lodash';
// Components
import { AsyncSelect } from './Select';
import { AsyncSelect } from '@grafana/ui';
// Utils & Services
import { debounce } from 'lodash';

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { Label } from 'app/core/components/Label/Label';
import Select from 'app/core/components/Select/Select';
import { Select } from '@grafana/ui';
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchHit } from 'app/types';

View File

@ -1,12 +1,10 @@
import React from 'react';
import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
import AsyncSelect from '@torkelo/react-select/lib/Async';
import { TagOption } from './TagOption';
import { TagBadge } from './TagBadge';
import IndicatorsContainer from 'app/core/components/Select/IndicatorsContainer';
import NoOptionsMessage from 'app/core/components/Select/NoOptionsMessage';
import { components } from '@torkelo/react-select';
import ResetStyles from 'app/core/components/Select/ResetStyles';
export interface Props {
tags: string[];
@ -51,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> {
getOptionValue: i => i.value,
getOptionLabel: i => i.label,
value: tags,
styles: ResetStyles,
styles: resetSelectStyles(),
filterOption: (option, searchQuery) => {
const regex = RegExp(searchQuery, 'i');
return regex.test(option.value);

View File

@ -0,0 +1,8 @@
import getFactors from 'app/core/utils/factors';
describe('factors', () => {
it('should return factors for 12', () => {
const factors = getFactors(12);
expect(factors).toEqual([1, 2, 3, 4, 6, 12]);
});
});

View File

@ -0,0 +1,5 @@
// Returns the factors of a number
// Example getFactors(12) -> [1, 2, 3, 4, 6, 12]
export default function getFactors(num: number): number[] {
return Array.from(new Array(num + 1), (_, i) => i).filter(i => num % i === 0);
}

View File

@ -1,5 +1,5 @@
// Libraries
import React, { PureComponent, SFC } from 'react';
import React, { PureComponent } from 'react';
// Services & Utils
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
@ -14,7 +14,7 @@ import 'app/features/alerting/AlertTabCtrl';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
import { PanelModel } from '../dashboard/panel_model';
import { TestRuleButton } from './TestRuleButton';
import { TestRuleResult } from './TestRuleResult';
interface Props {
angularPanel?: AngularComponent;
@ -22,16 +22,6 @@ interface Props {
panel: PanelModel;
}
interface LoadingPlaceholderProps {
text: string;
}
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
<div className="gf-form-group">
{text} <i className="fa fa-spinner fa-spin" />
</div>
);
export class AlertTab extends PureComponent<Props> {
element: any;
component: AngularComponent;
@ -120,14 +110,14 @@ export class AlertTab extends PureComponent<Props> {
};
};
renderTestRuleButton = () => {
renderTestRuleResult = () => {
const { panel, dashboard } = this.props;
return <TestRuleButton panelId={panel.id} dashboard={dashboard} LoadingPlaceholder={LoadingPlaceholder} />;
return <TestRuleResult panelId={panel.id} dashboard={dashboard} />;
};
testRule = (): EditorToolbarView => ({
title: 'Test Rule',
render: () => this.renderTestRuleButton(),
render: () => this.renderTestRuleResult(),
});
onAddAlert = () => {

View File

@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DashboardModel } from '../dashboard/dashboard_model';
import { Props, TestRuleButton } from './TestRuleButton';
import { Props, TestRuleResult } from './TestRuleResult';
jest.mock('app/core/services/backend_srv', () => ({
getBackendSrv: () => ({
@ -13,14 +13,13 @@ const setup = (propOverrides?: object) => {
const props: Props = {
panelId: 1,
dashboard: new DashboardModel({ panels: [{ id: 1 }] }),
LoadingPlaceholder: {},
};
Object.assign(props, propOverrides);
const wrapper = shallow(<TestRuleButton {...props} />);
const wrapper = shallow(<TestRuleResult {...props} />);
return { wrapper, instance: wrapper.instance() as TestRuleButton };
return { wrapper, instance: wrapper.instance() as TestRuleResult };
};
describe('Render', () => {

View File

@ -2,11 +2,11 @@ import React, { PureComponent } from 'react';
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../dashboard/dashboard_model';
import { LoadingPlaceholder } from '@grafana/ui/src';
export interface Props {
panelId: number;
dashboard: DashboardModel;
LoadingPlaceholder: any;
}
interface State {
@ -14,7 +14,7 @@ interface State {
testRuleResponse: {};
}
export class TestRuleButton extends PureComponent<Props, State> {
export class TestRuleResult extends PureComponent<Props, State> {
readonly state: State = {
isLoading: false,
testRuleResponse: {},
@ -27,13 +27,14 @@ export class TestRuleButton extends PureComponent<Props, State> {
async testRule() {
const { panelId, dashboard } = this.props;
const payload = { dashboard: dashboard.getSaveModelClone(), panelId };
this.setState({ isLoading: true });
const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload);
this.setState(prevState => ({ ...prevState, isLoading: false, testRuleResponse }));
this.setState({ isLoading: false, testRuleResponse });
}
render() {
const { testRuleResponse, isLoading } = this.state;
const { LoadingPlaceholder } = this.props;
if (isLoading === true) {
return <LoadingPlaceholder text="Evaluating rule" />;

View File

@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<JSONFormatter
config={
Object {
"animateOpen": true,
}
}
json={Object {}}
open={3}
/>
`;

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<Component
text="Evaluating rule"
/>
`;

View File

@ -9,6 +9,7 @@ import {
} from 'app/core/constants';
import { PanelModel } from './panel_model';
import { DashboardModel } from './dashboard_model';
import getFactors from 'app/core/utils/factors';
export class DashboardMigrator {
dashboard: DashboardModel;
@ -21,7 +22,7 @@ export class DashboardMigrator {
let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades = [];
this.dashboard.schemaVersion = 16;
this.dashboard.schemaVersion = 17;
if (oldVersion === this.dashboard.schemaVersion) {
return;
@ -368,6 +369,24 @@ export class DashboardMigrator {
this.upgradeToGridLayout(old);
}
if (oldVersion < 17) {
panelUpgrades.push(panel => {
if (panel.minSpan) {
const max = GRID_COLUMN_COUNT / panel.minSpan;
const factors = getFactors(GRID_COLUMN_COUNT);
// find the best match compared to factors
// (ie. [1,2,3,4,6,12,24] for 24 columns)
panel.maxPerRow =
factors[
_.findIndex(factors, o => {
return o > max;
}) - 1
];
}
delete panel.minSpan;
});
}
if (panelUpgrades.length === 0) {
return;
}

View File

@ -442,7 +442,7 @@ export class DashboardModel {
}
const selectedOptions = this.getSelectedVariableOptions(variable);
const minWidth = panel.minSpan || 6;
const maxPerRow = panel.maxPerRow || 4;
let xPos = 0;
let yPos = panel.gridPos.y;
@ -462,7 +462,7 @@ export class DashboardModel {
} else {
// set width based on how many are selected
// assumed the repeated panels should take up full row width
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth);
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, GRID_COLUMN_COUNT / maxPerRow);
copy.gridPos.x = xPos;
copy.gridPos.y = yPos;

View File

@ -1,10 +1,10 @@
// Libraries
import React, { PureComponent, SFC } from 'react';
import React, { PureComponent } from 'react';
import _ from 'lodash';
// Components
import 'app/features/panel/metrics_tab';
import { EditorTabBody, EditorToolbarView} from './EditorTabBody';
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions';
@ -36,12 +36,6 @@ interface State {
isAddingMixed: boolean;
}
interface LoadingPlaceholderProps {
text: string;
}
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
export class QueriesTab extends PureComponent<Props, State> {
element: HTMLElement;
component: AngularComponent;
@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent<Props, State> {
renderQueryInspector = () => {
const { panel } = this.props;
return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
return <QueryInspector panel={panel} />;
};
renderHelp = () => {

View File

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { LoadingPlaceholder } from '@grafana/ui';
interface DsQuery {
isLoading: boolean;
@ -10,7 +11,6 @@ interface DsQuery {
interface Props {
panel: any;
LoadingPlaceholder: any;
}
interface State {
@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
render() {
const { response, isLoading } = this.state.dsQuery;
const { LoadingPlaceholder } = this.props;
const { isMocking } = this.state;
const openNodes = this.getNrOfOpenNodes();

View File

@ -77,7 +77,7 @@ export class PanelModel {
repeatPanelId?: number;
repeatDirection?: string;
repeatedByRow?: boolean;
minSpan?: number;
maxPerRow?: number;
collapsed?: boolean;
panels?: any;
soloMode?: boolean;

View File

@ -127,7 +127,7 @@ describe('DashboardModel', () => {
});
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(16);
expect(model.schemaVersion).toBe(17);
});
it('graph thresholds should be migrated', () => {
@ -364,14 +364,6 @@ describe('DashboardModel', () => {
expect(dashboard.panels.length).toBe(2);
});
it('minSpan should be twice', () => {
model.rows = [createRow({ height: 8 }, [[6]])];
model.rows[0].panels[0] = { minSpan: 12 };
const dashboard = new DashboardModel(model);
expect(dashboard.panels[0].minSpan).toBe(24);
});
it('should assign id', () => {
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
model.rows[0].panels[0] = {};
@ -380,6 +372,16 @@ describe('DashboardModel', () => {
expect(dashboard.panels[0].id).toBe(1);
});
});
describe('when migrating from minSpan to maxPerRow', () => {
it('maxPerRow should be correct', () => {
const model = {
panels: [{ minSpan: 8 }],
};
const dashboard = new DashboardModel(model);
expect(dashboard.panels[0].maxPerRow).toBe(3);
});
});
});
function createRow(options, panelDescriptions: any[]) {

View File

@ -143,12 +143,9 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange)
const timeShift = '-' + timeShiftInterpolated;
newTimeData.timeInfo += ' timeshift ' + timeShift;
newTimeData.timeRange = {
from: dateMath.parseDateMath(timeShift, timeRange.from, false),
to: dateMath.parseDateMath(timeShift, timeRange.to, true),
raw: {
from: timeRange.from,
to: timeRange.to,
},
from: dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false),
to: dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true),
raw: newTimeData.timeRange.raw,
};
}

View File

@ -5,6 +5,7 @@ import Remarkable from 'remarkable';
import config from 'app/core/config';
import { profiler } from 'app/core/core';
import { Emitter } from 'app/core/core';
import getFactors from 'app/core/utils/factors';
import {
duplicatePanel,
copyPanel as copyPanelUtil,
@ -12,7 +13,7 @@ import {
sharePanel as sharePanelUtil,
} from 'app/features/dashboard/utils/panel';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
export class PanelCtrl {
panel: any;
@ -32,6 +33,7 @@ export class PanelCtrl {
events: Emitter;
timing: any;
loading: boolean;
maxPanelsPerRowOptions: number[];
constructor($scope, $injector) {
this.$injector = $injector;
@ -92,6 +94,7 @@ export class PanelCtrl {
if (!this.editModeInitiated) {
this.editModeInitiated = true;
this.events.emit('init-edit-mode', null);
this.maxPanelsPerRowOptions = getFactors(GRID_COLUMN_COUNT);
}
}

View File

@ -32,12 +32,17 @@
</select>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
<span class="gf-form-label width-9">Min width</span>
<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
<span class="gf-form-label width-9">Max per row</span>
<select class="gf-form-input" ng-model="ctrl.panel.maxPerRow" ng-options="f for f in [2,3,4,6,12,24]">
<option value=""></option>
</select>
</div>
<div class="gf-form-hint">
<div class="gf-form-hint-text muted">
Note: You may need to change the variable selection to see this in action.
</div>
</div>
</div>
</div>
</div>

View File

@ -1,8 +1,7 @@
import React, { PureComponent } from 'react';
import { MappingType, RangeMap, ValueMap } from '@grafana/ui';
import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui';
import { Label } from 'app/core/components/Label/Label';
import { Select } from 'app/core/components/Select/Select';
interface Props {
mapping: ValueMap | RangeMap;

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { GaugeOptions, PanelOptionsProps } from '@grafana/ui';
import { Label } from 'app/core/components/Label/Label';
import Select from 'app/core/components/Select/Select';
import { Select} from '@grafana/ui';
import UnitPicker from 'app/core/components/Select/UnitPicker';
const statOptions = [

View File

@ -65,7 +65,7 @@
}
],
"rows": [],
"schemaVersion": 16,
"schemaVersion": 17,
"style": "dark",
"tags": [],
"templating": {

View File

@ -1,4 +1,4 @@
// DEPENDENCIES
// DEPENDENCIES
@import '../../node_modules/react-table/react-table.css';
// VENDOR
@ -38,9 +38,6 @@
@import 'layout/lists';
@import 'layout/page';
// LOAD @grafana/ui components
@import '../../packages/grafana-ui/src/index';
// COMPONENTS
@import 'components/scrollbar';
@import 'components/cards';
@ -97,7 +94,6 @@
@import 'components/page_header';
@import 'components/dashboard_settings';
@import 'components/empty_list_cta';
@import 'components/form_select_box';
@import 'components/panel_editor';
@import 'components/toolbar';
@import 'components/add_data_source.scss';
@ -106,6 +102,9 @@
@import 'components/value-mappings';
@import 'components/popover-box';
// LOAD @grafana/ui components
@import '../../packages/grafana-ui/src/index';
// PAGES
@import 'pages/login';
@import 'pages/dashboard';

View File

@ -1105,7 +1105,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.1.0", "@types/react@^16.7.6":
"@types/react@*", "@types/react@16.7.6", "@types/react@^16.1.0", "@types/react@^16.7.6":
version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA==
@ -3185,7 +3185,7 @@ caniuse-api@^1.5.2:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
caniuse-db@1.0.30000772, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000772"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
integrity sha1-UarokXaChureSj2DGep21qAbUSs=