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 ...@@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
"list": [] "list": []
}, },
"refresh": "5s", "refresh": "5s",
"schemaVersion": 16, "schemaVersion": 17,
"version": 0, "version": 0,
"links": [] "links": []
} }
......
...@@ -292,9 +292,11 @@ The `direction` controls how the panels will be arranged. ...@@ -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 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 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. 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. 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'; 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 { components } from '@torkelo/react-select';
export const IndicatorsContainer = props => { export const IndicatorsContainer = (props: any) => {
const isOpen = props.selectProps.menuIsOpen; const isOpen = props.selectProps.menuIsOpen;
return ( return (
<components.IndicatorsContainer {...props}> <components.IndicatorsContainer {...props}>
......
import React from 'react'; 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 { components } from '@torkelo/react-select';
// @ts-ignore
import { OptionProps } from '@torkelo/react-select/lib/components/Option'; import { OptionProps } from '@torkelo/react-select/lib/components/Option';
export interface Props { export interface Props {
......
// Libraries // Libraries
import classNames from 'classnames'; import classNames from 'classnames';
import React, { PureComponent } from 'react'; 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'; import { default as ReactSelect } from '@torkelo/react-select';
// @ts-ignore
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async'; import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
// @ts-ignore
import { components } from '@torkelo/react-select'; import { components } from '@torkelo/react-select';
// Components // Components
import { Option, SingleValue } from './PickerOption'; import { SelectOption, SingleValue } from './SelectOption';
import OptionGroup from './OptionGroup'; import SelectOptionGroup from './SelectOptionGroup';
import IndicatorsContainer from './IndicatorsContainer'; import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage'; import NoOptionsMessage from './NoOptionsMessage';
import ResetStyles from './ResetStyles'; import resetSelectStyles from './resetSelectStyles';
import { CustomScrollbar } from '@grafana/ui'; import { CustomScrollbar } from '@grafana/ui';
export interface SelectOptionItem { export interface SelectOptionItem {
...@@ -53,7 +58,7 @@ interface AsyncProps { ...@@ -53,7 +58,7 @@ interface AsyncProps {
loadingMessage?: () => string; loadingMessage?: () => string;
} }
export const MenuList = props => { export const MenuList = (props: any) => {
return ( return (
<components.MenuList {...props}> <components.MenuList {...props}>
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar> <CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
...@@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> { ...@@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
classNamePrefix="gf-form-select-box" classNamePrefix="gf-form-select-box"
className={selectClassNames} className={selectClassNames}
components={{ components={{
Option, Option: SelectOption,
SingleValue, SingleValue,
IndicatorsContainer, IndicatorsContainer,
MenuList, MenuList,
Group: OptionGroup, Group: SelectOptionGroup,
}} }}
defaultValue={defaultValue} defaultValue={defaultValue}
value={value} value={value}
...@@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> { ...@@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
onChange={onChange} onChange={onChange}
options={options} options={options}
placeholder={placeholder || 'Choose'} placeholder={placeholder || 'Choose'}
styles={ResetStyles} styles={resetSelectStyles()}
isDisabled={isDisabled} isDisabled={isDisabled}
isLoading={isLoading} isLoading={isLoading}
isClearable={isClearable} isClearable={isClearable}
...@@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> { ...@@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
isLoading={isLoading} isLoading={isLoading}
defaultOptions={defaultOptions} defaultOptions={defaultOptions}
placeholder={placeholder || 'Choose'} placeholder={placeholder || 'Choose'}
styles={ResetStyles} styles={resetSelectStyles()}
loadingMessage={loadingMessage} loadingMessage={loadingMessage}
noOptionsMessage={noOptionsMessage} noOptionsMessage={noOptionsMessage}
isDisabled={isDisabled} isDisabled={isDisabled}
......
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; 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(), cx: jest.fn(),
clearValue: jest.fn(), clearValue: jest.fn(),
onSelect: jest.fn(),
getStyles: jest.fn(), getStyles: jest.fn(),
getValue: jest.fn(), getValue: jest.fn(),
hasValue: true, hasValue: true,
...@@ -18,21 +18,31 @@ const model = { ...@@ -18,21 +18,31 @@ const model = {
isFocused: false, isFocused: false,
isSelected: false, isSelected: false,
innerRef: null, innerRef: null,
innerProps: null, innerProps: {
id: '',
key: '',
onClick: jest.fn(),
onMouseOver: jest.fn(),
tabIndex: 1,
},
label: 'Option label', label: 'Option label',
type: null, type: 'option',
children: 'Model title', children: 'Model title',
data: {
title: 'Model title',
imgUrl: 'url/to/avatar',
label: 'User picker label',
},
className: 'class-for-user-picker', className: 'class-for-user-picker',
}; };
describe('PickerOption', () => { describe('SelectOption', () => {
it('renders correctly', () => { 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(); expect(tree).toMatchSnapshot();
}); });
}); });
import React from 'react'; 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 { components } from '@torkelo/react-select';
import { OptionProps } from 'react-select/lib/components/Option'; import { OptionProps } from 'react-select/lib/components/Option';
...@@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> { ...@@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> {
}; };
} }
export const Option = (props: ExtendedOptionProps) => { export const SelectOption = (props: ExtendedOptionProps) => {
const { children, isSelected, data } = props; const { children, isSelected, data } = props;
return ( return (
...@@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => { ...@@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
}; };
// was not able to type this without typescript error // was not able to type this without typescript error
export const SingleValue = props => { export const SingleValue = (props: any) => {
const { children, data } = props; const { children, data } = props;
return ( return (
...@@ -41,4 +44,4 @@ export const SingleValue = props => { ...@@ -41,4 +44,4 @@ export const SingleValue = props => {
); );
}; };
export default Option; export default SelectOption;
...@@ -9,7 +9,7 @@ interface State { ...@@ -9,7 +9,7 @@ interface State {
expanded: boolean; expanded: boolean;
} }
export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> { export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
state = { state = {
expanded: false, expanded: false,
}; };
...@@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State ...@@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State
} }
} }
componentDidUpdate(nextProps) { componentDidUpdate(nextProps: ExtendedGroupProps) {
if (nextProps.selectProps.inputValue !== '') { if (nextProps.selectProps.inputValue !== '') {
this.setState({ expanded: true }); this.setState({ expanded: true });
} }
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PickerOption renders correctly 1`] = ` exports[`SelectOption renders correctly 1`] = `
<div> <div
id=""
onClick={[MockFunction]}
onMouseOver={[MockFunction]}
tabIndex={1}
>
<div <div
className="gf-form-select-box__desc-option" className="gf-form-select-box__desc-option"
> >
......
export default { export default function resetSelectStyles() {
clearIndicator: () => ({}), return {
container: () => ({}), clearIndicator: () => ({}),
control: () => ({}), container: () => ({}),
dropdownIndicator: () => ({}), control: () => ({}),
group: () => ({}), dropdownIndicator: () => ({}),
groupHeading: () => ({}), group: () => ({}),
indicatorsContainer: () => ({}), groupHeading: () => ({}),
indicatorSeparator: () => ({}), indicatorsContainer: () => ({}),
input: () => ({}), indicatorSeparator: () => ({}),
loadingIndicator: () => ({}), input: () => ({}),
loadingMessage: () => ({}), loadingIndicator: () => ({}),
menu: () => ({}), loadingMessage: () => ({}),
menuList: ({ maxHeight }: { maxHeight: number }) => ({ menu: () => ({}),
maxHeight, menuList: ({ maxHeight }: { maxHeight: number }) => ({
}), maxHeight,
multiValue: () => ({}), }),
multiValueLabel: () => ({}), multiValue: () => ({}),
multiValueRemove: () => ({}), multiValueLabel: () => ({}),
noOptionsMessage: () => ({}), multiValueRemove: () => ({}),
option: () => ({}), noOptionsMessage: () => ({}),
placeholder: () => ({}), option: () => ({}),
singleValue: () => ({}), placeholder: () => ({}),
valueContainer: () => ({}), singleValue: () => ({}),
}; valueContainer: () => ({}),
};
}
...@@ -2,3 +2,4 @@ ...@@ -2,3 +2,4 @@
@import 'DeleteButton/DeleteButton'; @import 'DeleteButton/DeleteButton';
@import 'ThresholdsEditor/ThresholdsEditor'; @import 'ThresholdsEditor/ThresholdsEditor';
@import 'Tooltip/Tooltip'; @import 'Tooltip/Tooltip';
@import 'Select/Select';
...@@ -2,6 +2,14 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; ...@@ -2,6 +2,14 @@ export { DeleteButton } from './DeleteButton/DeleteButton';
export { Tooltip } from './Tooltip/Tooltip'; export { Tooltip } from './Tooltip/Tooltip';
export { Portal } from './Portal/Portal'; export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; 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 { ColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
......
...@@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard { ...@@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard {
func NewDashboardFolder(title string) *Dashboard { func NewDashboardFolder(title string) *Dashboard {
folder := NewDashboard(title) folder := NewDashboard(title)
folder.IsFolder = true folder.IsFolder = true
folder.Data.Set("schemaVersion", 16) folder.Data.Set("schemaVersion", 17)
folder.Data.Set("version", 0) folder.Data.Set("version", 0)
folder.IsFolder = true folder.IsFolder = true
return folder return folder
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import { UserPicker } from 'app/core/components/Select/UserPicker'; import { UserPicker } from 'app/core/components/Select/UserPicker';
import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker'; 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 { User } from 'app/types';
import { import {
dashboardPermissionLevels, dashboardPermissionLevels,
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import Select from 'app/core/components/Select/Select'; import { Select } from '@grafana/ui';
import { dashboardPermissionLevels } from 'app/types/acl'; import { dashboardPermissionLevels } from 'app/types/acl';
export interface Props { export interface Props {
......
import React, { PureComponent } from 'react'; 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 { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
import { FolderInfo } from 'app/types'; import { FolderInfo } from 'app/types';
......
...@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; ...@@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import _ from 'lodash'; import _ from 'lodash';
// Components // Components
import Select from './Select'; import { Select } from '@grafana/ui';
// Types // Types
import { DataSourceSelectItem } from 'app/types'; import { DataSourceSelectItem } from 'app/types';
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import { AsyncSelect } from './Select'; import { AsyncSelect } from '@grafana/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import Select from './Select'; import { Select } from '@grafana/ui';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
interface Props { interface Props {
......
...@@ -3,7 +3,7 @@ import React, { Component } from 'react'; ...@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import _ from 'lodash'; import _ from 'lodash';
// Components // Components
import { AsyncSelect } from './Select'; import { AsyncSelect } from '@grafana/ui';
// Utils & Services // Utils & Services
import { debounce } from 'lodash'; import { debounce } from 'lodash';
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Label } from 'app/core/components/Label/Label'; 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 { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchHit } from 'app/types'; import { DashboardSearchHit } from 'app/types';
......
import React from 'react'; import React from 'react';
import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
import AsyncSelect from '@torkelo/react-select/lib/Async'; import AsyncSelect from '@torkelo/react-select/lib/Async';
import { TagOption } from './TagOption'; import { TagOption } from './TagOption';
import { TagBadge } from './TagBadge'; 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 { components } from '@torkelo/react-select';
import ResetStyles from 'app/core/components/Select/ResetStyles';
export interface Props { export interface Props {
tags: string[]; tags: string[];
...@@ -51,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> { ...@@ -51,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> {
getOptionValue: i => i.value, getOptionValue: i => i.value,
getOptionLabel: i => i.label, getOptionLabel: i => i.label,
value: tags, value: tags,
styles: ResetStyles, styles: resetSelectStyles(),
filterOption: (option, searchQuery) => { filterOption: (option, searchQuery) => {
const regex = RegExp(searchQuery, 'i'); const regex = RegExp(searchQuery, 'i');
return regex.test(option.value); 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 // Libraries
import React, { PureComponent, SFC } from 'react'; import React, { PureComponent } from 'react';
// Services & Utils // Services & Utils
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
...@@ -14,7 +14,7 @@ import 'app/features/alerting/AlertTabCtrl'; ...@@ -14,7 +14,7 @@ import 'app/features/alerting/AlertTabCtrl';
// Types // Types
import { DashboardModel } from '../dashboard/dashboard_model'; import { DashboardModel } from '../dashboard/dashboard_model';
import { PanelModel } from '../dashboard/panel_model'; import { PanelModel } from '../dashboard/panel_model';
import { TestRuleButton } from './TestRuleButton'; import { TestRuleResult } from './TestRuleResult';
interface Props { interface Props {
angularPanel?: AngularComponent; angularPanel?: AngularComponent;
...@@ -22,16 +22,6 @@ interface Props { ...@@ -22,16 +22,6 @@ interface Props {
panel: PanelModel; 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> { export class AlertTab extends PureComponent<Props> {
element: any; element: any;
component: AngularComponent; component: AngularComponent;
...@@ -120,14 +110,14 @@ export class AlertTab extends PureComponent<Props> { ...@@ -120,14 +110,14 @@ export class AlertTab extends PureComponent<Props> {
}; };
}; };
renderTestRuleButton = () => { renderTestRuleResult = () => {
const { panel, dashboard } = this.props; const { panel, dashboard } = this.props;
return <TestRuleButton panelId={panel.id} dashboard={dashboard} LoadingPlaceholder={LoadingPlaceholder} />; return <TestRuleResult panelId={panel.id} dashboard={dashboard} />;
}; };
testRule = (): EditorToolbarView => ({ testRule = (): EditorToolbarView => ({
title: 'Test Rule', title: 'Test Rule',
render: () => this.renderTestRuleButton(), render: () => this.renderTestRuleResult(),
}); });
onAddAlert = () => { onAddAlert = () => {
......
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { DashboardModel } from '../dashboard/dashboard_model'; import { DashboardModel } from '../dashboard/dashboard_model';
import { Props, TestRuleButton } from './TestRuleButton'; import { Props, TestRuleResult } from './TestRuleResult';
jest.mock('app/core/services/backend_srv', () => ({ jest.mock('app/core/services/backend_srv', () => ({
getBackendSrv: () => ({ getBackendSrv: () => ({
...@@ -13,14 +13,13 @@ const setup = (propOverrides?: object) => { ...@@ -13,14 +13,13 @@ const setup = (propOverrides?: object) => {
const props: Props = { const props: Props = {
panelId: 1, panelId: 1,
dashboard: new DashboardModel({ panels: [{ id: 1 }] }), dashboard: new DashboardModel({ panels: [{ id: 1 }] }),
LoadingPlaceholder: {},
}; };
Object.assign(props, propOverrides); 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', () => { describe('Render', () => {
......
...@@ -2,11 +2,11 @@ import React, { PureComponent } from 'react'; ...@@ -2,11 +2,11 @@ import React, { PureComponent } from 'react';
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../dashboard/dashboard_model'; import { DashboardModel } from '../dashboard/dashboard_model';
import { LoadingPlaceholder } from '@grafana/ui/src';
export interface Props { export interface Props {
panelId: number; panelId: number;
dashboard: DashboardModel; dashboard: DashboardModel;
LoadingPlaceholder: any;
} }
interface State { interface State {
...@@ -14,7 +14,7 @@ interface State { ...@@ -14,7 +14,7 @@ interface State {
testRuleResponse: {}; testRuleResponse: {};
} }
export class TestRuleButton extends PureComponent<Props, State> { export class TestRuleResult extends PureComponent<Props, State> {
readonly state: State = { readonly state: State = {
isLoading: false, isLoading: false,
testRuleResponse: {}, testRuleResponse: {},
...@@ -27,13 +27,14 @@ export class TestRuleButton extends PureComponent<Props, State> { ...@@ -27,13 +27,14 @@ export class TestRuleButton extends PureComponent<Props, State> {
async testRule() { async testRule() {
const { panelId, dashboard } = this.props; const { panelId, dashboard } = this.props;
const payload = { dashboard: dashboard.getSaveModelClone(), panelId }; const payload = { dashboard: dashboard.getSaveModelClone(), panelId };
this.setState({ isLoading: true });
const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload); const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload);
this.setState(prevState => ({ ...prevState, isLoading: false, testRuleResponse })); this.setState({ isLoading: false, testRuleResponse });
} }
render() { render() {
const { testRuleResponse, isLoading } = this.state; const { testRuleResponse, isLoading } = this.state;
const { LoadingPlaceholder } = this.props;
if (isLoading === true) { if (isLoading === true) {
return <LoadingPlaceholder text="Evaluating rule" />; 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 { ...@@ -9,6 +9,7 @@ import {
} from 'app/core/constants'; } from 'app/core/constants';
import { PanelModel } from './panel_model'; import { PanelModel } from './panel_model';
import { DashboardModel } from './dashboard_model'; import { DashboardModel } from './dashboard_model';
import getFactors from 'app/core/utils/factors';
export class DashboardMigrator { export class DashboardMigrator {
dashboard: DashboardModel; dashboard: DashboardModel;
...@@ -21,7 +22,7 @@ export class DashboardMigrator { ...@@ -21,7 +22,7 @@ export class DashboardMigrator {
let i, j, k, n; let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion; const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades = []; const panelUpgrades = [];
this.dashboard.schemaVersion = 16; this.dashboard.schemaVersion = 17;
if (oldVersion === this.dashboard.schemaVersion) { if (oldVersion === this.dashboard.schemaVersion) {
return; return;
...@@ -368,6 +369,24 @@ export class DashboardMigrator { ...@@ -368,6 +369,24 @@ export class DashboardMigrator {
this.upgradeToGridLayout(old); 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) { if (panelUpgrades.length === 0) {
return; return;
} }
......
...@@ -442,7 +442,7 @@ export class DashboardModel { ...@@ -442,7 +442,7 @@ export class DashboardModel {
} }
const selectedOptions = this.getSelectedVariableOptions(variable); const selectedOptions = this.getSelectedVariableOptions(variable);
const minWidth = panel.minSpan || 6; const maxPerRow = panel.maxPerRow || 4;
let xPos = 0; let xPos = 0;
let yPos = panel.gridPos.y; let yPos = panel.gridPos.y;
...@@ -462,7 +462,7 @@ export class DashboardModel { ...@@ -462,7 +462,7 @@ export class DashboardModel {
} else { } else {
// set width based on how many are selected // set width based on how many are selected
// assumed the repeated panels should take up full row width // 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.x = xPos;
copy.gridPos.y = yPos; copy.gridPos.y = yPos;
......
// Libraries // Libraries
import React, { PureComponent, SFC } from 'react'; import React, { PureComponent } from 'react';
import _ from 'lodash'; import _ from 'lodash';
// Components // Components
import 'app/features/panel/metrics_tab'; 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 { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector'; import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions'; import { QueryOptions } from './QueryOptions';
...@@ -36,12 +36,6 @@ interface State { ...@@ -36,12 +36,6 @@ interface State {
isAddingMixed: boolean; isAddingMixed: boolean;
} }
interface LoadingPlaceholderProps {
text: string;
}
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
export class QueriesTab extends PureComponent<Props, State> { export class QueriesTab extends PureComponent<Props, State> {
element: HTMLElement; element: HTMLElement;
component: AngularComponent; component: AngularComponent;
...@@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent<Props, State> {
renderQueryInspector = () => { renderQueryInspector = () => {
const { panel } = this.props; const { panel } = this.props;
return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />; return <QueryInspector panel={panel} />;
}; };
renderHelp = () => { renderHelp = () => {
......
...@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; ...@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { LoadingPlaceholder } from '@grafana/ui';
interface DsQuery { interface DsQuery {
isLoading: boolean; isLoading: boolean;
...@@ -10,7 +11,6 @@ interface DsQuery { ...@@ -10,7 +11,6 @@ interface DsQuery {
interface Props { interface Props {
panel: any; panel: any;
LoadingPlaceholder: any;
} }
interface State { interface State {
...@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent<Props, State> {
render() { render() {
const { response, isLoading } = this.state.dsQuery; const { response, isLoading } = this.state.dsQuery;
const { LoadingPlaceholder } = this.props;
const { isMocking } = this.state; const { isMocking } = this.state;
const openNodes = this.getNrOfOpenNodes(); const openNodes = this.getNrOfOpenNodes();
......
...@@ -77,7 +77,7 @@ export class PanelModel { ...@@ -77,7 +77,7 @@ export class PanelModel {
repeatPanelId?: number; repeatPanelId?: number;
repeatDirection?: string; repeatDirection?: string;
repeatedByRow?: boolean; repeatedByRow?: boolean;
minSpan?: number; maxPerRow?: number;
collapsed?: boolean; collapsed?: boolean;
panels?: any; panels?: any;
soloMode?: boolean; soloMode?: boolean;
......
...@@ -127,7 +127,7 @@ describe('DashboardModel', () => { ...@@ -127,7 +127,7 @@ describe('DashboardModel', () => {
}); });
it('dashboard schema version should be set to latest', () => { 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', () => { it('graph thresholds should be migrated', () => {
...@@ -364,14 +364,6 @@ describe('DashboardModel', () => { ...@@ -364,14 +364,6 @@ describe('DashboardModel', () => {
expect(dashboard.panels.length).toBe(2); 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', () => { it('should assign id', () => {
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])]; model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
model.rows[0].panels[0] = {}; model.rows[0].panels[0] = {};
...@@ -380,6 +372,16 @@ describe('DashboardModel', () => { ...@@ -380,6 +372,16 @@ describe('DashboardModel', () => {
expect(dashboard.panels[0].id).toBe(1); 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[]) { function createRow(options, panelDescriptions: any[]) {
......
...@@ -143,12 +143,9 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange) ...@@ -143,12 +143,9 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange)
const timeShift = '-' + timeShiftInterpolated; const timeShift = '-' + timeShiftInterpolated;
newTimeData.timeInfo += ' timeshift ' + timeShift; newTimeData.timeInfo += ' timeshift ' + timeShift;
newTimeData.timeRange = { newTimeData.timeRange = {
from: dateMath.parseDateMath(timeShift, timeRange.from, false), from: dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false),
to: dateMath.parseDateMath(timeShift, timeRange.to, true), to: dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true),
raw: { raw: newTimeData.timeRange.raw,
from: timeRange.from,
to: timeRange.to,
},
}; };
} }
......
...@@ -5,6 +5,7 @@ import Remarkable from 'remarkable'; ...@@ -5,6 +5,7 @@ import Remarkable from 'remarkable';
import config from 'app/core/config'; import config from 'app/core/config';
import { profiler } from 'app/core/core'; import { profiler } from 'app/core/core';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
import getFactors from 'app/core/utils/factors';
import { import {
duplicatePanel, duplicatePanel,
copyPanel as copyPanelUtil, copyPanel as copyPanelUtil,
...@@ -12,7 +13,7 @@ import { ...@@ -12,7 +13,7 @@ import {
sharePanel as sharePanelUtil, sharePanel as sharePanelUtil,
} from 'app/features/dashboard/utils/panel'; } 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 { export class PanelCtrl {
panel: any; panel: any;
...@@ -32,6 +33,7 @@ export class PanelCtrl { ...@@ -32,6 +33,7 @@ export class PanelCtrl {
events: Emitter; events: Emitter;
timing: any; timing: any;
loading: boolean; loading: boolean;
maxPanelsPerRowOptions: number[];
constructor($scope, $injector) { constructor($scope, $injector) {
this.$injector = $injector; this.$injector = $injector;
...@@ -92,6 +94,7 @@ export class PanelCtrl { ...@@ -92,6 +94,7 @@ export class PanelCtrl {
if (!this.editModeInitiated) { if (!this.editModeInitiated) {
this.editModeInitiated = true; this.editModeInitiated = true;
this.events.emit('init-edit-mode', null); this.events.emit('init-edit-mode', null);
this.maxPanelsPerRowOptions = getFactors(GRID_COLUMN_COUNT);
} }
} }
......
...@@ -32,12 +32,17 @@ ...@@ -32,12 +32,17 @@
</select> </select>
</div> </div>
<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'"> <div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
<span class="gf-form-label width-9">Min width</span> <span class="gf-form-label width-9">Max per row</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]"> <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> <option value=""></option>
</select> </select>
</div> </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> </div>
</div> </div>
......
import React, { PureComponent } from 'react'; 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 { Label } from 'app/core/components/Label/Label';
import { Select } from 'app/core/components/Select/Select';
interface Props { interface Props {
mapping: ValueMap | RangeMap; mapping: ValueMap | RangeMap;
......
...@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; ...@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { GaugeOptions, PanelOptionsProps } from '@grafana/ui'; import { GaugeOptions, PanelOptionsProps } from '@grafana/ui';
import { Label } from 'app/core/components/Label/Label'; 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'; import UnitPicker from 'app/core/components/Select/UnitPicker';
const statOptions = [ const statOptions = [
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
} }
], ],
"rows": [], "rows": [],
"schemaVersion": 16, "schemaVersion": 17,
"style": "dark", "style": "dark",
"tags": [], "tags": [],
"templating": { "templating": {
......
// DEPENDENCIES // DEPENDENCIES
@import '../../node_modules/react-table/react-table.css'; @import '../../node_modules/react-table/react-table.css';
// VENDOR // VENDOR
...@@ -38,9 +38,6 @@ ...@@ -38,9 +38,6 @@
@import 'layout/lists'; @import 'layout/lists';
@import 'layout/page'; @import 'layout/page';
// LOAD @grafana/ui components
@import '../../packages/grafana-ui/src/index';
// COMPONENTS // COMPONENTS
@import 'components/scrollbar'; @import 'components/scrollbar';
@import 'components/cards'; @import 'components/cards';
...@@ -97,7 +94,6 @@ ...@@ -97,7 +94,6 @@
@import 'components/page_header'; @import 'components/page_header';
@import 'components/dashboard_settings'; @import 'components/dashboard_settings';
@import 'components/empty_list_cta'; @import 'components/empty_list_cta';
@import 'components/form_select_box';
@import 'components/panel_editor'; @import 'components/panel_editor';
@import 'components/toolbar'; @import 'components/toolbar';
@import 'components/add_data_source.scss'; @import 'components/add_data_source.scss';
...@@ -106,6 +102,9 @@ ...@@ -106,6 +102,9 @@
@import 'components/value-mappings'; @import 'components/value-mappings';
@import 'components/popover-box'; @import 'components/popover-box';
// LOAD @grafana/ui components
@import '../../packages/grafana-ui/src/index';
// PAGES // PAGES
@import 'pages/login'; @import 'pages/login';
@import 'pages/dashboard'; @import 'pages/dashboard';
......
...@@ -1105,7 +1105,7 @@ ...@@ -1105,7 +1105,7 @@
dependencies: dependencies:
"@types/react" "*" "@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" version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA== integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA==
...@@ -3185,7 +3185,7 @@ caniuse-api@^1.5.2: ...@@ -3185,7 +3185,7 @@ caniuse-api@^1.5.2:
lodash.memoize "^4.1.2" lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0" 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" version "1.0.30000772"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
integrity sha1-UarokXaChureSj2DGep21qAbUSs= 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