mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/develop' into 14409/threshold-ux-changes
This commit is contained in:
commit
c78d5fb24e
@ -8,15 +8,16 @@ interface ExtendedOptionProps extends OptionProps<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Option = (props: ExtendedOptionProps) => {
|
export const Option = (props: ExtendedOptionProps) => {
|
||||||
const { children, isSelected, data, className } = props;
|
const { children, isSelected, data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<div className={`description-picker-option__button btn btn-link ${className}`}>
|
<div className="gf-form-select-box__desc-option">
|
||||||
{isSelected && <i className="fa fa-check pull-right" aria-hidden="true" />}
|
<div className="gf-form-select-box__desc-option__body">
|
||||||
<div className="gf-form">{children}</div>
|
<div>{children}</div>
|
||||||
<div className="gf-form">
|
{data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
|
||||||
<div className="muted width-17">{data.description}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{isSelected && <i className="fa fa-check" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
</components.Option>
|
</components.Option>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
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 PickerOption from './PickerOption';
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ const model = {
|
|||||||
children: 'Model title',
|
children: 'Model title',
|
||||||
data: {
|
data: {
|
||||||
title: 'Model title',
|
title: 'Model title',
|
||||||
avatarUrl: 'url/to/avatar',
|
imgUrl: 'url/to/avatar',
|
||||||
label: 'User picker label',
|
label: 'User picker label',
|
||||||
},
|
},
|
||||||
className: 'class-for-user-picker',
|
className: 'class-for-user-picker',
|
||||||
|
@ -4,19 +4,41 @@ import { OptionProps } from 'react-select/lib/components/Option';
|
|||||||
|
|
||||||
// https://github.com/JedWatson/react-select/issues/3038
|
// https://github.com/JedWatson/react-select/issues/3038
|
||||||
interface ExtendedOptionProps extends OptionProps<any> {
|
interface ExtendedOptionProps extends OptionProps<any> {
|
||||||
data: any;
|
data: {
|
||||||
|
description?: string;
|
||||||
|
imgUrl?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PickerOption = (props: ExtendedOptionProps) => {
|
export const Option = (props: ExtendedOptionProps) => {
|
||||||
const { children, data, className } = props;
|
const { children, isSelected, data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<div className={`description-picker-option__button btn btn-link ${className}`}>
|
<div className="gf-form-select-box__desc-option">
|
||||||
{data.avatarUrl && <img src={data.avatarUrl} alt={data.label} className="user-picker-option__avatar" />}
|
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
||||||
{children}
|
<div className="gf-form-select-box__desc-option__body">
|
||||||
|
<div>{children}</div>
|
||||||
|
{data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
|
||||||
|
</div>
|
||||||
|
{isSelected && <i className="fa fa-check" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
</components.Option>
|
</components.Option>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PickerOption;
|
// was not able to type this without typescript error
|
||||||
|
export const SingleValue = props => {
|
||||||
|
const { children, data } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<components.SingleValue {...props}>
|
||||||
|
<div className="gf-form-select-box__img-value">
|
||||||
|
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</components.SingleValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Option;
|
||||||
|
60
public/app/core/components/Picker/Select.tsx
Normal file
60
public/app/core/components/Picker/Select.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// import React, { PureComponent } from 'react';
|
||||||
|
// import Select as ReactSelect from 'react-select';
|
||||||
|
// import DescriptionOption from './DescriptionOption';
|
||||||
|
// import IndicatorsContainer from './IndicatorsContainer';
|
||||||
|
// import ResetStyles from './ResetStyles';
|
||||||
|
//
|
||||||
|
// export interface OptionType {
|
||||||
|
// label: string;
|
||||||
|
// value: string;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// interface Props {
|
||||||
|
// defaultValue?: any;
|
||||||
|
// getOptionLabel: (item: T) => string;
|
||||||
|
// getOptionValue: (item: T) => string;
|
||||||
|
// onChange: (item: T) => {} | void;
|
||||||
|
// options: T[];
|
||||||
|
// placeholder?: string;
|
||||||
|
// width?: number;
|
||||||
|
// value: T;
|
||||||
|
// className?: string;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// export class Select<T> extends PureComponent<Props<T>> {
|
||||||
|
// static defaultProps = {
|
||||||
|
// width: null,
|
||||||
|
// className: '',
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// render() {
|
||||||
|
// const { defaultValue, getOptionLabel, getOptionValue, onSelected, options, placeholder, width, value, className } = this.props;
|
||||||
|
// let widthClass = '';
|
||||||
|
// if (width) {
|
||||||
|
// widthClass = 'width-'+width;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <ReactSelect
|
||||||
|
// classNamePrefix="gf-form-select-box"
|
||||||
|
// className={`gf-form-input gf-form-input--form-dropdown ${widthClass} ${className}`}
|
||||||
|
// components={{
|
||||||
|
// Option: DescriptionOption,
|
||||||
|
// IndicatorsContainer,
|
||||||
|
// }}
|
||||||
|
// defaultValue={defaultValue}
|
||||||
|
// value={value}
|
||||||
|
// getOptionLabel={getOptionLabel}
|
||||||
|
// getOptionValue={getOptionValue}
|
||||||
|
// menuShouldScrollIntoView={false}
|
||||||
|
// isSearchable={false}
|
||||||
|
// onChange={onSelected}
|
||||||
|
// options={options}
|
||||||
|
// placeholder={placeholder || 'Choose'}
|
||||||
|
// styles={ResetStyles}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// export default Select;
|
@ -1,6 +1,7 @@
|
|||||||
import React, { SFC } from 'react';
|
import React, { SFC } from 'react';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import DescriptionOption from './DescriptionOption';
|
import DescriptionOption from './DescriptionOption';
|
||||||
|
import IndicatorsContainer from './IndicatorsContainer';
|
||||||
import ResetStyles from './ResetStyles';
|
import ResetStyles from './ResetStyles';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -11,7 +12,7 @@ interface Props {
|
|||||||
onSelected: (item: any) => {} | void;
|
onSelected: (item: any) => {} | void;
|
||||||
options: any[];
|
options: any[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
width: number;
|
width?: number;
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,10 +29,11 @@ const SimplePicker: SFC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
classNamePrefix={`gf-form-select-box`}
|
classNamePrefix="gf-form-select-box"
|
||||||
className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
className={`${width ? 'width-' + width : ''} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||||
components={{
|
components={{
|
||||||
Option: DescriptionOption,
|
Option: DescriptionOption,
|
||||||
|
IndicatorsContainer,
|
||||||
}}
|
}}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -47,7 +47,7 @@ export class TeamPicker extends Component<Props, State> {
|
|||||||
id: team.id,
|
id: team.id,
|
||||||
label: team.name,
|
label: team.name,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
avatarUrl: team.avatarUrl,
|
imgUrl: team.avatarUrl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ export default class UnitPicker extends PureComponent<Props> {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={`width-${width} gf-form-input--form-dropdown`}
|
className={`width-${width} gf-form-input gf-form-input--form-dropdown`}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
menuShouldScrollIntoView={false}
|
menuShouldScrollIntoView={false}
|
||||||
|
@ -41,7 +41,7 @@ export class UserPicker extends Component<Props, State> {
|
|||||||
return result.map(user => ({
|
return result.map(user => ({
|
||||||
id: user.userId,
|
id: user.userId,
|
||||||
label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
|
label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
|
||||||
avatarUrl: user.avatarUrl,
|
imgUrl: user.avatarUrl,
|
||||||
login: user.login,
|
login: user.login,
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
|
@ -3,15 +3,19 @@
|
|||||||
exports[`PickerOption renders correctly 1`] = `
|
exports[`PickerOption renders correctly 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="description-picker-option__button btn btn-link class-for-user-picker"
|
className="gf-form-select-box__desc-option"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="User picker label"
|
className="gf-form-select-box__desc-option__img"
|
||||||
className="user-picker-option__avatar"
|
|
||||||
src="url/to/avatar"
|
src="url/to/avatar"
|
||||||
/>
|
/>
|
||||||
Model title
|
<div
|
||||||
|
className="gf-form-select-box__desc-option__body"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Model title
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -141,9 +141,10 @@ export class DashboardMigrator {
|
|||||||
|
|
||||||
// ensure query refIds
|
// ensure query refIds
|
||||||
panelUpgrades.push(panel => {
|
panelUpgrades.push(panel => {
|
||||||
|
console.log('asdasd', panel);
|
||||||
_.each(panel.targets, target => {
|
_.each(panel.targets, target => {
|
||||||
if (!target.refId) {
|
if (!target.refId) {
|
||||||
target.refId = this.dashboard.getNextQueryLetter(panel);
|
target.refId = panel.getNextQueryLetter && panel.getNextQueryLetter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -806,16 +806,6 @@ export class DashboardModel {
|
|||||||
return this.timezone === 'browser' ? moment(date).fromNow() : moment.utc(date).fromNow();
|
return this.timezone === 'browser' ? moment(date).fromNow() : moment.utc(date).fromNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextQueryLetter(panel) {
|
|
||||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
|
|
||||||
return _.find(letters, refId => {
|
|
||||||
return _.every(panel.targets, other => {
|
|
||||||
return other.refId !== refId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isTimezoneUtc() {
|
isTimezoneUtc() {
|
||||||
return this.getTimezone() === 'utc';
|
return this.getTimezone() === 'utc';
|
||||||
}
|
}
|
||||||
|
@ -1,115 +1,81 @@
|
|||||||
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation';
|
|
||||||
|
// Components
|
||||||
|
import ResetStyles from 'app/core/components/Picker/ResetStyles';
|
||||||
|
import { Option, SingleValue } from 'app/core/components/Picker/PickerOption';
|
||||||
|
import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
|
||||||
|
import Select from 'react-select';
|
||||||
|
|
||||||
|
// Types
|
||||||
import { DataSourceSelectItem } from 'app/types';
|
import { DataSourceSelectItem } from 'app/types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onChangeDataSource: (ds: DataSourceSelectItem) => void;
|
onChangeDataSource: (ds: DataSourceSelectItem) => void;
|
||||||
datasources: DataSourceSelectItem[];
|
datasources: DataSourceSelectItem[];
|
||||||
|
current: DataSourceSelectItem;
|
||||||
|
onBlur?: () => void;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export class DataSourcePicker extends PureComponent<Props> {
|
||||||
searchQuery: string;
|
static defaultProps = {
|
||||||
}
|
autoFocus: false,
|
||||||
|
};
|
||||||
|
|
||||||
export class DataSourcePicker extends PureComponent<Props, State> {
|
|
||||||
searchInput: HTMLElement;
|
searchInput: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
|
||||||
searchQuery: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataSources() {
|
onChange = item => {
|
||||||
const { searchQuery } = this.state;
|
const ds = this.props.datasources.find(ds => ds.name === item.value);
|
||||||
const regex = new RegExp(searchQuery, 'i');
|
this.props.onChangeDataSource(ds);
|
||||||
const { datasources } = this.props;
|
|
||||||
|
|
||||||
const filtered = datasources.filter(item => {
|
|
||||||
return regex.test(item.name) || regex.test(item.meta.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
get maxSelectedIndex() {
|
|
||||||
const filtered = this.getDataSources();
|
|
||||||
return filtered.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDataSource = (ds: DataSourceSelectItem, index: number, keyNavProps: KeyboardNavigationProps) => {
|
|
||||||
const { onChangeDataSource } = this.props;
|
|
||||||
const { selected, onMouseEnter } = keyNavProps;
|
|
||||||
const onClick = () => onChangeDataSource(ds);
|
|
||||||
const isSelected = selected === index;
|
|
||||||
const cssClass = classNames({
|
|
||||||
'ds-picker-list__item': true,
|
|
||||||
'ds-picker-list__item--selected': isSelected,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div key={index} className={cssClass} title={ds.name} onClick={onClick} onMouseEnter={() => onMouseEnter(index)}>
|
|
||||||
<img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
|
|
||||||
<div className="ds-picker-list__name">{ds.name}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.searchInput.focus();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChange = evt => {
|
|
||||||
const value = evt.target.value;
|
|
||||||
this.setState(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
searchQuery: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
renderFilters({ onKeyDown, selected }: KeyboardNavigationProps) {
|
|
||||||
const { searchQuery } = this.state;
|
|
||||||
return (
|
|
||||||
<label className="gf-form--has-input-icon">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="gf-form-input width-13"
|
|
||||||
placeholder=""
|
|
||||||
ref={elem => (this.searchInput = elem)}
|
|
||||||
onChange={this.onSearchQueryChange}
|
|
||||||
value={searchQuery}
|
|
||||||
onKeyDown={evt => {
|
|
||||||
onKeyDown(evt, this.maxSelectedIndex, () => {
|
|
||||||
const { onChangeDataSource } = this.props;
|
|
||||||
const ds = this.getDataSources()[selected];
|
|
||||||
onChangeDataSource(ds);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { datasources, current, autoFocus, onBlur } = this.props;
|
||||||
|
|
||||||
|
const options = datasources.map(ds => ({
|
||||||
|
value: ds.name,
|
||||||
|
label: ds.name,
|
||||||
|
imgUrl: ds.meta.info.logos.small,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const value = current && {
|
||||||
|
label: current.name,
|
||||||
|
value: current.name,
|
||||||
|
imgUrl: current.meta.info.logos.small,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardNavigation
|
<div className="gf-form-inline">
|
||||||
render={(keyNavProps: KeyboardNavigationProps) => (
|
<Select
|
||||||
<>
|
classNamePrefix={`gf-form-select-box`}
|
||||||
<div className="cta-form__bar">
|
isMulti={false}
|
||||||
{this.renderFilters(keyNavProps)}
|
menuShouldScrollIntoView={false}
|
||||||
<div className="gf-form--grow" />
|
isClearable={false}
|
||||||
</div>
|
className="gf-form-input gf-form-input--form-dropdown ds-picker"
|
||||||
<div className="ds-picker-list">
|
onChange={item => this.onChange(item)}
|
||||||
{this.getDataSources().map((ds, index) => this.renderDataSource(ds, index, keyNavProps))}
|
options={options}
|
||||||
</div>
|
styles={ResetStyles}
|
||||||
</>
|
autoFocus={autoFocus}
|
||||||
)}
|
onBlur={onBlur}
|
||||||
/>
|
openMenuOnFocus={true}
|
||||||
|
maxMenuHeight={500}
|
||||||
|
placeholder="Select datasource"
|
||||||
|
loadingMessage={() => 'Loading datasources...'}
|
||||||
|
noOptionsMessage={() => 'No datasources found'}
|
||||||
|
value={value}
|
||||||
|
components={{
|
||||||
|
Option,
|
||||||
|
SingleValue,
|
||||||
|
IndicatorsContainer,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
|||||||
interface Props {
|
interface Props {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
heading: string;
|
heading: string;
|
||||||
main?: EditorToolBarView;
|
renderToolbar?: () => JSX.Element;
|
||||||
toolbarItems: EditorToolBarView[];
|
toolbarItems?: EditorToolBarView[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorToolBarView {
|
export interface EditorToolBarView {
|
||||||
@ -15,7 +15,7 @@ export interface EditorToolBarView {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
render: (closeFunction: any) => JSX.Element | JSX.Element[];
|
render: (closeFunction?: any) => JSX.Element | JSX.Element[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -25,6 +25,10 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EditorTabBody extends PureComponent<Props, State> {
|
export class EditorTabBody extends PureComponent<Props, State> {
|
||||||
|
static defaultProps = {
|
||||||
|
toolbarItems: [],
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -65,16 +69,6 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMainSelection(view: EditorToolBarView) {
|
|
||||||
return (
|
|
||||||
<div className="toolbar__main" onClick={() => this.onToggleToolBarView(view)} key={view.title + view.icon}>
|
|
||||||
<img className="toolbar__main-image" src={view.imgSrc} />
|
|
||||||
<div className="toolbar__main-name">{view.title}</div>
|
|
||||||
<i className="fa fa-caret-down" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderButton(view: EditorToolBarView) {
|
renderButton(view: EditorToolBarView) {
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (view.onClick) {
|
if (view.onClick) {
|
||||||
@ -104,16 +98,20 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, toolbarItems, main, heading } = this.props;
|
const { children, renderToolbar, heading, toolbarItems } = this.props;
|
||||||
const { openView, fadeIn, isOpen } = this.state;
|
const { openView, fadeIn, isOpen } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<div className="toolbar__heading">{heading}</div>
|
<div className="toolbar__heading">{heading}</div>
|
||||||
{main && this.renderMainSelection(main)}
|
{renderToolbar && renderToolbar()}
|
||||||
<div className="gf-form--grow" />
|
{toolbarItems.length > 0 && (
|
||||||
{toolbarItems.map(item => this.renderButton(item))}
|
<>
|
||||||
|
<div className="gf-form--grow" />
|
||||||
|
{toolbarItems.map(item => this.renderButton(item))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-editor__scroll">
|
<div className="panel-editor__scroll">
|
||||||
<CustomScrollbar autoHide={false}>
|
<CustomScrollbar autoHide={false}>
|
||||||
|
@ -1,36 +1,39 @@
|
|||||||
|
// Libraries
|
||||||
import React, { SFC, PureComponent } from 'react';
|
import React, { SFC, PureComponent } from 'react';
|
||||||
|
import Remarkable from 'remarkable';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
// Components
|
||||||
import DataSourceOption from './DataSourceOption';
|
import DataSourceOption from './DataSourceOption';
|
||||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
|
||||||
import { EditorTabBody } from './EditorTabBody';
|
import { EditorTabBody } from './EditorTabBody';
|
||||||
import { DataSourcePicker } from './DataSourcePicker';
|
import { DataSourcePicker } from './DataSourcePicker';
|
||||||
import { PanelModel } from '../panel_model';
|
|
||||||
import { DashboardModel } from '../dashboard_model';
|
|
||||||
import './../../panel/metrics_tab';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { QueryInspector } from './QueryInspector';
|
import { QueryInspector } from './QueryInspector';
|
||||||
import { TimeRangeOptions } from './TimeRangeOptions';
|
import { TimeRangeOptions } from './TimeRangeOptions';
|
||||||
|
import './../../panel/metrics_tab';
|
||||||
|
import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { DataSourceSelectItem } from 'app/types';
|
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
import Remarkable from 'remarkable';
|
// Types
|
||||||
|
import { PanelModel } from '../panel_model';
|
||||||
|
import { DashboardModel } from '../dashboard_model';
|
||||||
|
import { DataSourceSelectItem, DataQuery } from 'app/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Help {
|
|
||||||
isLoading: boolean;
|
|
||||||
helpHtml: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
currentDatasource: DataSourceSelectItem;
|
currentDS: DataSourceSelectItem;
|
||||||
help: Help;
|
helpContent: JSX.Element;
|
||||||
hideTimeOverride: boolean;
|
isLoadingHelp: boolean;
|
||||||
|
isPickerOpen: boolean;
|
||||||
|
isAddingMixed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingPlaceholderProps {
|
interface LoadingPlaceholderProps {
|
||||||
@ -40,7 +43,7 @@ interface LoadingPlaceholderProps {
|
|||||||
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
|
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
|
||||||
|
|
||||||
export class QueriesTab extends PureComponent<Props, State> {
|
export class QueriesTab extends PureComponent<Props, State> {
|
||||||
element: any;
|
element: HTMLElement;
|
||||||
component: AngularComponent;
|
component: AngularComponent;
|
||||||
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
|
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
|
||||||
backendSrv: BackendSrv = getBackendSrv();
|
backendSrv: BackendSrv = getBackendSrv();
|
||||||
@ -50,12 +53,26 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
const { panel } = props;
|
const { panel } = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentDatasource: this.datasources.find(datasource => datasource.value === panel.datasource),
|
currentDS: this.datasources.find(datasource => datasource.value === panel.datasource),
|
||||||
help: {
|
isLoadingHelp: false,
|
||||||
isLoading: false,
|
helpContent: null,
|
||||||
helpHtml: null,
|
isPickerOpen: false,
|
||||||
},
|
isAddingMixed: false,
|
||||||
hideTimeOverride: false,
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAngularQueryComponentScope(): AngularQueryComponentScope {
|
||||||
|
const { panel, dashboard } = this.props;
|
||||||
|
|
||||||
|
return {
|
||||||
|
panel: panel,
|
||||||
|
dashboard: dashboard,
|
||||||
|
refresh: () => panel.refresh(),
|
||||||
|
render: () => panel.render,
|
||||||
|
addQuery: this.onAddQuery,
|
||||||
|
moveQuery: this.onMoveQuery,
|
||||||
|
removeQuery: this.onRemoveQuery,
|
||||||
|
events: panel.events,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,15 +81,10 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { panel, dashboard } = this.props;
|
|
||||||
const loader = getAngularLoader();
|
const loader = getAngularLoader();
|
||||||
const template = '<metrics-tab />';
|
const template = '<metrics-tab />';
|
||||||
const scopeProps = {
|
const scopeProps = {
|
||||||
ctrl: {
|
ctrl: this.getAngularQueryComponentScope(),
|
||||||
panel: panel,
|
|
||||||
dashboard: dashboard,
|
|
||||||
refresh: () => panel.refresh(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.component = loader.load(this.element, scopeProps, template);
|
this.component = loader.load(this.element, scopeProps, template);
|
||||||
@ -86,7 +98,8 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
onChangeDataSource = datasource => {
|
onChangeDataSource = datasource => {
|
||||||
const { panel } = this.props;
|
const { panel } = this.props;
|
||||||
const { currentDatasource } = this.state;
|
const { currentDS } = this.state;
|
||||||
|
|
||||||
// switching to mixed
|
// switching to mixed
|
||||||
if (datasource.meta.mixed) {
|
if (datasource.meta.mixed) {
|
||||||
panel.targets.forEach(target => {
|
panel.targets.forEach(target => {
|
||||||
@ -95,62 +108,58 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
target.datasource = config.defaultDatasource;
|
target.datasource = config.defaultDatasource;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (currentDatasource && currentDatasource.meta.mixed) {
|
} else if (currentDS) {
|
||||||
panel.targets.forEach(target => {
|
// if switching from mixed
|
||||||
delete target.datasource;
|
if (currentDS.meta.mixed) {
|
||||||
});
|
for (const target of panel.targets) {
|
||||||
|
delete target.datasource;
|
||||||
|
}
|
||||||
|
} else if (currentDS.meta.id !== datasource.meta.id) {
|
||||||
|
// we are changing data source type, clear queries
|
||||||
|
panel.targets = [{ refId: 'A' }];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.datasource = datasource.value;
|
panel.datasource = datasource.value;
|
||||||
panel.refresh();
|
panel.refresh();
|
||||||
|
|
||||||
this.setState(prevState => ({
|
this.setState({
|
||||||
...prevState,
|
currentDS: datasource,
|
||||||
currentDatasource: datasource,
|
});
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadHelp = () => {
|
loadHelp = () => {
|
||||||
const { currentDatasource } = this.state;
|
const { currentDS } = this.state;
|
||||||
const hasHelp = currentDatasource.meta.hasQueryHelp;
|
const hasHelp = currentDS.meta.hasQueryHelp;
|
||||||
|
|
||||||
if (hasHelp) {
|
if (hasHelp) {
|
||||||
this.setState(prevState => ({
|
this.setState({
|
||||||
...prevState,
|
helpContent: <h3>Loading help...</h3>,
|
||||||
help: {
|
isLoadingHelp: true,
|
||||||
helpHtml: <h2>Loading help...</h2>,
|
});
|
||||||
isLoading: true,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.backendSrv
|
this.backendSrv
|
||||||
.get(`/api/plugins/${currentDatasource.meta.id}/markdown/query_help`)
|
.get(`/api/plugins/${currentDS.meta.id}/markdown/query_help`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const md = new Remarkable();
|
const md = new Remarkable();
|
||||||
const helpHtml = md.render(res); // TODO: Clean out dangerous code? Previous: this.helpHtml = this.$sce.trustAsHtml(md.render(res));
|
const helpHtml = md.render(res);
|
||||||
this.setState(prevState => ({
|
this.setState({
|
||||||
...prevState,
|
helpContent: <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />,
|
||||||
help: {
|
isLoadingHelp: false,
|
||||||
helpHtml: <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />,
|
});
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.setState(prevState => ({
|
this.setState({
|
||||||
...prevState,
|
helpContent: <h3>'Error occured when loading help'</h3>,
|
||||||
help: {
|
isLoadingHelp: false,
|
||||||
helpHtml: 'Error occured when loading help',
|
});
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderOptions = close => {
|
renderOptions = close => {
|
||||||
const { currentDatasource } = this.state;
|
const { currentDS } = this.state;
|
||||||
const { queryOptions } = currentDatasource.meta;
|
const { queryOptions } = currentDS.meta;
|
||||||
const { panel } = this.props;
|
const { panel } = this.props;
|
||||||
|
|
||||||
const onChangeFn = (panelKey: string) => {
|
const onChangeFn = (panelKey: string) => {
|
||||||
@ -223,26 +232,83 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderHelp = () => {
|
renderHelp = () => {
|
||||||
const { helpHtml, isLoading } = this.state.help;
|
const { helpContent, isLoadingHelp } = this.state;
|
||||||
return isLoading ? <LoadingPlaceholder text="Loading help..." /> : helpHtml;
|
return isLoadingHelp ? <LoadingPlaceholder text="Loading help..." /> : helpContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddQuery = (query?: Partial<DataQuery>) => {
|
||||||
|
this.props.panel.addQuery(query);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddQueryClick = () => {
|
||||||
|
if (this.state.currentDS.meta.mixed) {
|
||||||
|
this.setState({ isAddingMixed: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.panel.addQuery();
|
||||||
|
this.component.digest();
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveQuery = (query: DataQuery) => {
|
||||||
|
const { panel } = this.props;
|
||||||
|
|
||||||
|
const index = _.indexOf(panel.targets, query);
|
||||||
|
panel.targets.splice(index, 1);
|
||||||
|
panel.refresh();
|
||||||
|
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMoveQuery = (query: DataQuery, direction: number) => {
|
||||||
|
const { panel } = this.props;
|
||||||
|
|
||||||
|
const index = _.indexOf(panel.targets, query);
|
||||||
|
_.move(panel.targets, index, index + direction);
|
||||||
|
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolbar = () => {
|
||||||
|
const { currentDS } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataSourcePicker
|
||||||
|
datasources={this.datasources}
|
||||||
|
onChangeDataSource={this.onChangeDataSource}
|
||||||
|
current={currentDS}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderMixedPicker = () => {
|
||||||
|
return (
|
||||||
|
<DataSourcePicker
|
||||||
|
datasources={this.datasources}
|
||||||
|
onChangeDataSource={this.onAddMixedQuery}
|
||||||
|
current={null}
|
||||||
|
autoFocus={true}
|
||||||
|
onBlur={this.onMixedPickerBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMixedQuery = datasource => {
|
||||||
|
this.onAddQuery({ datasource: datasource.name });
|
||||||
|
this.component.digest();
|
||||||
|
this.setState({ isAddingMixed: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMixedPickerBlur = () => {
|
||||||
|
this.setState({ isAddingMixed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentDatasource } = this.state;
|
const { panel } = this.props;
|
||||||
const { hasQueryHelp } = currentDatasource.meta;
|
const { currentDS, isAddingMixed } = this.state;
|
||||||
const dsInformation = {
|
const { hasQueryHelp } = currentDS.meta;
|
||||||
title: currentDatasource.name,
|
|
||||||
imgSrc: currentDatasource.meta.info.logos.small,
|
|
||||||
render: closeOpenView => (
|
|
||||||
<DataSourcePicker
|
|
||||||
datasources={this.datasources}
|
|
||||||
onChangeDataSource={ds => {
|
|
||||||
closeOpenView();
|
|
||||||
this.onChangeDataSource(ds);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryInspector = {
|
const queryInspector = {
|
||||||
title: 'Query Inspector',
|
title: 'Query Inspector',
|
||||||
@ -265,10 +331,31 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorTabBody heading="Queries" main={dsInformation} toolbarItems={[options, queryInspector, dsHelp]}>
|
<EditorTabBody
|
||||||
<>
|
heading="Queries"
|
||||||
<div ref={element => (this.element = element)} style={{ width: '100%' }} />
|
renderToolbar={this.renderToolbar}
|
||||||
</>
|
toolbarItems={[options, queryInspector, dsHelp]}
|
||||||
|
>
|
||||||
|
<div className="query-editor-rows gf-form-group">
|
||||||
|
<div ref={element => (this.element = element)} />
|
||||||
|
|
||||||
|
<div className="gf-form-query">
|
||||||
|
<div className="gf-form gf-form-query-letter-cell">
|
||||||
|
<label className="gf-form-label">
|
||||||
|
<span className="gf-form-query-letter-cell-carret muted">
|
||||||
|
<i className="fa fa-caret-down" />
|
||||||
|
</span>
|
||||||
|
<span className="gf-form-query-letter-cell-letter">{panel.getNextQueryLetter()}</span>
|
||||||
|
</label>
|
||||||
|
{!isAddingMixed && (
|
||||||
|
<button className="btn btn-secondary gf-form-btn" onClick={this.onAddQueryClick}>
|
||||||
|
Add Query
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isAddingMixed && this.renderMixedPicker()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</EditorTabBody>
|
</EditorTabBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
|
|||||||
// Components
|
// Components
|
||||||
import { EditorTabBody } from './EditorTabBody';
|
import { EditorTabBody } from './EditorTabBody';
|
||||||
import { VizTypePicker } from './VizTypePicker';
|
import { VizTypePicker } from './VizTypePicker';
|
||||||
|
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
@ -21,9 +22,24 @@ interface Props {
|
|||||||
onTypeChanged: (newType: PanelPlugin) => void;
|
onTypeChanged: (newType: PanelPlugin) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VisualizationTab extends PureComponent<Props> {
|
interface State {
|
||||||
|
isVizPickerOpen: boolean;
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VisualizationTab extends PureComponent<Props, State> {
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
angularOptions: AngularComponent;
|
angularOptions: AngularComponent;
|
||||||
|
searchInput: HTMLElement;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isVizPickerOpen: false,
|
||||||
|
searchQuery: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getPanelDefaultOptions = () => {
|
getPanelDefaultOptions = () => {
|
||||||
const { panel, plugin } = this.props;
|
const { panel, plugin } = this.props;
|
||||||
@ -87,10 +103,11 @@ export class VisualizationTab extends PureComponent<Props> {
|
|||||||
|
|
||||||
let template = '';
|
let template = '';
|
||||||
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
|
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
|
||||||
template += `
|
template +=
|
||||||
<div class="form-section" ng-cloak>
|
`
|
||||||
<div class="form-section__header">{{ctrl.editorTabs[${i}].title}}</div>
|
<div class="form-section" ng-cloak>` +
|
||||||
<div class="form-section__body">
|
(i > 0 ? `<div class="form-section__header">{{ctrl.editorTabs[${i}].title}}</div>` : '') +
|
||||||
|
`<div class="form-section__body">
|
||||||
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,28 +136,81 @@ export class VisualizationTab extends PureComponent<Props> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onOpenVizPicker = () => {
|
||||||
|
this.setState({ isVizPickerOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCloseVizPicker = () => {
|
||||||
|
this.setState({ isVizPickerOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchQueryChange = evt => {
|
||||||
|
const value = evt.target.value;
|
||||||
|
this.setState({
|
||||||
|
searchQuery: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolbar = (): JSX.Element => {
|
||||||
|
const { plugin } = this.props;
|
||||||
|
const { searchQuery } = this.state;
|
||||||
|
|
||||||
|
if (this.state.isVizPickerOpen) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className="gf-form--has-input-icon">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input width-13"
|
||||||
|
placeholder=""
|
||||||
|
onChange={this.onSearchQueryChange}
|
||||||
|
value={searchQuery}
|
||||||
|
ref={elem => elem && elem.focus()}
|
||||||
|
/>
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<button className="btn btn-link" onClick={this.onCloseVizPicker}>
|
||||||
|
<i className="fa fa-chevron-up" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="toolbar__main" onClick={this.onOpenVizPicker}>
|
||||||
|
<img className="toolbar__main-image" src={plugin.info.logos.small} />
|
||||||
|
<div className="toolbar__main-name">{plugin.name}</div>
|
||||||
|
<i className="fa fa-caret-down" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onTypeChanged = (plugin: PanelPlugin) => {
|
||||||
|
if (plugin.id === this.props.plugin.id) {
|
||||||
|
this.setState({ isVizPickerOpen: false });
|
||||||
|
} else {
|
||||||
|
this.props.onTypeChanged(plugin);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { plugin } = this.props;
|
const { plugin } = this.props;
|
||||||
|
const { isVizPickerOpen, searchQuery } = this.state;
|
||||||
const panelSelection = {
|
|
||||||
title: plugin.name,
|
|
||||||
imgSrc: plugin.info.logos.small,
|
|
||||||
render: () => {
|
|
||||||
// the needs to be scoped inside this closure
|
|
||||||
const { plugin, onTypeChanged } = this.props;
|
|
||||||
return <VizTypePicker current={plugin} onTypeChanged={onTypeChanged} />;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelHelp = {
|
|
||||||
title: '',
|
|
||||||
icon: 'fa fa-question',
|
|
||||||
render: () => <h2>Help</h2>,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorTabBody heading="Visualization" main={panelSelection} toolbarItems={[panelHelp]}>
|
<EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar}>
|
||||||
{this.renderPanelOptions()}
|
<>
|
||||||
|
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
|
||||||
|
<VizTypePicker
|
||||||
|
current={plugin}
|
||||||
|
onTypeChanged={this.onTypeChanged}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onClose={this.onCloseVizPicker}
|
||||||
|
/>
|
||||||
|
</FadeIn>
|
||||||
|
{this.renderPanelOptions()}
|
||||||
|
</>
|
||||||
</EditorTabBody>
|
</EditorTabBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,27 +4,20 @@ import _ from 'lodash';
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { PanelPlugin } from 'app/types/plugins';
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
import VizTypePickerPlugin from './VizTypePickerPlugin';
|
import VizTypePickerPlugin from './VizTypePickerPlugin';
|
||||||
import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
current: PanelPlugin;
|
current: PanelPlugin;
|
||||||
onTypeChanged: (newType: PanelPlugin) => void;
|
onTypeChanged: (newType: PanelPlugin) => void;
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VizTypePicker extends PureComponent<Props, State> {
|
export class VizTypePicker extends PureComponent<Props> {
|
||||||
searchInput: HTMLElement;
|
searchInput: HTMLElement;
|
||||||
pluginList = this.getPanelPlugins('');
|
pluginList = this.getPanelPlugins('');
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
searchQuery: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxSelectedIndex() {
|
get maxSelectedIndex() {
|
||||||
@ -32,12 +25,6 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
return filteredPluginList.length - 1;
|
return filteredPluginList.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.searchInput.focus();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelPlugins(filter): PanelPlugin[] {
|
getPanelPlugins(filter): PanelPlugin[] {
|
||||||
const panels = _.chain(config.panels)
|
const panels = _.chain(config.panels)
|
||||||
.filter({ hideFromList: false })
|
.filter({ hideFromList: false })
|
||||||
@ -48,27 +35,22 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
return _.sortBy(panels, 'sort');
|
return _.sortBy(panels, 'sort');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderVizPlugin = (plugin: PanelPlugin, index: number, keyNavProps: KeyboardNavigationProps) => {
|
renderVizPlugin = (plugin: PanelPlugin, index: number) => {
|
||||||
const { onTypeChanged } = this.props;
|
const { onTypeChanged } = this.props;
|
||||||
const { selected, onMouseEnter } = keyNavProps;
|
|
||||||
const isSelected = selected === index;
|
|
||||||
const isCurrent = plugin.id === this.props.current.id;
|
const isCurrent = plugin.id === this.props.current.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VizTypePickerPlugin
|
<VizTypePickerPlugin
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
isSelected={isSelected}
|
|
||||||
isCurrent={isCurrent}
|
isCurrent={isCurrent}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
onMouseEnter={() => {
|
|
||||||
onMouseEnter(index);
|
|
||||||
}}
|
|
||||||
onClick={() => onTypeChanged(plugin)}
|
onClick={() => onTypeChanged(plugin)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getFilteredPluginList = (): PanelPlugin[] => {
|
getFilteredPluginList = (): PanelPlugin[] => {
|
||||||
const { searchQuery } = this.state;
|
const { searchQuery } = this.props;
|
||||||
const regex = new RegExp(searchQuery, 'i');
|
const regex = new RegExp(searchQuery, 'i');
|
||||||
const pluginList = this.pluginList;
|
const pluginList = this.pluginList;
|
||||||
|
|
||||||
@ -79,57 +61,15 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
|||||||
return filtered;
|
return filtered;
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchQueryChange = evt => {
|
|
||||||
const value = evt.target.value;
|
|
||||||
this.setState(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
searchQuery: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
renderFilters = ({ onKeyDown, selected }: KeyboardNavigationProps) => {
|
|
||||||
const { searchQuery } = this.state;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="gf-form--has-input-icon">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="gf-form-input width-13"
|
|
||||||
placeholder=""
|
|
||||||
ref={elem => (this.searchInput = elem)}
|
|
||||||
onChange={this.onSearchQueryChange}
|
|
||||||
value={searchQuery}
|
|
||||||
onKeyDown={evt => {
|
|
||||||
onKeyDown(evt, this.maxSelectedIndex, () => {
|
|
||||||
const { onTypeChanged } = this.props;
|
|
||||||
const vizType = this.getFilteredPluginList()[selected];
|
|
||||||
onTypeChanged(vizType);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const filteredPluginList = this.getFilteredPluginList();
|
const filteredPluginList = this.getFilteredPluginList();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardNavigation
|
<div className="viz-picker">
|
||||||
render={(keyNavProps: KeyboardNavigationProps) => (
|
<div className="viz-picker-list">
|
||||||
<>
|
{filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index))}
|
||||||
<div className="cta-form__bar">
|
</div>
|
||||||
{this.renderFilters(keyNavProps)}
|
</div>
|
||||||
<div className="gf-form--grow" />
|
|
||||||
</div>
|
|
||||||
<div className="viz-picker">
|
|
||||||
{filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index, keyNavProps))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { PanelPlugin } from 'app/types/plugins';
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSelected: boolean;
|
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onMouseEnter: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VizTypePickerPlugin = React.memo(
|
const VizTypePickerPlugin = React.memo(
|
||||||
({ isSelected, isCurrent, plugin, onClick, onMouseEnter }: Props) => {
|
({ isCurrent, plugin, onClick }: Props) => {
|
||||||
const cssClass = classNames({
|
const cssClass = classNames({
|
||||||
'viz-picker__item': true,
|
'viz-picker__item': true,
|
||||||
'viz-picker__item--selected': isSelected,
|
|
||||||
'viz-picker__item--current': isCurrent,
|
'viz-picker__item--current': isCurrent,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssClass} onClick={onClick} title={plugin.name} onMouseEnter={onMouseEnter}>
|
<div className={cssClass} onClick={onClick} title={plugin.name}>
|
||||||
<div className="viz-picker__item-name">{plugin.name}</div>
|
<div className="viz-picker__item-name">{plugin.name}</div>
|
||||||
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prevProps, nextProps) => {
|
(prevProps, nextProps) => {
|
||||||
if (prevProps.isSelected === nextProps.isSelected && prevProps.isCurrent === nextProps.isCurrent) {
|
if (prevProps.isCurrent === nextProps.isCurrent) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
||||||
|
import { DataQuery } from 'app/types';
|
||||||
|
|
||||||
export interface GridPos {
|
export interface GridPos {
|
||||||
x: number;
|
x: number;
|
||||||
@ -237,6 +238,24 @@ export class PanelModel {
|
|||||||
this.restorePanelOptions(pluginId);
|
this.restorePanelOptions(pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addQuery(query?: Partial<DataQuery>) {
|
||||||
|
query = query || { refId: 'A' };
|
||||||
|
query.refId = this.getNextQueryLetter();
|
||||||
|
query.isNew = true;
|
||||||
|
|
||||||
|
this.targets.push(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextQueryLetter(): string {
|
||||||
|
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
return _.find(letters, refId => {
|
||||||
|
return _.every(this.targets, other => {
|
||||||
|
return other.refId !== refId;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.events.emit('panel-teardown');
|
this.events.emit('panel-teardown');
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
|
@ -1,181 +1,24 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Remarkable from 'remarkable';
|
|
||||||
|
|
||||||
// Services & utils
|
// Services & utils
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import config from 'app/core/config';
|
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DashboardModel } from '../dashboard/dashboard_model';
|
import { DashboardModel } from '../dashboard/dashboard_model';
|
||||||
|
import { PanelModel } from '../dashboard/panel_model';
|
||||||
|
import { DataQuery } from 'app/types';
|
||||||
|
|
||||||
export class MetricsTabCtrl {
|
export interface AngularQueryComponentScope {
|
||||||
dsName: string;
|
panel: PanelModel;
|
||||||
panel: any;
|
|
||||||
panelCtrl: any;
|
|
||||||
datasources: any[];
|
|
||||||
datasourceInstance: any;
|
|
||||||
nextRefId: string;
|
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
panelDsValue: any;
|
|
||||||
addQueryDropdown: any;
|
|
||||||
queryTroubleshooterOpen: boolean;
|
|
||||||
helpOpen: boolean;
|
|
||||||
optionsOpen: boolean;
|
|
||||||
hasQueryHelp: boolean;
|
|
||||||
helpHtml: string;
|
|
||||||
queryOptions: any;
|
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
|
refresh: () => void;
|
||||||
/** @ngInject */
|
render: () => void;
|
||||||
constructor($scope, private $sce, datasourceSrv, private backendSrv) {
|
removeQuery: (query: DataQuery) => void;
|
||||||
this.panelCtrl = $scope.ctrl;
|
addQuery: (query?: DataQuery) => void;
|
||||||
$scope.ctrl = this;
|
moveQuery: (query: DataQuery, direction: number) => void;
|
||||||
|
|
||||||
this.panel = this.panelCtrl.panel;
|
|
||||||
this.panel.datasource = this.panel.datasource || null;
|
|
||||||
this.panel.targets = this.panel.targets || [{}];
|
|
||||||
|
|
||||||
this.dashboard = this.panelCtrl.dashboard;
|
|
||||||
this.datasources = datasourceSrv.getMetricSources();
|
|
||||||
this.panelDsValue = this.panelCtrl.panel.datasource;
|
|
||||||
|
|
||||||
// added here as old query controller expects this on panelCtrl but
|
|
||||||
// they are getting MetricsTabCtrl instead
|
|
||||||
this.events = this.panel.events;
|
|
||||||
|
|
||||||
for (const ds of this.datasources) {
|
|
||||||
if (ds.value === this.panelDsValue) {
|
|
||||||
this.datasourceInstance = ds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addQueryDropdown = { text: 'Add Query', value: null, fake: true };
|
|
||||||
|
|
||||||
// update next ref id
|
|
||||||
this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
|
||||||
this.updateDatasourceOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDatasourceOptions() {
|
|
||||||
if (this.datasourceInstance) {
|
|
||||||
this.hasQueryHelp = this.datasourceInstance.meta.hasQueryHelp;
|
|
||||||
this.queryOptions = this.datasourceInstance.meta.queryOptions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getOptions(includeBuiltin) {
|
|
||||||
return Promise.resolve(
|
|
||||||
this.datasources
|
|
||||||
.filter(value => {
|
|
||||||
return includeBuiltin || !value.meta.builtIn;
|
|
||||||
})
|
|
||||||
.map(ds => {
|
|
||||||
return { value: ds.value, text: ds.name, datasource: ds };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
datasourceChanged(option) {
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setDatasource(option.datasource);
|
|
||||||
this.updateDatasourceOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
setDatasource(datasource) {
|
|
||||||
// switching to mixed
|
|
||||||
if (datasource.meta.mixed) {
|
|
||||||
_.each(this.panel.targets, target => {
|
|
||||||
target.datasource = this.panel.datasource;
|
|
||||||
if (!target.datasource) {
|
|
||||||
target.datasource = config.defaultDatasource;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (this.datasourceInstance) {
|
|
||||||
// if switching from mixed
|
|
||||||
if (this.datasourceInstance.meta.mixed) {
|
|
||||||
_.each(this.panel.targets, target => {
|
|
||||||
delete target.datasource;
|
|
||||||
});
|
|
||||||
} else if (this.datasourceInstance.meta.id !== datasource.meta.id) {
|
|
||||||
// we are changing data source type, clear queries
|
|
||||||
this.panel.targets = [{ refId: 'A' }];
|
|
||||||
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.datasourceInstance = datasource;
|
|
||||||
this.panel.datasource = datasource.value;
|
|
||||||
this.panel.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
addMixedQuery(option) {
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.panelCtrl.addQuery({
|
|
||||||
isNew: true,
|
|
||||||
datasource: option.datasource.name,
|
|
||||||
});
|
|
||||||
this.addQueryDropdown = { text: 'Add Query', value: null, fake: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleHelp() {
|
|
||||||
this.optionsOpen = false;
|
|
||||||
this.queryTroubleshooterOpen = false;
|
|
||||||
this.helpOpen = !this.helpOpen;
|
|
||||||
|
|
||||||
this.backendSrv.get(`/api/plugins/${this.datasourceInstance.meta.id}/markdown/query_help`).then(res => {
|
|
||||||
const md = new Remarkable();
|
|
||||||
this.helpHtml = this.$sce.trustAsHtml(md.render(res));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleOptions() {
|
|
||||||
this.helpOpen = false;
|
|
||||||
this.queryTroubleshooterOpen = false;
|
|
||||||
this.optionsOpen = !this.optionsOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleQueryTroubleshooter() {
|
|
||||||
this.helpOpen = false;
|
|
||||||
this.optionsOpen = false;
|
|
||||||
this.queryTroubleshooterOpen = !this.queryTroubleshooterOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
addQuery(query?) {
|
|
||||||
query = query || {};
|
|
||||||
query.refId = this.dashboard.getNextQueryLetter(this.panel);
|
|
||||||
query.isNew = true;
|
|
||||||
|
|
||||||
this.panel.targets.push(query);
|
|
||||||
this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
this.panel.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.panel.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeQuery(target) {
|
|
||||||
const index = _.indexOf(this.panel.targets, target);
|
|
||||||
this.panel.targets.splice(index, 1);
|
|
||||||
this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
|
||||||
this.panel.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
moveQuery(target, direction) {
|
|
||||||
const index = _.indexOf(this.panel.targets, target);
|
|
||||||
_.move(this.panel.targets, index, index + direction);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -185,7 +28,6 @@ export function metricsTabDirective() {
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: true,
|
scope: true,
|
||||||
templateUrl: 'public/app/features/panel/partials/metrics_tab.html',
|
templateUrl: 'public/app/features/panel/partials/metrics_tab.html',
|
||||||
controller: MetricsTabCtrl,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
<div class="query-editor-rows gf-form-group" ng-if="ctrl.datasourceInstance">
|
|
||||||
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
|
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
|
||||||
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
|
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
|
||||||
<plugin-component type="query-ctrl">
|
<plugin-component type="query-ctrl">
|
||||||
@ -7,21 +5,20 @@
|
|||||||
</rebuild-on-change>
|
</rebuild-on-change>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-query">
|
<!-- <div class="gf-form-query"> -->
|
||||||
<div class="gf-form gf-form-query-letter-cell">
|
<!-- <div class="gf-form gf-form-query-letter-cell"> -->
|
||||||
<label class="gf-form-label">
|
<!-- <label class="gf-form-label"> -->
|
||||||
<span class="gf-form-query-letter-cell-carret">
|
<!-- <span class="gf-form-query-letter-cell-carret"> -->
|
||||||
<i class="fa fa-caret-down"></i>
|
<!-- <i class="fa fa-caret-down"></i> -->
|
||||||
</span>
|
<!-- </span> -->
|
||||||
<span class="gf-form-query-letter-cell-letter">{{ctrl.nextRefId}}</span>
|
<!-- <span class="gf-form-query-letter-cell-letter">{{ctrl.nextRefId}}</span> -->
|
||||||
</label>
|
<!-- </label> -->
|
||||||
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.datasourceInstance.meta.mixed">
|
<!-- <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.datasourceInstance.meta.mixed"> -->
|
||||||
Add Query
|
<!-- Add Query -->
|
||||||
</button>
|
<!-- </button> -->
|
||||||
<div class="dropdown" ng-if="ctrl.datasourceInstance.meta.mixed">
|
<!-- <div class="dropdown" ng-if="ctrl.datasourceInstance.meta.mixed"> -->
|
||||||
<gf-form-dropdown model="ctrl.addQueryDropdown" get-options="ctrl.getOptions(false)" on-change="ctrl.addMixedQuery($option)">
|
<!-- <gf-form-dropdown model="ctrl.addQueryDropdown" get-options="ctrl.getOptions(false)" on-change="ctrl.addMixedQuery($option)"> -->
|
||||||
</gf-form-dropdown>
|
<!-- </gf-form-dropdown> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</div>
|
|
||||||
|
@ -20,7 +20,7 @@ export class QueryRowCtrl {
|
|||||||
this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions;
|
this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions;
|
||||||
|
|
||||||
if (!this.target.refId) {
|
if (!this.target.refId) {
|
||||||
this.target.refId = this.panelCtrl.dashboard.getNextQueryLetter(this.panel);
|
this.target.refId = this.panel.getNextQueryLetter();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleCollapse(true);
|
this.toggleCollapse(true);
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form max-width-26">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-8">Legend format</label>
|
<label class="gf-form-label width-8">Legend format</label>
|
||||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
|
<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
|
||||||
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
|
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
|
||||||
@ -58,4 +58,4 @@
|
|||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</query-editor-row>
|
</query-editor-row>
|
||||||
|
155
public/app/plugins/panel/gauge/MappingRow.tsx
Normal file
155
public/app/plugins/panel/gauge/MappingRow.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { Label } from 'app/core/components/Label/Label';
|
||||||
|
import SimplePicker from 'app/core/components/Picker/SimplePicker';
|
||||||
|
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mapping: ValueMap | RangeMap;
|
||||||
|
updateMapping: (mapping) => void;
|
||||||
|
removeMapping: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
from: string;
|
||||||
|
id: number;
|
||||||
|
operator: string;
|
||||||
|
text: string;
|
||||||
|
to: string;
|
||||||
|
type: MappingType;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingOptions = [
|
||||||
|
{ value: MappingType.ValueToText, label: 'Value' },
|
||||||
|
{ value: MappingType.RangeToText, label: 'Range' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default class MappingRow extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
...props.mapping,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMappingValueChange = event => {
|
||||||
|
this.setState({ value: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingFromChange = event => {
|
||||||
|
this.setState({ from: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingToChange = event => {
|
||||||
|
this.setState({ to: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingTextChange = event => {
|
||||||
|
this.setState({ text: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingTypeChange = mappingType => {
|
||||||
|
this.setState({ type: mappingType });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMapping = () => {
|
||||||
|
this.props.updateMapping({ ...this.state });
|
||||||
|
};
|
||||||
|
|
||||||
|
renderRow() {
|
||||||
|
const { from, text, to, type, value } = this.state;
|
||||||
|
|
||||||
|
if (type === MappingType.RangeToText) {
|
||||||
|
return (
|
||||||
|
<div className="gf-form">
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>From</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
value={from}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingFromChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>To</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
value={to}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingToChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>Text</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
value={text}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingTextChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form">
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>Value</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingValueChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>Text</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
value={text}
|
||||||
|
onChange={this.onMappingTextChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { type } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mapping-row">
|
||||||
|
<div className="gf-form-inline mapping-row-type">
|
||||||
|
<Label width={5}>Type</Label>
|
||||||
|
<SimplePicker
|
||||||
|
placeholder="Choose type"
|
||||||
|
options={mappingOptions}
|
||||||
|
value={mappingOptions.find(o => o.value === type)}
|
||||||
|
getOptionLabel={i => i.label}
|
||||||
|
getOptionValue={i => i.value}
|
||||||
|
onSelected={type => this.onMappingTypeChange(type.value)}
|
||||||
|
width={7}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>{this.renderRow()}</div>
|
||||||
|
<div onClick={this.props.removeMapping} className="threshold-row-remove">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import Thresholds, { BasicGaugeColor } from './Thresholds';
|
import Thresholds from './Thresholds';
|
||||||
import { OptionsProps } from './module';
|
import { defaultProps, OptionsProps } from './module';
|
||||||
import { PanelOptionsProps } from '../../../types';
|
import { PanelOptionsProps } from '../../../types';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: PanelOptionsProps<OptionsProps> = {
|
const props: PanelOptionsProps<OptionsProps> = {
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
options: {} as OptionsProps,
|
options: {
|
||||||
|
...defaultProps.options,
|
||||||
|
thresholds: [
|
||||||
|
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||||
|
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@ -15,12 +21,6 @@ const setup = (propOverrides?: object) => {
|
|||||||
return shallow(<Thresholds {...props} />).instance() as Thresholds;
|
return shallow(<Thresholds {...props} />).instance() as Thresholds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const thresholds = [
|
|
||||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
|
||||||
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
|
||||||
{ index: 2, label: 'Max', value: 100, canRemove: false, color: BasicGaugeColor.Red },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('Add threshold', () => {
|
describe('Add threshold', () => {
|
||||||
it('should add threshold between min and max', () => {
|
it('should add threshold between min and max', () => {
|
||||||
const instance = setup();
|
const instance = setup();
|
||||||
@ -28,24 +28,31 @@ describe('Add threshold', () => {
|
|||||||
instance.onAddThreshold(1);
|
instance.onAddThreshold(1);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||||
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(131, 123, 52, 0.99)' },
|
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||||
{ index: 2, label: 'Max', value: 100, canRemove: false, color: BasicGaugeColor.Red },
|
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add threshold between min and added threshold', () => {
|
it('should add threshold between min and added threshold', () => {
|
||||||
const instance = setup({
|
const instance = setup({
|
||||||
options: { thresholds: thresholds },
|
options: {
|
||||||
|
...defaultProps.options,
|
||||||
|
thresholds: [
|
||||||
|
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||||
|
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||||
|
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.onAddThreshold(1);
|
instance.onAddThreshold(1);
|
||||||
|
|
||||||
expect(instance.state.thresholds).toEqual([
|
expect(instance.state.thresholds).toEqual([
|
||||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||||
{ index: 1, label: '', value: 25, canRemove: true, color: 'rgba(144, 151, 43, 0.93)' },
|
{ index: 1, label: '', value: 25, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||||
{ index: 2, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
{ index: 2, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||||
{ index: 3, label: 'Max', value: 100, canRemove: false, color: BasicGaugeColor.Red },
|
{ index: 3, label: 'Max', value: 100, canRemove: false },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,27 +3,18 @@ import classNames from 'classnames/bind';
|
|||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
||||||
import { OptionModuleProps } from './module';
|
import { OptionModuleProps } from './module';
|
||||||
import { Threshold } from 'app/types';
|
import { BasicGaugeColor, Threshold } from 'app/types';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BasicGaugeColor {
|
|
||||||
Green = 'rgba(50, 172, 45, 0.97)',
|
|
||||||
Orange = 'rgba(237, 129, 40, 0.89)',
|
|
||||||
Red = 'rgb(212, 74, 58)',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
thresholds: this.props.options.thresholds || [
|
thresholds: props.options.thresholds,
|
||||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
|
||||||
{ index: 1, label: 'Max', value: 100, canRemove: false, color: BasicGaugeColor.Red },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
73
public/app/plugins/panel/gauge/ValueMappings.test.tsx
Normal file
73
public/app/plugins/panel/gauge/ValueMappings.test.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ValueMappings from './ValueMappings';
|
||||||
|
import { defaultProps, OptionModuleProps } from './module';
|
||||||
|
import { MappingType } from 'app/types';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: OptionModuleProps = {
|
||||||
|
onChange: jest.fn(),
|
||||||
|
options: {
|
||||||
|
...defaultProps.options,
|
||||||
|
mappings: [
|
||||||
|
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||||
|
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<ValueMappings {...props} />);
|
||||||
|
|
||||||
|
const instance = wrapper.instance() as ValueMappings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
wrapper,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('On remove mapping', () => {
|
||||||
|
it('Should remove mapping with id 0', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
instance.onRemoveMapping(1);
|
||||||
|
|
||||||
|
expect(instance.state.mappings).toEqual([
|
||||||
|
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove mapping with id 1', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
instance.onRemoveMapping(2);
|
||||||
|
|
||||||
|
expect(instance.state.mappings).toEqual([
|
||||||
|
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next id to add', () => {
|
||||||
|
it('should be 4', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
|
||||||
|
instance.addMapping();
|
||||||
|
|
||||||
|
expect(instance.state.nextIdToAdd).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to 1', () => {
|
||||||
|
const { instance } = setup({ options: { ...defaultProps.options } });
|
||||||
|
|
||||||
|
expect(instance.state.nextIdToAdd).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
100
public/app/plugins/panel/gauge/ValueMappings.tsx
Normal file
100
public/app/plugins/panel/gauge/ValueMappings.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import MappingRow from './MappingRow';
|
||||||
|
import { OptionModuleProps } from './module';
|
||||||
|
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
mappings: Array<ValueMap | RangeMap>;
|
||||||
|
nextIdToAdd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ValueMappings extends PureComponent<OptionModuleProps, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const mappings = props.options.mappings;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
mappings: mappings || [],
|
||||||
|
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxIdFromMappings(mappings) {
|
||||||
|
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMapping = () =>
|
||||||
|
this.setState(prevState => ({
|
||||||
|
mappings: [
|
||||||
|
...prevState.mappings,
|
||||||
|
{
|
||||||
|
id: prevState.nextIdToAdd,
|
||||||
|
operator: '',
|
||||||
|
value: '',
|
||||||
|
text: '',
|
||||||
|
type: MappingType.ValueToText,
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextIdToAdd: prevState.nextIdToAdd + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onRemoveMapping = id => {
|
||||||
|
this.setState(
|
||||||
|
prevState => ({
|
||||||
|
mappings: prevState.mappings.filter(m => {
|
||||||
|
return m.id !== id;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateGauge = mapping => {
|
||||||
|
this.setState(
|
||||||
|
prevState => ({
|
||||||
|
mappings: prevState.mappings.map(m => {
|
||||||
|
if (m.id === mapping.id) {
|
||||||
|
return { ...mapping };
|
||||||
|
}
|
||||||
|
|
||||||
|
return m;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { mappings } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="section gf-form-group">
|
||||||
|
<h5 className="page-heading">Value mappings</h5>
|
||||||
|
<div>
|
||||||
|
{mappings.length > 0 &&
|
||||||
|
mappings.map((mapping, index) => (
|
||||||
|
<MappingRow
|
||||||
|
key={`${mapping.text}-${index}`}
|
||||||
|
mapping={mapping}
|
||||||
|
updateMapping={this.updateGauge}
|
||||||
|
removeMapping={() => this.onRemoveMapping(mapping.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="add-mapping-row" onClick={this.addMapping}>
|
||||||
|
<div className="add-mapping-row-icon">
|
||||||
|
<i className="fa fa-plus" />
|
||||||
|
</div>
|
||||||
|
<div className="add-mapping-row-label">Add mapping</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="section gf-form-group"
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
className="page-heading"
|
||||||
|
>
|
||||||
|
Value mappings
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
<MappingRow
|
||||||
|
key="Ok-0"
|
||||||
|
mapping={
|
||||||
|
Object {
|
||||||
|
"id": 1,
|
||||||
|
"operator": "",
|
||||||
|
"text": "Ok",
|
||||||
|
"type": 1,
|
||||||
|
"value": "20",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeMapping={[Function]}
|
||||||
|
updateMapping={[Function]}
|
||||||
|
/>
|
||||||
|
<MappingRow
|
||||||
|
key="Meh-1"
|
||||||
|
mapping={
|
||||||
|
Object {
|
||||||
|
"from": "21",
|
||||||
|
"id": 2,
|
||||||
|
"operator": "",
|
||||||
|
"text": "Meh",
|
||||||
|
"to": "30",
|
||||||
|
"type": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeMapping={[Function]}
|
||||||
|
updateMapping={[Function]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="add-mapping-row"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="add-mapping-row-icon"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-plus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="add-mapping-row-label"
|
||||||
|
>
|
||||||
|
Add mapping
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -1,10 +1,19 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import Gauge from 'app/viz/Gauge';
|
import Gauge from 'app/viz/Gauge';
|
||||||
import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types';
|
|
||||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||||
import ValueOptions from './ValueOptions';
|
import ValueOptions from './ValueOptions';
|
||||||
import GaugeOptions from './GaugeOptions';
|
import GaugeOptions from './GaugeOptions';
|
||||||
import Thresholds from './Thresholds';
|
import Thresholds from './Thresholds';
|
||||||
|
import ValueMappings from './ValueMappings';
|
||||||
|
import {
|
||||||
|
BasicGaugeColor,
|
||||||
|
NullValueMode,
|
||||||
|
PanelOptionsProps,
|
||||||
|
PanelProps,
|
||||||
|
RangeMap,
|
||||||
|
Threshold,
|
||||||
|
ValueMap,
|
||||||
|
} from 'app/types';
|
||||||
|
|
||||||
export interface OptionsProps {
|
export interface OptionsProps {
|
||||||
decimals: number;
|
decimals: number;
|
||||||
@ -15,6 +24,7 @@ export interface OptionsProps {
|
|||||||
suffix: string;
|
suffix: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
|
mappings: Array<RangeMap | ValueMap>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionModuleProps {
|
export interface OptionModuleProps {
|
||||||
@ -30,6 +40,14 @@ export const defaultProps = {
|
|||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
suffix: '',
|
suffix: '',
|
||||||
|
decimals: 0,
|
||||||
|
stat: '',
|
||||||
|
unit: '',
|
||||||
|
mappings: [],
|
||||||
|
thresholds: [
|
||||||
|
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
||||||
|
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,11 +70,17 @@ class Options extends PureComponent<PanelOptionsProps<OptionsProps>> {
|
|||||||
static defaultProps = defaultProps;
|
static defaultProps = defaultProps;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { onChange, options } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ValueOptions onChange={this.props.onChange} options={this.props.options} />
|
<div className="form-section">
|
||||||
<GaugeOptions onChange={this.props.onChange} options={this.props.options} />
|
<ValueOptions onChange={onChange} options={options} />
|
||||||
<Thresholds onChange={this.props.onChange} options={this.props.options} />
|
<GaugeOptions onChange={onChange} options={options} />
|
||||||
|
<Thresholds onChange={onChange} options={options} />
|
||||||
|
</div>
|
||||||
|
<div className="form-section">
|
||||||
|
<ValueMappings onChange={onChange} options={options} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,200 +1,190 @@
|
|||||||
<div class="edit-tab-with-sidemenu">
|
<div class="edit-tab-content" ng-repeat="style in editor.panel.styles">
|
||||||
<aside class="edit-sidemenu-aside">
|
<p class="column-styles-heading">{{style.pattern || 'New rule'}}</p>
|
||||||
<ul class="edit-sidemenu">
|
<div class="section gf-form-group">
|
||||||
<li ng-repeat="style in editor.panel.styles" ng-class="{active: editor.activeStyleIndex === $index}">
|
<h5 class="section-heading">Options</h5>
|
||||||
<a ng-click="editor.activeStyleIndex = $index">{{style.pattern || 'New rule'}}</a>
|
<div class="gf-form-inline">
|
||||||
</li>
|
<div class="gf-form">
|
||||||
<li>
|
<label class="gf-form-label width-13">Apply to columns named</label>
|
||||||
<a class="pointer" ng-click="editor.addColumnStyle()">
|
<input type="text" placeholder="Name or regex" class="gf-form-input width-13" ng-model="style.pattern" bs-tooltip="'Specify regex using /my.*regex/ syntax'"
|
||||||
<i class="fa fa-plus"></i> Add
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div class="edit-tab-content" ng-repeat="style in editor.panel.styles" ng-if="editor.activeStyleIndex === $index">
|
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<h5 class="section-heading">Options</h5>
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-13">Apply to columns named</label>
|
|
||||||
<input type="text" placeholder="Name or regex" class="gf-form-input width-13" ng-model="style.pattern" bs-tooltip="'Specify regex using /my.*regex/ syntax'"
|
|
||||||
bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur
|
bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur
|
||||||
data-placement="right">
|
data-placement="right">
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form" ng-if="style.type !== 'hidden'">
|
|
||||||
<label class="gf-form-label width-13">Column Header</label>
|
|
||||||
<input type="text" class="gf-form-input width-13" ng-model="style.alias" ng-change="editor.render()" ng-model-onblur placeholder="Override header label">
|
|
||||||
</div>
|
|
||||||
<gf-form-switch class="gf-form" label-class="width-13" label="Render value as link" checked="style.link" change="editor.render()"></gf-form-switch>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form" ng-if="style.type !== 'hidden'">
|
||||||
|
<label class="gf-form-label width-13">Column Header</label>
|
||||||
|
<input type="text" class="gf-form-input width-13" ng-model="style.alias" ng-change="editor.render()" ng-model-onblur placeholder="Override header label">
|
||||||
|
</div>
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-13" label="Render value as link" checked="style.link" change="editor.render()"></gf-form-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Type</h5>
|
<h5 class="section-heading">Type</h5>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-11">Type</label>
|
<label class="gf-form-label width-11">Type</label>
|
||||||
<div class="gf-form-select-wrapper width-16">
|
<div class="gf-form-select-wrapper width-16">
|
||||||
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
|
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form" ng-if="style.type === 'date'">
|
</div>
|
||||||
<label class="gf-form-label width-11">Date Format</label>
|
<div class="gf-form" ng-if="style.type === 'date'">
|
||||||
<gf-form-dropdown model="style.dateFormat" css-class="gf-form-input width-16" lookup-text="true"
|
<label class="gf-form-label width-11">Date Format</label>
|
||||||
|
<gf-form-dropdown model="style.dateFormat" css-class="gf-form-input width-16" lookup-text="true"
|
||||||
get-options="editor.dateFormats" on-change="editor.render()" allow-custom="true">
|
get-options="editor.dateFormats" on-change="editor.render()" allow-custom="true">
|
||||||
</gf-form-dropdown>
|
</gf-form-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="style.type === 'string'">
|
<div ng-if="style.type === 'string'">
|
||||||
<gf-form-switch class="gf-form" label-class="width-11" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize"
|
<gf-form-switch class="gf-form" label-class="width-11" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize"
|
||||||
change="editor.render()"></gf-form-switch>
|
change="editor.render()"></gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="style.type === 'string'">
|
<div ng-if="style.type === 'string'">
|
||||||
<gf-form-switch class="gf-form" label-class="width-11" ng-if="style.type === 'string'" label="Preserve Formatting" checked="style.preserveFormat"
|
<gf-form-switch class="gf-form" label-class="width-11" ng-if="style.type === 'string'" label="Preserve Formatting" checked="style.preserveFormat"
|
||||||
change="editor.render()"></gf-form-switch>
|
change="editor.render()"></gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="style.type === 'number'">
|
<div ng-if="style.type === 'number'">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-11">Unit</label>
|
<label class="gf-form-label width-11">Unit</label>
|
||||||
<div class="gf-form-dropdown-typeahead width-16" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
|
<div class="gf-form-dropdown-typeahead width-16" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-11">Decimals</label>
|
<label class="gf-form-label width-11">Decimals</label>
|
||||||
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()"
|
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()"
|
||||||
ng-model-onblur>
|
ng-model-onblur>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group" ng-if="style.type === 'string'">
|
<div class="section gf-form-group" ng-if="style.type === 'string'">
|
||||||
<h5 class="section-heading">Value Mappings</h5>
|
<h5 class="section-heading">Value Mappings</h5>
|
||||||
<div class="editor-row">
|
<div class="editor-row">
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label">
|
<span class="gf-form-label">
|
||||||
Type
|
Type
|
||||||
</span>
|
</span>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select class="gf-form-input" ng-model="style.mappingType"
|
<select class="gf-form-input" ng-model="style.mappingType"
|
||||||
ng-options="c.value as c.text for c in editor.mappingTypes" ng-change="editor.render()"></select>
|
ng-options="c.value as c.text for c in editor.mappingTypes" ng-change="editor.render()"></select>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form-group" ng-if="style.mappingType==1">
|
|
||||||
<div class="gf-form" ng-repeat="map in style.valueMaps">
|
|
||||||
<span class="gf-form-label">
|
|
||||||
<i class="fa fa-remove pointer" ng-click="editor.removeValueMap(style, $index)"></i>
|
|
||||||
</span>
|
|
||||||
<input type="text" class="gf-form-input max-width-6" ng-model="map.value" placeholder="Value" ng-blur="editor.render()">
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<i class="fa fa-arrow-right"></i>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="gf-form-input max-width-8" ng-model="map.text" placeholder="Text" ng-blur="editor.render()">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<a class="pointer" ng-click="editor.addValueMap(style)"><i class="fa fa-plus"></i></a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form-group" ng-if="style.mappingType==2">
|
|
||||||
<div class="gf-form" ng-repeat="rangeMap in style.rangeMaps">
|
|
||||||
<span class="gf-form-label">
|
|
||||||
<i class="fa fa-remove pointer" ng-click="editor.removeRangeMap(style, $index)"></i>
|
|
||||||
</span>
|
|
||||||
<span class="gf-form-label">From</span>
|
|
||||||
<input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="editor.render()">
|
|
||||||
<span class="gf-form-label">To</span>
|
|
||||||
<input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="editor.render()">
|
|
||||||
<span class="gf-form-label">Text</span>
|
|
||||||
<input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="editor.render()">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<a class="pointer" ng-click="editor.addRangeMap(style)"><i class="fa fa-plus"></i></a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="gf-form-group" ng-if="style.mappingType==1">
|
||||||
</div>
|
<div class="gf-form" ng-repeat="map in style.valueMaps">
|
||||||
|
<span class="gf-form-label">
|
||||||
<div class="section gf-form-group" ng-if="['number', 'string'].indexOf(style.type) !== -1">
|
<i class="fa fa-remove pointer" ng-click="editor.removeValueMap(style, $index)"></i>
|
||||||
<h5 class="section-heading">Thresholds</h5>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Thresholds
|
|
||||||
<tip>Comma separated values</tip>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()"
|
|
||||||
array-join>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Color Mode</label>
|
|
||||||
<div class="gf-form-select-wrapper width-10">
|
|
||||||
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Colors</label>
|
|
||||||
<span class="gf-form-label">
|
|
||||||
<color-picker color="style.colors[0]" onChange="editor.onColorChange($index, 0)"></color-picker>
|
|
||||||
</span>
|
|
||||||
<span class="gf-form-label">
|
|
||||||
<color-picker color="style.colors[1]" onChange="editor.onColorChange($index, 1)"></color-picker>
|
|
||||||
</span>
|
|
||||||
<span class="gf-form-label">
|
|
||||||
<color-picker color="style.colors[2]" onChange="editor.onColorChange($index, 2)"></color-picker>
|
|
||||||
</span>
|
|
||||||
<div class="gf-form-label">
|
|
||||||
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section gf-form-group" ng-if="style.link">
|
|
||||||
<h5 class="section-heading">Link</h5>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-9">
|
|
||||||
Url
|
|
||||||
<info-popover mode="right-normal">
|
|
||||||
<p>Specify an URL (relative or absolute)</p>
|
|
||||||
<span>
|
|
||||||
Use special variables to specify cell values:
|
|
||||||
<br>
|
|
||||||
<em>${__cell}</em> refers to current cell value
|
|
||||||
<br>
|
|
||||||
<em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
|
||||||
<em>${__cell_1}</em> refers to second column's value.
|
|
||||||
<br>
|
|
||||||
<em>${__cell:raw}</em> syntax. By default values are URI encoded. If the value is a complete URL you can disable all encoding using
|
|
||||||
</span>
|
</span>
|
||||||
</info-popover>
|
<input type="text" class="gf-form-input max-width-6" ng-model="map.value" placeholder="Value" ng-blur="editor.render()">
|
||||||
</label>
|
<label class="gf-form-label">
|
||||||
<input type="text" class="gf-form-input width-29" ng-model="style.linkUrl" ng-blur="editor.render()" ng-model-onblur data-placement="right">
|
<i class="fa fa-arrow-right"></i>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="gf-form-input max-width-8" ng-model="map.text" placeholder="Text" ng-blur="editor.render()">
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="editor.addValueMap(style)"><i class="fa fa-plus"></i></a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form-group" ng-if="style.mappingType==2">
|
||||||
|
<div class="gf-form" ng-repeat="rangeMap in style.rangeMaps">
|
||||||
|
<span class="gf-form-label">
|
||||||
|
<i class="fa fa-remove pointer" ng-click="editor.removeRangeMap(style, $index)"></i>
|
||||||
|
</span>
|
||||||
|
<span class="gf-form-label">From</span>
|
||||||
|
<input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="editor.render()">
|
||||||
|
<span class="gf-form-label">To</span>
|
||||||
|
<input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="editor.render()">
|
||||||
|
<span class="gf-form-label">Text</span>
|
||||||
|
<input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="editor.render()">
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="editor.addRangeMap(style)"><i class="fa fa-plus"></i></a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
</div>
|
||||||
<label class="gf-form-label width-9">
|
</div>
|
||||||
Tooltip
|
|
||||||
<info-popover mode="right-normal">
|
<div class="section gf-form-group" ng-if="['number', 'string'].indexOf(style.type) !== -1">
|
||||||
<p>Specify text for link tooltip.</p>
|
<h5 class="section-heading">Thresholds</h5>
|
||||||
<span>
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Thresholds
|
||||||
|
<tip>Comma separated values</tip>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()"
|
||||||
|
array-join>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Color Mode</label>
|
||||||
|
<div class="gf-form-select-wrapper width-10">
|
||||||
|
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Colors</label>
|
||||||
|
<span class="gf-form-label">
|
||||||
|
<color-picker color="style.colors[0]" onChange="editor.onColorChange($index, 0)"></color-picker>
|
||||||
|
</span>
|
||||||
|
<span class="gf-form-label">
|
||||||
|
<color-picker color="style.colors[1]" onChange="editor.onColorChange($index, 1)"></color-picker>
|
||||||
|
</span>
|
||||||
|
<span class="gf-form-label">
|
||||||
|
<color-picker color="style.colors[2]" onChange="editor.onColorChange($index, 2)"></color-picker>
|
||||||
|
</span>
|
||||||
|
<div class="gf-form-label">
|
||||||
|
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section gf-form-group" ng-if="style.link">
|
||||||
|
<h5 class="section-heading">Link</h5>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">
|
||||||
|
Url
|
||||||
|
<info-popover mode="right-normal">
|
||||||
|
<p>Specify an URL (relative or absolute)</p>
|
||||||
|
<span>
|
||||||
|
Use special variables to specify cell values:
|
||||||
|
<br>
|
||||||
|
<em>${__cell}</em> refers to current cell value
|
||||||
|
<br>
|
||||||
|
<em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
||||||
|
<em>${__cell_1}</em> refers to second column's value.
|
||||||
|
<br>
|
||||||
|
<em>${__cell:raw}</em> syntax. By default values are URI encoded. If the value is a complete URL you can disable all encoding using
|
||||||
|
</span>
|
||||||
|
</info-popover>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="gf-form-input width-29" ng-model="style.linkUrl" ng-blur="editor.render()" ng-model-onblur data-placement="right">
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">
|
||||||
|
Tooltip
|
||||||
|
<info-popover mode="right-normal">
|
||||||
|
<p>Specify text for link tooltip.</p>
|
||||||
|
<span>
|
||||||
This title appears when user hovers pointer over the cell with link. Use the same variables as for URL.
|
This title appears when user hovers pointer over the cell with link. Use the same variables as for URL.
|
||||||
</span>
|
</span>
|
||||||
</info-popover></label>
|
</info-popover>
|
||||||
<input type="text" class="gf-form-input width-29" ng-model="style.linkTooltip" ng-blur="editor.render()" ng-model-onblur
|
</label>
|
||||||
|
<input type="text" class="gf-form-input width-29" ng-model="style.linkTooltip" ng-blur="editor.render()" ng-model-onblur
|
||||||
data-placement="right">
|
data-placement="right">
|
||||||
</div>
|
|
||||||
<gf-form-switch class="gf-form" label-class="width-9" label="Open in new tab" checked="style.linkTargetBlank"></gf-form-switch>
|
|
||||||
</div>
|
</div>
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-9" label="Open in new tab" checked="style.linkTargetBlank"></gf-form-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
<div class="gf-form-group">
|
||||||
<button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)">
|
<button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)">
|
||||||
<i class="fa fa-trash"></i> Remove Rule
|
<i class="fa fa-trash"></i> Remove Rule
|
||||||
</button>
|
</button>
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form-button-row">
|
||||||
|
<button class="btn btn-inverse" ng-click="editor.addColumnStyle()">
|
||||||
|
<i class="fa fa-plus"></i> Add column style
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
DataQueryOptions,
|
DataQueryOptions,
|
||||||
IntervalValues,
|
IntervalValues,
|
||||||
} from './series';
|
} from './series';
|
||||||
import { PanelProps, PanelOptionsProps, Threshold } from './panel';
|
import { BasicGaugeColor, MappingType, PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel';
|
||||||
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
|
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
|
||||||
import { Organization, OrganizationState } from './organization';
|
import { Organization, OrganizationState } from './organization';
|
||||||
import {
|
import {
|
||||||
@ -93,7 +93,11 @@ export {
|
|||||||
Threshold,
|
Threshold,
|
||||||
ValidationEvents,
|
ValidationEvents,
|
||||||
ValidationRule,
|
ValidationRule,
|
||||||
|
ValueMap,
|
||||||
|
RangeMap,
|
||||||
IntervalValues,
|
IntervalValues,
|
||||||
|
MappingType,
|
||||||
|
BasicGaugeColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
|
@ -36,3 +36,30 @@ export interface Threshold {
|
|||||||
color?: string;
|
color?: string;
|
||||||
canRemove: boolean;
|
canRemove: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MappingType {
|
||||||
|
ValueToText = 1,
|
||||||
|
RangeToText = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BasicGaugeColor {
|
||||||
|
Green = 'rgba(50, 172, 45, 0.97)',
|
||||||
|
Orange = 'rgba(237, 129, 40, 0.89)',
|
||||||
|
Red = 'rgb(212, 74, 58)',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseMap {
|
||||||
|
id: number;
|
||||||
|
operator: string;
|
||||||
|
text: string;
|
||||||
|
type: MappingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValueMap extends BaseMap {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RangeMap extends BaseMap {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
@ -1,38 +1,42 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { Threshold, TimeSeriesVMs } from 'app/types';
|
import { MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
|
||||||
import config from '../core/config';
|
import config from '../core/config';
|
||||||
import kbn from '../core/utils/kbn';
|
import kbn from '../core/utils/kbn';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
decimals: number;
|
decimals: number;
|
||||||
|
height: number;
|
||||||
|
mappings: Array<RangeMap | ValueMap>;
|
||||||
|
maxValue: number;
|
||||||
|
minValue: number;
|
||||||
|
prefix: string;
|
||||||
timeSeries: TimeSeriesVMs;
|
timeSeries: TimeSeriesVMs;
|
||||||
showThresholdMarkers: boolean;
|
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
|
showThresholdMarkers: boolean;
|
||||||
showThresholdLabels: boolean;
|
showThresholdLabels: boolean;
|
||||||
|
stat: string;
|
||||||
|
suffix: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
|
||||||
stat: string;
|
|
||||||
prefix: string;
|
|
||||||
suffix: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Gauge extends PureComponent<Props> {
|
export class Gauge extends PureComponent<Props> {
|
||||||
canvasElement: any;
|
canvasElement: any;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
minValue: 0,
|
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
|
mappings: [],
|
||||||
|
minValue: 0,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
suffix: '',
|
suffix: '',
|
||||||
unit: 'none',
|
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
|
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
|
||||||
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
|
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
|
||||||
],
|
],
|
||||||
|
unit: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -43,16 +47,49 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatWithMappings(mappings, value) {
|
||||||
|
const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
|
||||||
|
const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
|
||||||
|
|
||||||
|
const valueMap = valueMaps.map(mapping => {
|
||||||
|
if (mapping.value && value === mapping.value) {
|
||||||
|
return mapping.text;
|
||||||
|
}
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const rangeMap = rangeMaps.map(mapping => {
|
||||||
|
if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
|
||||||
|
return mapping.text;
|
||||||
|
}
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
rangeMap,
|
||||||
|
valueMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
formatValue(value) {
|
formatValue(value) {
|
||||||
const { decimals, prefix, suffix, unit } = this.props;
|
const { decimals, mappings, prefix, suffix, unit } = this.props;
|
||||||
|
|
||||||
const formatFunc = kbn.valueFormats[unit];
|
const formatFunc = kbn.valueFormats[unit];
|
||||||
|
const formattedValue = formatFunc(value, decimals);
|
||||||
|
|
||||||
|
if (mappings.length > 0) {
|
||||||
|
const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
|
||||||
|
|
||||||
|
if (valueMap) {
|
||||||
|
return valueMap;
|
||||||
|
} else if (rangeMap) {
|
||||||
|
return rangeMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${prefix} ${formatFunc(value, decimals)} ${suffix}`;
|
return `${prefix} ${formattedValue} ${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
|
@ -96,8 +96,6 @@
|
|||||||
@import 'components/empty_list_cta';
|
@import 'components/empty_list_cta';
|
||||||
@import 'components/popper';
|
@import 'components/popper';
|
||||||
@import 'components/form_select_box';
|
@import 'components/form_select_box';
|
||||||
@import 'components/user-picker';
|
|
||||||
@import 'components/description-picker';
|
|
||||||
@import 'components/panel_editor';
|
@import 'components/panel_editor';
|
||||||
@import 'components/toolbar';
|
@import 'components/toolbar';
|
||||||
@import 'components/delete_button';
|
@import 'components/delete_button';
|
||||||
@ -106,6 +104,7 @@
|
|||||||
@import 'components/unit-picker';
|
@import 'components/unit-picker';
|
||||||
@import 'components/thresholds';
|
@import 'components/thresholds';
|
||||||
@import 'components/toggle_button_group';
|
@import 'components/toggle_button_group';
|
||||||
|
@import 'components/value-mappings';
|
||||||
|
|
||||||
// PAGES
|
// PAGES
|
||||||
@import 'pages/login';
|
@import 'pages/login';
|
||||||
|
@ -271,7 +271,7 @@ $menu-dropdown-shadow: 5px 5px 20px -5px $black;
|
|||||||
$tab-border-color: $dark-4;
|
$tab-border-color: $dark-4;
|
||||||
|
|
||||||
// Toolbar
|
// Toolbar
|
||||||
$toolbar-bg: $black;
|
$toolbar-bg: $input-black;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@ -375,13 +375,14 @@ $checkbox-color: $dark-1;
|
|||||||
//Panel Edit
|
//Panel Edit
|
||||||
// -------------------------
|
// -------------------------
|
||||||
$panel-editor-shadow: 0 0 20px black;
|
$panel-editor-shadow: 0 0 20px black;
|
||||||
|
$panel-editor-border: 1px solid $dark-3;
|
||||||
$panel-editor-side-menu-shadow: drop-shadow(0 0 10px $black);
|
$panel-editor-side-menu-shadow: drop-shadow(0 0 10px $black);
|
||||||
$panel-editor-toolbar-view-bg: $black;
|
$panel-editor-toolbar-view-bg: $input-black;
|
||||||
$panel-editor-viz-item-shadow: 0 0 8px $dark-5;
|
$panel-editor-viz-item-shadow: 0 0 8px $dark-5;
|
||||||
$panel-editor-viz-item-border: 1px solid $dark-5;
|
$panel-editor-viz-item-border: 1px solid $dark-5;
|
||||||
$panel-editor-viz-item-shadow-hover: 0 0 4px $blue;
|
$panel-editor-viz-item-shadow-hover: 0 0 4px $blue;
|
||||||
$panel-editor-viz-item-border-hover: 1px solid $blue;
|
$panel-editor-viz-item-border-hover: 1px solid $blue;
|
||||||
$panel-editor-viz-item-bg: $black;
|
$panel-editor-viz-item-bg: $input-black;
|
||||||
$panel-editor-tabs-line-color: #e3e3e3;
|
$panel-editor-tabs-line-color: #e3e3e3;
|
||||||
$panel-editor-viz-item-bg-hover: darken($blue, 47%);
|
$panel-editor-viz-item-bg-hover: darken($blue, 47%);
|
||||||
$panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
|
$panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
|
||||||
|
@ -384,6 +384,7 @@ $checkbox-color: $gray-7;
|
|||||||
//Panel Edit
|
//Panel Edit
|
||||||
// -------------------------
|
// -------------------------
|
||||||
$panel-editor-shadow: 2px 2px 8px $gray-3;
|
$panel-editor-shadow: 2px 2px 8px $gray-3;
|
||||||
|
$panel-editor-border: 1px solid $dark-4;
|
||||||
$panel-editor-side-menu-shadow: drop-shadow(0 0 2px $gray-3);
|
$panel-editor-side-menu-shadow: drop-shadow(0 0 2px $gray-3);
|
||||||
$panel-editor-toolbar-view-bg: $white;
|
$panel-editor-toolbar-view-bg: $white;
|
||||||
$panel-editor-viz-item-shadow: 0 0 4px $gray-3;
|
$panel-editor-viz-item-shadow: 0 0 4px $gray-3;
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
||||||
color: $btn-link-color;
|
color: $btn-link-color;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the backgrounds
|
// Set the backgrounds
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
.description-picker-option__button {
|
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
border-radius: 0;
|
|
||||||
white-space: normal;
|
|
||||||
i.fa-check {
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -47,12 +47,17 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
|
|
||||||
.gf-form-select-box__input {
|
.gf-form-select-box__input {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
input {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form-select-box__menu {
|
.gf-form-select-box__menu {
|
||||||
background: $dropdownBackground;
|
background: $input-bg;
|
||||||
|
box-shadow: $menu-dropdown-shadow;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form-select-box__menu-list {
|
.gf-form-select-box__menu-list {
|
||||||
@ -64,16 +69,20 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .gf-form-select-box__single-value { */
|
||||||
|
/* } */
|
||||||
|
|
||||||
.gf-form-select-box__multi-value {
|
.gf-form-select-box__multi-value {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form-select-box__option {
|
.gf-form-select-box__option {
|
||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.gf-form-select-box__option--is-focused {
|
&.gf-form-select-box__option--is-focused {
|
||||||
color: $dropdownLinkColorHover;
|
color: $dropdownLinkColorHover;
|
||||||
background-color: $dropdownLinkBackgroundHover;
|
background: $menu-dropdown-hover-bg;
|
||||||
@include left-brand-border-gradient();
|
@include left-brand-border-gradient();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +99,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
|
|
||||||
.gf-form-select-box__value-container {
|
.gf-form-select-box__value-container {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding: 8px 10px;
|
padding: 6px 10px;
|
||||||
> div {
|
> div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@ -119,10 +128,12 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
border-width: 0 5px 5px;
|
border-width: 0 5px 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form-input--form-dropdown {
|
.gf-form-input--form-dropdown {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form--has-input-icon {
|
.gf-form--has-input-icon {
|
||||||
@ -130,3 +141,32 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gf-form-select-box__desc-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
justify-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 7px 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gf-form-select-box__desc-option__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-right: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gf-form-select-box__desc-option__desc {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gf-form-select-box__desc-option__img {
|
||||||
|
width: 16px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
@ -110,7 +110,6 @@ $input-border: 1px solid $input-border-color;
|
|||||||
|
|
||||||
&--grow {
|
&--grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 2.6rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
|
@ -21,10 +21,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
&.json-formatter-object::after {
|
&.json-formatter-object::after {
|
||||||
content: "No properties";
|
content: 'No properties';
|
||||||
}
|
}
|
||||||
&.json-formatter-array::after {
|
&.json-formatter-array::after {
|
||||||
content: "[]";
|
content: '[]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +33,9 @@
|
|||||||
color: $json-explorer-string-color;
|
color: $json-explorer-string-color;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-formatter-number {
|
.json-formatter-number {
|
||||||
color: $json-explorer-number-color;
|
color: $json-explorer-number-color;
|
||||||
}
|
}
|
||||||
@ -87,7 +89,7 @@
|
|||||||
&::after {
|
&::after {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: transform $json-explorer-rotate-time ease-in;
|
transition: transform $json-explorer-rotate-time ease-in;
|
||||||
content: "►";
|
content: '►';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background: $page-bg;
|
background: $page-bg;
|
||||||
margin: 0 20px 0 84px;
|
margin: 0 20px 0 84px;
|
||||||
border-left: 2px solid $orange;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: $panel-editor-shadow;
|
box-shadow: $panel-editor-shadow;
|
||||||
}
|
}
|
||||||
@ -133,14 +132,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.viz-picker {
|
.viz-picker {
|
||||||
|
margin-top: -40px;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viz-picker-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.viz-picker__item {
|
.viz-picker__item {
|
||||||
background: $panel-editor-viz-item-bg;
|
background: $panel-bg;
|
||||||
border: $panel-editor-viz-item-border;
|
border: $panel-border;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
@ -162,7 +166,7 @@
|
|||||||
border: 1px solid $orange;
|
border: 1px solid $orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
&:hover {
|
||||||
box-shadow: $panel-editor-viz-item-shadow-hover;
|
box-shadow: $panel-editor-viz-item-shadow-hover;
|
||||||
background: $panel-editor-viz-item-bg-hover;
|
background: $panel-editor-viz-item-bg-hover;
|
||||||
border: $panel-editor-viz-item-border-hover;
|
border: $panel-editor-viz-item-border-hover;
|
||||||
@ -273,6 +277,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ds-picker {
|
||||||
|
position: relative;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-picker-menu {
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 500px;
|
||||||
|
position: absolute;
|
||||||
|
background: $panel-editor-toolbar-view-bg;
|
||||||
|
padding: 5px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.ds-picker-list__name {
|
.ds-picker-list__name {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -306,6 +324,13 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background: $input-label-bg;
|
background: $input-label-bg;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section__body {
|
.form-section__body {
|
||||||
|
@ -77,7 +77,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
height: 37px;
|
||||||
|
width: 37px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
.user-picker-option__button {
|
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.user-picker-option__avatar {
|
|
||||||
width: 20px;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
37
public/sass/components/_value-mappings.scss
Normal file
37
public/sass/components/_value-mappings.scss
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
.mapping-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-row-type {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-row-input {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-mapping-row {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 37px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-mapping-row-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
background-color: $green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-mapping-row-label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: $input-label-bg;
|
||||||
|
width: calc(100% - 36px);
|
||||||
|
}
|
@ -28,6 +28,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-styles-heading {
|
||||||
|
border-bottom: 1px solid $gray-1;
|
||||||
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
.edit-tab-with-sidemenu {
|
.edit-tab-with-sidemenu {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -83,6 +83,10 @@ button.close {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.center-vh {
|
.center-vh {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
Loading…
Reference in New Issue
Block a user