Commit d97cd450 by Hugo Häggmark

Merge

parents d2b71cff bc78c8d5
......@@ -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": []
}
......
......@@ -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.
......
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>
);
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}>
......
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 {
......
// 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}
......
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,
innerProps: {
id: '',
key: '',
onClick: jest.fn(),
onMouseOver: jest.fn(),
tabIndex: 1,
},
label: 'Option label',
type: null,
type: 'option',
children: 'Model title',
data: {
title: 'Model title',
imgUrl: 'url/to/avatar',
label: 'User picker label',
},
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();
});
});
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;
......@@ -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 });
}
......
// 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"
>
......
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: () => ({}),
};
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: () => ({}),
};
}
......@@ -2,3 +2,4 @@
@import 'DeleteButton/DeleteButton';
@import 'ThresholdsEditor/ThresholdsEditor';
@import 'Tooltip/Tooltip';
@import 'Select/Select';
......@@ -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';
......
......@@ -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
......
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,
......
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 {
......
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';
......
......@@ -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';
......
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';
......
import React, { PureComponent } from 'react';
import Select from './Select';
import { Select } from '@grafana/ui';
import kbn from 'app/core/utils/kbn';
interface Props {
......
......@@ -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';
......
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';
......
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);
......
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]);
});
});
// 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);
}
// 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 = () => {
......
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', () => {
......
......@@ -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" />;
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<JSONFormatter
config={
Object {
"animateOpen": true,
}
}
json={Object {}}
open={3}
/>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<Component
text="Evaluating rule"
/>
`;
......@@ -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;
}
......
......@@ -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;
......
// 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 = () => {
......
......@@ -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();
......
......@@ -77,7 +77,7 @@ export class PanelModel {
repeatPanelId?: number;
repeatDirection?: string;
repeatedByRow?: boolean;
minSpan?: number;
maxPerRow?: number;
collapsed?: boolean;
panels?: any;
soloMode?: boolean;
......
......@@ -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[]) {
......
......@@ -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,
};
}
......
......@@ -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);
}
}
......
......@@ -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>
......
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;
......
......@@ -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 = [
......
......@@ -65,7 +65,7 @@
}
],
"rows": [],
"schemaVersion": 16,
"schemaVersion": 17,
"style": "dark",
"tags": [],
"templating": {
......
// 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';
......
......@@ -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=
......
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