Commit c78d5fb2 by Peter Holmberg

Merge remote-tracking branch 'origin/develop' into 14409/threshold-ux-changes

parents 8615de81 1d325cf6
......@@ -8,15 +8,16 @@ interface ExtendedOptionProps extends OptionProps<any> {
}
export const Option = (props: ExtendedOptionProps) => {
const { children, isSelected, data, className } = props;
const { children, isSelected, data } = props;
return (
<components.Option {...props}>
<div className={`description-picker-option__button btn btn-link ${className}`}>
{isSelected && <i className="fa fa-check pull-right" aria-hidden="true" />}
<div className="gf-form">{children}</div>
<div className="gf-form">
<div className="muted width-17">{data.description}</div>
<div className="gf-form-select-box__desc-option">
<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>
</components.Option>
);
......
import React from 'react';
import React from 'react';
import renderer from 'react-test-renderer';
import PickerOption from './PickerOption';
......@@ -24,7 +24,7 @@ const model = {
children: 'Model title',
data: {
title: 'Model title',
avatarUrl: 'url/to/avatar',
imgUrl: 'url/to/avatar',
label: 'User picker label',
},
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
interface ExtendedOptionProps extends OptionProps<any> {
data: any;
data: {
description?: string;
imgUrl?: string;
};
}
export const PickerOption = (props: ExtendedOptionProps) => {
const { children, data, className } = props;
export const Option = (props: ExtendedOptionProps) => {
const { children, isSelected, data } = props;
return (
<components.Option {...props}>
<div className={`description-picker-option__button btn btn-link ${className}`}>
{data.avatarUrl && <img src={data.avatarUrl} alt={data.label} className="user-picker-option__avatar" />}
{children}
<div className="gf-form-select-box__desc-option">
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
<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>
</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;
// 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;
import React, { SFC } from 'react';
import Select from 'react-select';
import DescriptionOption from './DescriptionOption';
import IndicatorsContainer from './IndicatorsContainer';
import ResetStyles from './ResetStyles';
interface Props {
......@@ -11,7 +12,7 @@ interface Props {
onSelected: (item: any) => {} | void;
options: any[];
placeholder?: string;
width: number;
width?: number;
value: any;
}
......@@ -28,10 +29,11 @@ const SimplePicker: SFC<Props> = ({
}) => {
return (
<Select
classNamePrefix={`gf-form-select-box`}
className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
classNamePrefix="gf-form-select-box"
className={`${width ? 'width-' + width : ''} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
components={{
Option: DescriptionOption,
IndicatorsContainer,
}}
defaultValue={defaultValue}
value={value}
......
......@@ -47,7 +47,7 @@ export class TeamPicker extends Component<Props, State> {
id: team.id,
label: team.name,
name: team.name,
avatarUrl: team.avatarUrl,
imgUrl: team.avatarUrl,
};
});
......
......@@ -63,7 +63,7 @@ export default class UnitPicker extends PureComponent<Props> {
return (
<Select
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}
isSearchable={true}
menuShouldScrollIntoView={false}
......
......@@ -41,7 +41,7 @@ export class UserPicker extends Component<Props, State> {
return result.map(user => ({
id: user.userId,
label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl,
imgUrl: user.avatarUrl,
login: user.login,
}));
})
......
......@@ -3,15 +3,19 @@
exports[`PickerOption renders correctly 1`] = `
<div>
<div
className="description-picker-option__button btn btn-link class-for-user-picker"
className="gf-form-select-box__desc-option"
>
<img
alt="User picker label"
className="user-picker-option__avatar"
className="gf-form-select-box__desc-option__img"
src="url/to/avatar"
/>
Model title
<div
className="gf-form-select-box__desc-option__body"
>
<div>
Model title
</div>
</div>
</div>
</div>
`;
\ No newline at end of file
......@@ -141,9 +141,10 @@ export class DashboardMigrator {
// ensure query refIds
panelUpgrades.push(panel => {
console.log('asdasd', panel);
_.each(panel.targets, target => {
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();
}
getNextQueryLetter(panel) {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, refId => {
return _.every(panel.targets, other => {
return other.refId !== refId;
});
});
}
isTimezoneUtc() {
return this.getTimezone() === 'utc';
}
......
// Libraries
import React, { PureComponent } from 'react';
import classNames from 'classnames';
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';
export interface Props {
onChangeDataSource: (ds: DataSourceSelectItem) => void;
datasources: DataSourceSelectItem[];
current: DataSourceSelectItem;
onBlur?: () => void;
autoFocus?: boolean;
}
interface State {
searchQuery: string;
}
export class DataSourcePicker extends PureComponent<Props> {
static defaultProps = {
autoFocus: false,
};
export class DataSourcePicker extends PureComponent<Props, State> {
searchInput: HTMLElement;
constructor(props) {
super(props);
this.state = {
searchQuery: '',
};
}
getDataSources() {
const { searchQuery } = this.state;
const regex = new RegExp(searchQuery, 'i');
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>
);
onChange = item => {
const ds = this.props.datasources.find(ds => ds.name === item.value);
this.props.onChangeDataSource(ds);
};
componentDidMount() {
setTimeout(() => {
this.searchInput.focus();
}, 300);
}
render() {
const { datasources, current, autoFocus, onBlur } = this.props;
onSearchQueryChange = evt => {
const value = evt.target.value;
this.setState(prevState => ({
...prevState,
searchQuery: value,
const options = datasources.map(ds => ({
value: ds.name,
label: ds.name,
imgUrl: ds.meta.info.logos.small,
}));
};
renderFilters({ onKeyDown, selected }: KeyboardNavigationProps) {
const { searchQuery } = this.state;
const value = current && {
label: current.name,
value: current.name,
imgUrl: current.meta.info.logos.small,
};
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);
});
<div className="gf-form-inline">
<Select
classNamePrefix={`gf-form-select-box`}
isMulti={false}
menuShouldScrollIntoView={false}
isClearable={false}
className="gf-form-input gf-form-input--form-dropdown ds-picker"
onChange={item => this.onChange(item)}
options={options}
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,
}}
/>
<i className="gf-form-input-icon fa fa-search" />
</label>
);
}
render() {
return (
<KeyboardNavigation
render={(keyNavProps: KeyboardNavigationProps) => (
<>
<div className="cta-form__bar">
{this.renderFilters(keyNavProps)}
<div className="gf-form--grow" />
</div>
<div className="ds-picker-list">
{this.getDataSources().map((ds, index) => this.renderDataSource(ds, index, keyNavProps))}
</div>
</>
)}
/>
</div>
);
}
}
......
......@@ -5,8 +5,8 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn';
interface Props {
children: JSX.Element;
heading: string;
main?: EditorToolBarView;
toolbarItems: EditorToolBarView[];
renderToolbar?: () => JSX.Element;
toolbarItems?: EditorToolBarView[];
}
export interface EditorToolBarView {
......@@ -15,7 +15,7 @@ export interface EditorToolBarView {
icon?: string;
disabled?: boolean;
onClick?: () => void;
render: (closeFunction: any) => JSX.Element | JSX.Element[];
render: (closeFunction?: any) => JSX.Element | JSX.Element[];
}
interface State {
......@@ -25,6 +25,10 @@ interface State {
}
export class EditorTabBody extends PureComponent<Props, State> {
static defaultProps = {
toolbarItems: [],
};
constructor(props) {
super(props);
......@@ -65,16 +69,6 @@ export class EditorTabBody extends PureComponent<Props, 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) {
const onClick = () => {
if (view.onClick) {
......@@ -104,16 +98,20 @@ export class EditorTabBody extends PureComponent<Props, State> {
}
render() {
const { children, toolbarItems, main, heading } = this.props;
const { children, renderToolbar, heading, toolbarItems } = this.props;
const { openView, fadeIn, isOpen } = this.state;
return (
<>
<div className="toolbar">
<div className="toolbar__heading">{heading}</div>
{main && this.renderMainSelection(main)}
<div className="gf-form--grow" />
{toolbarItems.map(item => this.renderButton(item))}
{renderToolbar && renderToolbar()}
{toolbarItems.length > 0 && (
<>
<div className="gf-form--grow" />
{toolbarItems.map(item => this.renderButton(item))}
</>
)}
</div>
<div className="panel-editor__scroll">
<CustomScrollbar autoHide={false}>
......
......@@ -7,6 +7,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
// Components
import { EditorTabBody } from './EditorTabBody';
import { VizTypePicker } from './VizTypePicker';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
// Types
import { PanelModel } from '../panel_model';
......@@ -21,9 +22,24 @@ interface Props {
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;
angularOptions: AngularComponent;
searchInput: HTMLElement;
constructor(props) {
super(props);
this.state = {
isVizPickerOpen: false,
searchQuery: '',
};
}
getPanelDefaultOptions = () => {
const { panel, plugin } = this.props;
......@@ -87,10 +103,11 @@ export class VisualizationTab extends PureComponent<Props> {
let template = '';
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
template += `
<div class="form-section" ng-cloak>
<div class="form-section__header">{{ctrl.editorTabs[${i}].title}}</div>
<div class="form-section__body">
template +=
`
<div class="form-section" ng-cloak>` +
(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>
</div>
</div>
......@@ -119,28 +136,81 @@ export class VisualizationTab extends PureComponent<Props> {
this.forceUpdate();
};
render() {
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>
);
}
};
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} />;
},
};
onTypeChanged = (plugin: PanelPlugin) => {
if (plugin.id === this.props.plugin.id) {
this.setState({ isVizPickerOpen: false });
} else {
this.props.onTypeChanged(plugin);
}
};
const panelHelp = {
title: '',
icon: 'fa fa-question',
render: () => <h2>Help</h2>,
};
render() {
const { plugin } = this.props;
const { isVizPickerOpen, searchQuery } = this.state;
return (
<EditorTabBody heading="Visualization" main={panelSelection} toolbarItems={[panelHelp]}>
{this.renderPanelOptions()}
<EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar}>
<>
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
<VizTypePicker
current={plugin}
onTypeChanged={this.onTypeChanged}
searchQuery={searchQuery}
onClose={this.onCloseVizPicker}
/>
</FadeIn>
{this.renderPanelOptions()}
</>
</EditorTabBody>
);
}
......
......@@ -4,27 +4,20 @@ import _ from 'lodash';
import config from 'app/core/config';
import { PanelPlugin } from 'app/types/plugins';
import VizTypePickerPlugin from './VizTypePickerPlugin';
import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation';
export interface Props {
current: PanelPlugin;
onTypeChanged: (newType: PanelPlugin) => void;
}
interface State {
searchQuery: string;
onClose: () => void;
}
export class VizTypePicker extends PureComponent<Props, State> {
export class VizTypePicker extends PureComponent<Props> {
searchInput: HTMLElement;
pluginList = this.getPanelPlugins('');
constructor(props) {
super(props);
this.state = {
searchQuery: '',
};
}
get maxSelectedIndex() {
......@@ -32,12 +25,6 @@ export class VizTypePicker extends PureComponent<Props, State> {
return filteredPluginList.length - 1;
}
componentDidMount() {
setTimeout(() => {
this.searchInput.focus();
}, 300);
}
getPanelPlugins(filter): PanelPlugin[] {
const panels = _.chain(config.panels)
.filter({ hideFromList: false })
......@@ -48,27 +35,22 @@ export class VizTypePicker extends PureComponent<Props, State> {
return _.sortBy(panels, 'sort');
}
renderVizPlugin = (plugin: PanelPlugin, index: number, keyNavProps: KeyboardNavigationProps) => {
renderVizPlugin = (plugin: PanelPlugin, index: number) => {
const { onTypeChanged } = this.props;
const { selected, onMouseEnter } = keyNavProps;
const isSelected = selected === index;
const isCurrent = plugin.id === this.props.current.id;
return (
<VizTypePickerPlugin
key={plugin.id}
isSelected={isSelected}
isCurrent={isCurrent}
plugin={plugin}
onMouseEnter={() => {
onMouseEnter(index);
}}
onClick={() => onTypeChanged(plugin)}
/>
);
};
getFilteredPluginList = (): PanelPlugin[] => {
const { searchQuery } = this.state;
const { searchQuery } = this.props;
const regex = new RegExp(searchQuery, 'i');
const pluginList = this.pluginList;
......@@ -79,57 +61,15 @@ export class VizTypePicker extends PureComponent<Props, State> {
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() {
const filteredPluginList = this.getFilteredPluginList();
return (
<KeyboardNavigation
render={(keyNavProps: KeyboardNavigationProps) => (
<>
<div className="cta-form__bar">
{this.renderFilters(keyNavProps)}
<div className="gf-form--grow" />
</div>
<div className="viz-picker">
{filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index, keyNavProps))}
</div>
</>
)}
/>
<div className="viz-picker">
<div className="viz-picker-list">
{filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index))}
</div>
</div>
);
}
}
import React from 'react';
import React from 'react';
import classNames from 'classnames';
import { PanelPlugin } from 'app/types/plugins';
interface Props {
isSelected: boolean;
isCurrent: boolean;
plugin: PanelPlugin;
onClick: () => void;
onMouseEnter: () => void;
}
const VizTypePickerPlugin = React.memo(
({ isSelected, isCurrent, plugin, onClick, onMouseEnter }: Props) => {
({ isCurrent, plugin, onClick }: Props) => {
const cssClass = classNames({
'viz-picker__item': true,
'viz-picker__item--selected': isSelected,
'viz-picker__item--current': isCurrent,
});
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>
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
</div>
);
},
(prevProps, nextProps) => {
if (prevProps.isSelected === nextProps.isSelected && prevProps.isCurrent === nextProps.isCurrent) {
if (prevProps.isCurrent === nextProps.isCurrent) {
return true;
}
return false;
......
import { Emitter } from 'app/core/utils/emitter';
import _ from 'lodash';
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
import { DataQuery } from 'app/types';
export interface GridPos {
x: number;
......@@ -237,6 +238,24 @@ export class PanelModel {
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() {
this.events.emit('panel-teardown');
this.events.removeAllListeners();
......
// Libraries
import _ from 'lodash';
import Remarkable from 'remarkable';
// Services & utils
import coreModule from 'app/core/core_module';
import config from 'app/core/config';
import { Emitter } from 'app/core/utils/emitter';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
import { PanelModel } from '../dashboard/panel_model';
import { DataQuery } from 'app/types';
export class MetricsTabCtrl {
dsName: string;
panel: any;
panelCtrl: any;
datasources: any[];
datasourceInstance: any;
nextRefId: string;
export interface AngularQueryComponentScope {
panel: PanelModel;
dashboard: DashboardModel;
panelDsValue: any;
addQueryDropdown: any;
queryTroubleshooterOpen: boolean;
helpOpen: boolean;
optionsOpen: boolean;
hasQueryHelp: boolean;
helpHtml: string;
queryOptions: any;
events: Emitter;
/** @ngInject */
constructor($scope, private $sce, datasourceSrv, private backendSrv) {
this.panelCtrl = $scope.ctrl;
$scope.ctrl = this;
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);
}
refresh: () => void;
render: () => void;
removeQuery: (query: DataQuery) => void;
addQuery: (query?: DataQuery) => void;
moveQuery: (query: DataQuery, direction: number) => void;
}
/** @ngInject */
......@@ -185,7 +28,6 @@ export function metricsTabDirective() {
restrict: 'E',
scope: true,
templateUrl: 'public/app/features/panel/partials/metrics_tab.html',
controller: MetricsTabCtrl,
};
}
......
<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}">
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
<plugin-component type="query-ctrl">
......@@ -7,21 +5,20 @@
</rebuild-on-change>
</div>
<div class="gf-form-query">
<div class="gf-form gf-form-query-letter-cell">
<label class="gf-form-label">
<span class="gf-form-query-letter-cell-carret">
<i class="fa fa-caret-down"></i>
</span>
<span class="gf-form-query-letter-cell-letter">{{ctrl.nextRefId}}</span>
</label>
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.datasourceInstance.meta.mixed">
Add Query
</button>
<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>
</div>
</div>
</div>
</div>
<!-- <div class="gf&#45;form&#45;query"> -->
<!-- <div class="gf&#45;form gf&#45;form&#45;query&#45;letter&#45;cell"> -->
<!-- <label class="gf&#45;form&#45;label"> -->
<!-- <span class="gf&#45;form&#45;query&#45;letter&#45;cell&#45;carret"> -->
<!-- <i class="fa fa&#45;caret&#45;down"></i> -->
<!-- </span> -->
<!-- <span class="gf&#45;form&#45;query&#45;letter&#45;cell&#45;letter">{{ctrl.nextRefId}}</span> -->
<!-- </label> -->
<!-- <button class="btn btn&#45;secondary gf&#45;form&#45;btn" ng&#45;click="ctrl.addQuery()" ng&#45;hide="ctrl.datasourceInstance.meta.mixed"> -->
<!-- Add Query -->
<!-- </button> -->
<!-- <div class="dropdown" ng&#45;if="ctrl.datasourceInstance.meta.mixed"> -->
<!-- <gf&#45;form&#45;dropdown model="ctrl.addQueryDropdown" get&#45;options="ctrl.getOptions(false)" on&#45;change="ctrl.addMixedQuery($option)"> -->
<!-- </gf&#45;form&#45;dropdown> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
......@@ -20,7 +20,7 @@ export class QueryRowCtrl {
this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions;
if (!this.target.refId) {
this.target.refId = this.panelCtrl.dashboard.getNextQueryLetter(this.panel);
this.target.refId = this.panel.getNextQueryLetter();
}
this.toggleCollapse(true);
......
......@@ -8,7 +8,7 @@
</div>
<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>
<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()">
......@@ -58,4 +58,4 @@
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</query-editor-row>
\ No newline at end of file
</query-editor-row>
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>
);
}
}
import React from 'react';
import { shallow } from 'enzyme';
import Thresholds, { BasicGaugeColor } from './Thresholds';
import { OptionsProps } from './module';
import Thresholds from './Thresholds';
import { defaultProps, OptionsProps } from './module';
import { PanelOptionsProps } from '../../../types';
const setup = (propOverrides?: object) => {
const props: PanelOptionsProps<OptionsProps> = {
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);
......@@ -15,12 +21,6 @@ const setup = (propOverrides?: object) => {
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', () => {
it('should add threshold between min and max', () => {
const instance = setup();
......@@ -28,24 +28,31 @@ describe('Add threshold', () => {
instance.onAddThreshold(1);
expect(instance.state.thresholds).toEqual([
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(131, 123, 52, 0.99)' },
{ index: 2, label: 'Max', value: 100, canRemove: false, color: BasicGaugeColor.Red },
{ 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 },
]);
});
it('should add threshold between min and added threshold', () => {
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);
expect(instance.state.thresholds).toEqual([
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
{ index: 1, label: '', value: 25, canRemove: true, color: 'rgba(144, 151, 43, 0.93)' },
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
{ 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: 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 { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
import { OptionModuleProps } from './module';
import { Threshold } from 'app/types';
import { BasicGaugeColor, Threshold } from 'app/types';
interface State {
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> {
constructor(props) {
super(props);
this.state = {
thresholds: this.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 },
],
thresholds: props.options.thresholds,
};
}
......
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);
});
});
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>
);
}
}
// 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>
`;
import React, { PureComponent } from 'react';
import Gauge from 'app/viz/Gauge';
import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types';
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
import ValueOptions from './ValueOptions';
import GaugeOptions from './GaugeOptions';
import Thresholds from './Thresholds';
import ValueMappings from './ValueMappings';
import {
BasicGaugeColor,
NullValueMode,
PanelOptionsProps,
PanelProps,
RangeMap,
Threshold,
ValueMap,
} from 'app/types';
export interface OptionsProps {
decimals: number;
......@@ -15,6 +24,7 @@ export interface OptionsProps {
suffix: string;
unit: string;
thresholds: Threshold[];
mappings: Array<RangeMap | ValueMap>;
}
export interface OptionModuleProps {
......@@ -30,6 +40,14 @@ export const defaultProps = {
showThresholdMarkers: true,
showThresholdLabels: false,
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;
render() {
const { onChange, options } = this.props;
return (
<div>
<ValueOptions onChange={this.props.onChange} options={this.props.options} />
<GaugeOptions onChange={this.props.onChange} options={this.props.options} />
<Thresholds onChange={this.props.onChange} options={this.props.options} />
<div className="form-section">
<ValueOptions onChange={onChange} options={options} />
<GaugeOptions onChange={onChange} options={options} />
<Thresholds onChange={onChange} options={options} />
</div>
<div className="form-section">
<ValueMappings onChange={onChange} options={options} />
</div>
</div>
);
}
......
......@@ -21,7 +21,7 @@ import {
DataQueryOptions,
IntervalValues,
} 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 { Organization, OrganizationState } from './organization';
import {
......@@ -93,7 +93,11 @@ export {
Threshold,
ValidationEvents,
ValidationRule,
ValueMap,
RangeMap,
IntervalValues,
MappingType,
BasicGaugeColor,
};
export interface StoreState {
......
......@@ -36,3 +36,30 @@ export interface Threshold {
color?: string;
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;
}
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { Threshold, TimeSeriesVMs } from 'app/types';
import { MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
import config from '../core/config';
import kbn from '../core/utils/kbn';
interface Props {
decimals: number;
height: number;
mappings: Array<RangeMap | ValueMap>;
maxValue: number;
minValue: number;
prefix: string;
timeSeries: TimeSeriesVMs;
showThresholdMarkers: boolean;
thresholds: Threshold[];
showThresholdMarkers: boolean;
showThresholdLabels: boolean;
unit: string;
width: number;
height: number;
stat: string;
prefix: string;
suffix: string;
unit: string;
width: number;
}
export class Gauge extends PureComponent<Props> {
canvasElement: any;
static defaultProps = {
minValue: 0,
maxValue: 100,
mappings: [],
minValue: 0,
prefix: '',
showThresholdMarkers: true,
showThresholdLabels: false,
suffix: '',
unit: 'none',
thresholds: [
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
],
unit: 'none',
};
componentDidMount() {
......@@ -43,16 +47,49 @@ export class Gauge extends PureComponent<Props> {
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) {
const { decimals, prefix, suffix, unit } = this.props;
const { decimals, mappings, prefix, suffix, unit } = this.props;
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)) {
return '-';
}
return `${prefix} ${formatFunc(value, decimals)} ${suffix}`;
return `${prefix} ${formattedValue} ${suffix}`;
}
draw() {
......
......@@ -96,8 +96,6 @@
@import 'components/empty_list_cta';
@import 'components/popper';
@import 'components/form_select_box';
@import 'components/user-picker';
@import 'components/description-picker';
@import 'components/panel_editor';
@import 'components/toolbar';
@import 'components/delete_button';
......@@ -106,6 +104,7 @@
@import 'components/unit-picker';
@import 'components/thresholds';
@import 'components/toggle_button_group';
@import 'components/value-mappings';
// PAGES
@import 'pages/login';
......
......@@ -271,7 +271,7 @@ $menu-dropdown-shadow: 5px 5px 20px -5px $black;
$tab-border-color: $dark-4;
// Toolbar
$toolbar-bg: $black;
$toolbar-bg: $input-black;
// Pagination
// -------------------------
......@@ -375,13 +375,14 @@ $checkbox-color: $dark-1;
//Panel Edit
// -------------------------
$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-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-border: 1px solid $dark-5;
$panel-editor-viz-item-shadow-hover: 0 0 4px $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-viz-item-bg-hover: darken($blue, 47%);
$panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
......
......@@ -384,6 +384,7 @@ $checkbox-color: $gray-7;
//Panel Edit
// -------------------------
$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-toolbar-view-bg: $white;
$panel-editor-viz-item-shadow: 0 0 4px $gray-3;
......
......@@ -78,6 +78,7 @@
.btn-link {
color: $btn-link-color;
background: transparent;
}
// Set the backgrounds
......
.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 {
padding-left: 5px;
input {
line-height: inherit;
}
}
.gf-form-select-box__menu {
background: $dropdownBackground;
background: $input-bg;
box-shadow: $menu-dropdown-shadow;
position: absolute;
z-index: 2;
min-width: 100%;
}
.gf-form-select-box__menu-list {
......@@ -64,16 +69,20 @@ $select-input-bg-disabled: $input-bg-disabled;
width: 100%;
}
/* .gf-form-select-box__single-value { */
/* } */
.gf-form-select-box__multi-value {
display: inline;
}
.gf-form-select-box__option {
border-left: 2px solid transparent;
white-space: nowrap;
&.gf-form-select-box__option--is-focused {
color: $dropdownLinkColorHover;
background-color: $dropdownLinkBackgroundHover;
background: $menu-dropdown-hover-bg;
@include left-brand-border-gradient();
}
......@@ -90,7 +99,7 @@ $select-input-bg-disabled: $input-bg-disabled;
.gf-form-select-box__value-container {
display: table-cell;
padding: 8px 10px;
padding: 6px 10px;
> div {
display: inline-block;
}
......@@ -119,10 +128,12 @@ $select-input-bg-disabled: $input-bg-disabled;
border-width: 0 5px 5px;
}
}
.gf-form-input--form-dropdown {
padding: 0;
border: 0;
overflow: visible;
position: relative;
}
.gf-form--has-input-icon {
......@@ -130,3 +141,32 @@ $select-input-bg-disabled: $input-bg-disabled;
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 {
flex-grow: 1;
min-height: 2.6rem;
}
&--error {
......
......@@ -21,10 +21,10 @@
display: none;
}
&.json-formatter-object::after {
content: "No properties";
content: 'No properties';
}
&.json-formatter-array::after {
content: "[]";
content: '[]';
}
}
}
......@@ -33,7 +33,9 @@
color: $json-explorer-string-color;
white-space: normal;
word-wrap: break-word;
word-break: break-all;
}
.json-formatter-number {
color: $json-explorer-number-color;
}
......@@ -87,7 +89,7 @@
&::after {
display: inline-block;
transition: transform $json-explorer-rotate-time ease-in;
content: "►";
content: '►';
}
}
......
......@@ -34,7 +34,6 @@
flex-grow: 1;
background: $page-bg;
margin: 0 20px 0 84px;
border-left: 2px solid $orange;
border-radius: 3px;
box-shadow: $panel-editor-shadow;
}
......@@ -133,14 +132,19 @@
}
.viz-picker {
margin-top: -40px;
padding: 20px;
position: relative;
}
.viz-picker-list {
display: flex;
flex-wrap: wrap;
margin-bottom: 13px;
}
.viz-picker__item {
background: $panel-editor-viz-item-bg;
border: $panel-editor-viz-item-border;
background: $panel-bg;
border: $panel-border;
border-radius: 3px;
height: 100px;
width: 150px;
......@@ -162,7 +166,7 @@
border: 1px solid $orange;
}
&--selected {
&:hover {
box-shadow: $panel-editor-viz-item-shadow-hover;
background: $panel-editor-viz-item-bg-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 {
text-overflow: ellipsis;
overflow: hidden;
......@@ -306,6 +324,13 @@
margin-bottom: 20px;
background: $input-label-bg;
border-radius: 3px;
position: relative;
.btn {
position: absolute;
right: 0;
top: 2px;
}
}
.form-section__body {
......
......@@ -77,7 +77,8 @@
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 37px;
width: 37px;
cursor: pointer;
}
......
.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;
}
.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) {
.edit-tab-with-sidemenu {
flex-direction: column;
......
......@@ -83,6 +83,10 @@ button.close {
position: absolute;
}
.flex-grow {
flex-grow: 1;
}
.center-vh {
display: flex;
align-items: center;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment