mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'develop' into 12759-timeshift
This commit is contained in:
commit
81a9a3a3c1
1
.gitignore
vendored
1
.gitignore
vendored
@ -76,3 +76,4 @@ debug.test
|
||||
/devenv/bulk_alerting_dashboards/*.json
|
||||
|
||||
/scripts/build/release_publisher/release_publisher
|
||||
*.patch
|
||||
|
37
public/app/core/components/Animations/FadeIn.tsx
Normal file
37
public/app/core/components/Animations/FadeIn.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { SFC } from 'react';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
interface Props {
|
||||
duration: number;
|
||||
children: JSX.Element;
|
||||
in: boolean;
|
||||
}
|
||||
|
||||
export const FadeIn: SFC<Props> = props => {
|
||||
const defaultStyle = {
|
||||
transition: `opacity ${props.duration}ms linear`,
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
const transitionStyles = {
|
||||
exited: { opacity: 0, display: 'none' },
|
||||
entering: { opacity: 0 },
|
||||
entered: { opacity: 1 },
|
||||
exiting: { opacity: 0 },
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition in={props.in} timeout={props.duration}>
|
||||
{state => (
|
||||
<div
|
||||
style={{
|
||||
...defaultStyle,
|
||||
...transitionStyles[state],
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
@ -23,7 +23,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
|
||||
const transitionStyles = {
|
||||
exited: { maxHeight: 0 },
|
||||
entering: { maxHeight: maxHeight },
|
||||
entered: { maxHeight: maxHeight, overflow: 'visible' },
|
||||
entered: { maxHeight: 'unset', overflow: 'visible' },
|
||||
exiting: { maxHeight: 0 },
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
@ -5,18 +6,20 @@ import coreModule from '../core_module';
|
||||
function dashClass($timeout) {
|
||||
return {
|
||||
link: ($scope, elem) => {
|
||||
const body = $('body');
|
||||
|
||||
$scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
|
||||
console.log('view-mode-changed', panel.fullscreen);
|
||||
if (panel.fullscreen) {
|
||||
elem.addClass('panel-in-fullscreen');
|
||||
body.addClass('panel-in-fullscreen');
|
||||
} else {
|
||||
$timeout(() => {
|
||||
elem.removeClass('panel-in-fullscreen');
|
||||
body.removeClass('panel-in-fullscreen');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
|
||||
body.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
|
||||
|
||||
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
||||
if (newValue) {
|
||||
|
@ -18,6 +18,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
|
||||
|
||||
if (action.payload.partial) {
|
||||
query = _.defaults(query, state.query);
|
||||
query = _.omitBy(query, _.isNull);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -4,6 +4,7 @@ import _ from 'lodash';
|
||||
|
||||
export interface AngularComponent {
|
||||
destroy();
|
||||
digest();
|
||||
}
|
||||
|
||||
export class AngularLoader {
|
||||
@ -24,6 +25,9 @@ export class AngularLoader {
|
||||
scope.$destroy();
|
||||
compiledElem.remove();
|
||||
},
|
||||
digest: () => {
|
||||
scope.$digest();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { store } from 'app/store/store';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { store } from '../../store/configureStore';
|
||||
import { store } from '../../store/store';
|
||||
|
||||
export function connectWithStore(WrappedComponent, ...args) {
|
||||
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
|
||||
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
||||
import sizeMe from 'react-sizeme';
|
||||
|
||||
let lastGridWidth = 1200;
|
||||
let ignoreNextWidthChange = false;
|
||||
|
||||
function GridWrapper({
|
||||
size,
|
||||
@ -24,8 +25,12 @@ function GridWrapper({
|
||||
isFullscreen,
|
||||
}) {
|
||||
const width = size.width > 0 ? size.width : lastGridWidth;
|
||||
|
||||
// logic to ignore width changes (optimization)
|
||||
if (width !== lastGridWidth) {
|
||||
if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
|
||||
if (ignoreNextWidthChange) {
|
||||
ignoreNextWidthChange = false;
|
||||
} else if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
|
||||
onWidthChange();
|
||||
lastGridWidth = width;
|
||||
}
|
||||
@ -138,6 +143,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
}
|
||||
|
||||
onViewModeChanged(payload) {
|
||||
ignoreNextWidthChange = true;
|
||||
this.setState({ animated: !payload.fullscreen });
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,19 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { DashboardRow } from './DashboardRow';
|
||||
import { AddPanelPanel } from './AddPanelPanel';
|
||||
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||
import { PluginExports, PanelPlugin } from 'app/types/plugins';
|
||||
|
||||
import { AddPanelPanel } from './AddPanelPanel';
|
||||
import { getPanelPluginNotFound } from './PanelPluginNotFound';
|
||||
import { DashboardRow } from './DashboardRow';
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
@ -18,20 +22,19 @@ export interface Props {
|
||||
}
|
||||
|
||||
export interface State {
|
||||
pluginExports: PluginExports;
|
||||
plugin: PanelPlugin;
|
||||
}
|
||||
|
||||
export class DashboardPanel extends PureComponent<Props, State> {
|
||||
element: any;
|
||||
angularPanel: AngularComponent;
|
||||
pluginInfo: any;
|
||||
specialPanels = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pluginExports: null,
|
||||
plugin: null,
|
||||
};
|
||||
|
||||
this.specialPanels['row'] = this.renderRow.bind(this);
|
||||
@ -64,20 +67,22 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle plugin loading & changing of plugin type
|
||||
if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) {
|
||||
this.pluginInfo = config.panels[this.props.panel.type];
|
||||
const { panel } = this.props;
|
||||
|
||||
if (this.pluginInfo.exports) {
|
||||
// handle plugin loading & changing of plugin type
|
||||
if (!this.state.plugin || this.state.plugin.id !== panel.type) {
|
||||
const plugin = config.panels[panel.type] || getPanelPluginNotFound(panel.type);
|
||||
|
||||
if (plugin.exports) {
|
||||
this.cleanUpAngularPanel();
|
||||
this.setState({ pluginExports: this.pluginInfo.exports });
|
||||
this.setState({ plugin: plugin });
|
||||
} else {
|
||||
importPluginModule(this.pluginInfo.module).then(pluginExports => {
|
||||
importPluginModule(plugin.module).then(pluginExports => {
|
||||
this.cleanUpAngularPanel();
|
||||
// cache plugin exports (saves a promise async cycle next time)
|
||||
this.pluginInfo.exports = pluginExports;
|
||||
plugin.exports = pluginExports;
|
||||
// update panel state
|
||||
this.setState({ pluginExports: pluginExports });
|
||||
this.setState({ plugin: plugin });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -113,7 +118,9 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
renderReactPanel() {
|
||||
const { pluginExports } = this.state;
|
||||
const { dashboard, panel } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
const containerClass = this.props.isEditing ? 'panel-editor-container' : 'panel-height-helper';
|
||||
const panelWrapperClass = this.props.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
|
||||
// this might look strange with these classes that change when edit, but
|
||||
@ -121,37 +128,30 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className={panelWrapperClass}>
|
||||
<PanelChrome
|
||||
component={pluginExports.PanelComponent}
|
||||
panel={this.props.panel}
|
||||
dashboard={this.props.dashboard}
|
||||
/>
|
||||
<PanelChrome component={plugin.exports.PanelComponent} panel={panel} dashboard={dashboard} />
|
||||
</div>
|
||||
{this.props.panel.isEditing && (
|
||||
<div className="panel-editor-container__editor">
|
||||
<PanelEditor
|
||||
panel={this.props.panel}
|
||||
panelType={this.props.panel.type}
|
||||
dashboard={this.props.dashboard}
|
||||
onTypeChanged={this.onPluginTypeChanged}
|
||||
pluginExports={pluginExports}
|
||||
/>
|
||||
</div>
|
||||
{panel.isEditing && (
|
||||
<PanelEditor panel={panel} plugin={plugin} dashboard={dashboard} onTypeChanged={this.onPluginTypeChanged} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
if (this.isSpecial()) {
|
||||
return this.specialPanels[this.props.panel.type]();
|
||||
return this.specialPanels[panel.type]();
|
||||
}
|
||||
|
||||
if (!this.state.pluginExports) {
|
||||
// if we have not loaded plugin exports yet, wait
|
||||
if (!plugin || !plugin.exports) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.state.pluginExports.PanelComponent) {
|
||||
// if exporting PanelComponent it must be a react panel
|
||||
if (plugin.exports.PanelComponent) {
|
||||
return this.renderReactPanel();
|
||||
}
|
||||
|
||||
|
88
public/app/features/dashboard/dashgrid/DataSourcePicker.tsx
Normal file
88
public/app/features/dashboard/dashgrid/DataSourcePicker.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { DataSourceSelectItem } from 'app/types';
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
datasources: DataSourceSelectItem[];
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export class DataSourcePicker extends PureComponent<Props, State> {
|
||||
searchInput: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
datasources: getDatasourceSrv().getMetricSources(),
|
||||
searchQuery: '',
|
||||
};
|
||||
}
|
||||
|
||||
getDataSources() {
|
||||
const { datasources, searchQuery } = this.state;
|
||||
const regex = new RegExp(searchQuery, 'i');
|
||||
|
||||
const filtered = datasources.filter(item => {
|
||||
return regex.test(item.name) || regex.test(item.meta.name);
|
||||
});
|
||||
|
||||
return _.sortBy(filtered, 'sort');
|
||||
}
|
||||
|
||||
renderDataSource = (ds: DataSourceSelectItem, index) => {
|
||||
const cssClass = classNames({
|
||||
'ds-picker-list__item': true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={index} className={cssClass} title={ds.name}>
|
||||
<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);
|
||||
}
|
||||
|
||||
renderFilters() {
|
||||
return (
|
||||
<>
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-13"
|
||||
placeholder=""
|
||||
ref={elem => (this.searchInput = elem)}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<div className="p-l-1">
|
||||
<button className="btn toggle-btn gf-form-btn active">All</button>
|
||||
<button className="btn toggle-btn gf-form-btn">Favorites</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="cta-form__bar">
|
||||
{this.renderFilters()}
|
||||
<div className="gf-form--grow" />
|
||||
</div>
|
||||
<div className="ds-picker-list">{this.getDataSources().map(this.renderDataSource)}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
96
public/app/features/dashboard/dashgrid/EditorTabBody.tsx
Normal file
96
public/app/features/dashboard/dashgrid/EditorTabBody.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
|
||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element;
|
||||
main: EditorToolBarView;
|
||||
toolbarItems: EditorToolBarView[];
|
||||
}
|
||||
|
||||
export interface EditorToolBarView {
|
||||
title: string;
|
||||
imgSrc?: string;
|
||||
icon?: string;
|
||||
render: () => JSX.Element;
|
||||
}
|
||||
|
||||
interface State {
|
||||
openView?: EditorToolBarView;
|
||||
}
|
||||
|
||||
export class EditorTabBody extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
openView: null,
|
||||
};
|
||||
}
|
||||
|
||||
onToggleToolBarView = (item: EditorToolBarView) => {
|
||||
this.setState({
|
||||
openView: item === this.state.openView ? null : item,
|
||||
});
|
||||
};
|
||||
|
||||
onCloseOpenView = () => {
|
||||
this.setState({ openView: null });
|
||||
};
|
||||
|
||||
renderMainSelection(view: EditorToolBarView) {
|
||||
return (
|
||||
<div className="toolbar__main" onClick={() => this.onToggleToolBarView(view)} key={view.title}>
|
||||
<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) {
|
||||
return (
|
||||
<div className="nav-buttons" key={view.title}>
|
||||
<button className="btn navbar-button" onClick={() => this.onToggleToolBarView(view)}>
|
||||
{view.icon && <i className={view.icon} />} {view.title}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOpenView(view: EditorToolBarView) {
|
||||
return (
|
||||
<div className="toolbar-subview">
|
||||
<button className="toolbar-subview__close" onClick={this.onCloseOpenView}>
|
||||
<i className="fa fa-chevron-up" />
|
||||
</button>
|
||||
{view.render()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, toolbarItems, main } = this.props;
|
||||
const { openView } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar">
|
||||
{this.renderMainSelection(main)}
|
||||
<div className="gf-form--grow" />
|
||||
{toolbarItems.map(item => this.renderButton(item))}
|
||||
</div>
|
||||
<div className="panel-editor__scroll">
|
||||
<CustomScrollbar autoHide={false}>
|
||||
<div className="panel-editor__content">
|
||||
<FadeIn in={openView !== null} duration={200}>
|
||||
{openView && this.renderOpenView(openView)}
|
||||
</FadeIn>
|
||||
{children}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -2,20 +2,19 @@ import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { QueriesTab } from './QueriesTab';
|
||||
import { VizTypePicker } from './VizTypePicker';
|
||||
import { VisualizationTab } from './VisualizationTab';
|
||||
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { store } from 'app/store/store';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelPlugin, PluginExports } from 'app/types/plugins';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
|
||||
interface PanelEditorProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
panelType: string;
|
||||
pluginExports: PluginExports;
|
||||
plugin: PanelPlugin;
|
||||
onTypeChanged: (newType: PanelPlugin) => void;
|
||||
}
|
||||
|
||||
@ -34,43 +33,10 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
this.tabs = [
|
||||
{ id: 'queries', text: 'Queries', icon: 'fa fa-database' },
|
||||
{ id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' },
|
||||
{ id: 'alert', text: 'Alert', icon: 'gicon gicon-alert' },
|
||||
];
|
||||
}
|
||||
|
||||
renderQueriesTab() {
|
||||
return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
renderPanelOptions() {
|
||||
const { pluginExports, panel } = this.props;
|
||||
|
||||
if (pluginExports.PanelOptionsComponent) {
|
||||
const OptionsComponent = pluginExports.PanelOptionsComponent;
|
||||
return <OptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||
} else {
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
}
|
||||
|
||||
onPanelOptionsChanged = (options: any) => {
|
||||
this.props.panel.updateOptions(options);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
renderVizTab() {
|
||||
return (
|
||||
<div className="viz-editor">
|
||||
<div className="viz-editor-col1">
|
||||
<VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.props.onTypeChanged} />
|
||||
</div>
|
||||
<div className="viz-editor-col2">
|
||||
<h5 className="page-heading">Options</h5>
|
||||
{this.renderPanelOptions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onChangeTab = (tab: PanelEditorTab) => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
@ -81,28 +47,44 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: { tab: null, fullscreen: null, edit: null },
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { panel, dashboard, onTypeChanged, plugin } = this.props;
|
||||
const { location } = store.getState();
|
||||
const activeTab = location.query.tab || 'queries';
|
||||
|
||||
return (
|
||||
<div className="tabbed-view tabbed-view--new">
|
||||
<div className="tabbed-view-header">
|
||||
<div className="panel-editor-container__editor">
|
||||
<div className="panel-editor-resizer">
|
||||
<div className="panel-editor-resizer__handle">
|
||||
<div className="panel-editor-resizer__handle-dots" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-editor-tabs">
|
||||
<ul className="gf-tabs">
|
||||
{this.tabs.map(tab => {
|
||||
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
|
||||
<i className="fa fa-remove" />
|
||||
<button className="panel-editor-tabs__close" onClick={this.onClose}>
|
||||
<i className="fa fa-reply" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tabbed-view-body">
|
||||
{activeTab === 'queries' && this.renderQueriesTab()}
|
||||
{activeTab === 'visualization' && this.renderVizTab()}
|
||||
</div>
|
||||
{activeTab === 'queries' && <QueriesTab panel={panel} dashboard={dashboard} />}
|
||||
{activeTab === 'visualization' && (
|
||||
<VisualizationTab panel={panel} dashboard={dashboard} plugin={plugin} onTypeChanged={onTypeChanged} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -121,8 +103,8 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) {
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="gf-tabs-item" key={tab.id}>
|
||||
<a className={tabClasses} onClick={() => onClick(tab)}>
|
||||
<li className="gf-tabs-item" onClick={() => onClick(tab)}>
|
||||
<a className={tabClasses}>
|
||||
<i className={tab.icon} /> {tab.text}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -0,0 +1,64 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelPlugin, PanelProps } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
class PanelPluginNotFound extends PureComponent<Props> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
'text-align': 'center',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div className="alert alert-error" style={{ margin: '0 auto' }}>
|
||||
Panel plugin with id {this.props.pluginId} could not be found
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPanelPluginNotFound(id: string): PanelPlugin {
|
||||
const NotFound = class NotFound extends PureComponent<PanelProps> {
|
||||
render() {
|
||||
return <PanelPluginNotFound pluginId={id} />;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id: id,
|
||||
name: id,
|
||||
sort: 100,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
info: {
|
||||
author: {
|
||||
name: '',
|
||||
},
|
||||
description: '',
|
||||
links: [],
|
||||
logos: {
|
||||
large: '',
|
||||
small: '',
|
||||
},
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '',
|
||||
},
|
||||
|
||||
exports: {
|
||||
PanelComponent: NotFound,
|
||||
},
|
||||
};
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & utils
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import { DataSourcePicker } from './DataSourcePicker';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
@ -48,6 +47,27 @@ export class QueriesTab extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
||||
const currentDataSource = {
|
||||
title: 'ProductionDB',
|
||||
imgSrc: 'public/app/plugins/datasource/prometheus/img/prometheus_logo.svg',
|
||||
render: () => <DataSourcePicker />,
|
||||
};
|
||||
|
||||
const queryInspector = {
|
||||
title: 'Query Inspector',
|
||||
render: () => <h2>hello</h2>,
|
||||
};
|
||||
|
||||
const dsHelp = {
|
||||
title: '',
|
||||
icon: 'fa fa-question',
|
||||
render: () => <h2>hello</h2>,
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorTabBody main={currentDataSource} toolbarItems={[queryInspector, dsHelp]}>
|
||||
<div ref={element => (this.element = element)} style={{ width: '100%' }} />
|
||||
</EditorTabBody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
57
public/app/features/dashboard/dashgrid/VisualizationTab.tsx
Normal file
57
public/app/features/dashboard/dashgrid/VisualizationTab.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import { VizTypePicker } from './VizTypePicker';
|
||||
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
onTypeChanged: (newType: PanelPlugin) => void;
|
||||
}
|
||||
|
||||
export class VisualizationTab extends PureComponent<Props> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderPanelOptions() {
|
||||
const { plugin, panel } = this.props;
|
||||
const { PanelOptionsComponent } = plugin.exports;
|
||||
|
||||
if (PanelOptionsComponent) {
|
||||
return <PanelOptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||
} else {
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
}
|
||||
|
||||
onPanelOptionsChanged = (options: any) => {
|
||||
this.props.panel.updateOptions(options);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { plugin } = this.props;
|
||||
|
||||
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} />;
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorTabBody main={panelSelection} toolbarItems={[]}>
|
||||
{this.renderPanelOptions()}
|
||||
</EditorTabBody>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import config from 'app/core/config';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
|
||||
interface Props {
|
||||
currentType: string;
|
||||
current: PanelPlugin;
|
||||
onTypeChanged: (newType: PanelPlugin) => void;
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ interface State {
|
||||
}
|
||||
|
||||
export class VizTypePicker extends PureComponent<Props, State> {
|
||||
searchInput: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@ -36,34 +38,55 @@ export class VizTypePicker extends PureComponent<Props, State> {
|
||||
renderVizPlugin = (plugin, index) => {
|
||||
const cssClass = classNames({
|
||||
'viz-picker__item': true,
|
||||
'viz-picker__item--selected': plugin.id === this.props.currentType,
|
||||
'viz-picker__item--selected': plugin.id === this.props.current.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
|
||||
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||
<div className="viz-picker__item-name">{plugin.name}</div>
|
||||
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
renderFilters() {
|
||||
return (
|
||||
<div className="viz-picker">
|
||||
<div className="viz-picker__search">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input type="text" className="gf-form-input" placeholder="Search type" />
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
</div>
|
||||
<>
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-13"
|
||||
placeholder=""
|
||||
ref={elem => (this.searchInput = elem)}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<div className="p-l-1">
|
||||
<button className="btn toggle-btn gf-form-btn active">Basic Types</button>
|
||||
<button className="btn toggle-btn gf-form-btn">Master Types</button>
|
||||
</div>
|
||||
<div className="viz-picker__items">
|
||||
<CustomScrollbar>
|
||||
<div className="scroll-margin-helper">{this.state.pluginList.map(this.renderVizPlugin)}</div>
|
||||
</CustomScrollbar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { pluginList } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="cta-form__bar">
|
||||
{this.renderFilters()}
|
||||
<div className="gf-form--grow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="viz-picker">{pluginList.map(this.renderVizPlugin)}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { AddPanelPanel } from './../dashgrid/AddPanelPanel';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { shallow } from 'enzyme';
|
||||
import config from '../../../core/config';
|
||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||
|
||||
jest.mock('app/core/store', () => ({
|
||||
get: key => {
|
||||
@ -18,76 +19,11 @@ describe('AddPanelPanel', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
config.panels = [
|
||||
{
|
||||
id: 'singlestat',
|
||||
hideFromList: false,
|
||||
name: 'Singlestat',
|
||||
sort: 2,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
meta: {},
|
||||
info: {
|
||||
logos: {
|
||||
small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hidden',
|
||||
hideFromList: true,
|
||||
name: 'Hidden',
|
||||
sort: 100,
|
||||
meta: {},
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
info: {
|
||||
logos: {
|
||||
small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'graph',
|
||||
hideFromList: false,
|
||||
name: 'Graph',
|
||||
sort: 1,
|
||||
meta: {},
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
info: {
|
||||
logos: {
|
||||
small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alexander_zabbix',
|
||||
hideFromList: false,
|
||||
name: 'Zabbix',
|
||||
sort: 100,
|
||||
meta: {},
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
info: {
|
||||
logos: {
|
||||
small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'piechart',
|
||||
hideFromList: false,
|
||||
name: 'Piechart',
|
||||
sort: 100,
|
||||
meta: {},
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
info: {
|
||||
logos: {
|
||||
small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
getPanelPlugin({ id: 'singlestat', sort: 2 }),
|
||||
getPanelPlugin({ id: 'hidden', sort: 100, hideFromList: true }),
|
||||
getPanelPlugin({ id: 'graph', sort: 1 }),
|
||||
getPanelPlugin({ id: 'alexander_zabbix', sort: 100 }),
|
||||
getPanelPlugin({ id: 'piechart', sort: 100 }),
|
||||
];
|
||||
|
||||
dashboardMock = { toggleRow: jest.fn() };
|
||||
@ -97,16 +33,14 @@ describe('AddPanelPanel', () => {
|
||||
});
|
||||
|
||||
it('should fetch all panels sorted with core plugins first', () => {
|
||||
//console.log(wrapper.debug());
|
||||
//console.log(wrapper.find('.add-panel__item').get(0).props.title);
|
||||
expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Singlestat');
|
||||
expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('Piechart');
|
||||
expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('singlestat');
|
||||
expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('piechart');
|
||||
});
|
||||
|
||||
it('should filter', () => {
|
||||
wrapper.find('input').simulate('change', { target: { value: 'p' } });
|
||||
|
||||
expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Piechart');
|
||||
expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('Graph');
|
||||
expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('piechart');
|
||||
expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('graph');
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { store } from 'app/store/store';
|
||||
|
||||
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
|
@ -1,125 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { DataSource, Plugin } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
dataSource: DataSource;
|
||||
dataSourceMeta: Plugin;
|
||||
}
|
||||
interface State {
|
||||
name: string;
|
||||
}
|
||||
|
||||
enum DataSourceStates {
|
||||
Alpha = 'alpha',
|
||||
Beta = 'beta',
|
||||
}
|
||||
|
||||
export class DataSourceSettings extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
name: props.dataSource.name,
|
||||
};
|
||||
}
|
||||
|
||||
onNameChange = event => {
|
||||
this.setState({
|
||||
name: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onSubmit = event => {
|
||||
event.preventDefault();
|
||||
console.log(event);
|
||||
};
|
||||
|
||||
onDelete = event => {
|
||||
console.log(event);
|
||||
};
|
||||
|
||||
isReadyOnly() {
|
||||
return this.props.dataSource.readOnly === true;
|
||||
}
|
||||
|
||||
shouldRenderInfoBox() {
|
||||
const { state } = this.props.dataSourceMeta;
|
||||
|
||||
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
|
||||
}
|
||||
|
||||
getInfoText() {
|
||||
const { dataSourceMeta } = this.props;
|
||||
|
||||
switch (dataSourceMeta.state) {
|
||||
case DataSourceStates.Alpha:
|
||||
return (
|
||||
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
|
||||
' will include breaking changes.'
|
||||
);
|
||||
|
||||
case DataSourceStates.Beta:
|
||||
return (
|
||||
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
|
||||
' development and could be missing important features.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="page-sub-heading">Settings</h3>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-30">
|
||||
<span className="gf-form-label width-10">Name</span>
|
||||
<input
|
||||
className="gf-form-input max-width-23"
|
||||
type="text"
|
||||
value={name}
|
||||
placeholder="name"
|
||||
onChange={this.onNameChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
|
||||
{this.isReadyOnly() && (
|
||||
<div className="grafana-info-box span8">
|
||||
This datasource was added by config and cannot be modified using the UI. Please contact your server admin
|
||||
to update this datasource.
|
||||
</div>
|
||||
)}
|
||||
<div className="gf-form-button-row">
|
||||
<button type="submit" className="btn btn-success" disabled={this.isReadyOnly()} onClick={this.onSubmit}>
|
||||
Save & Test
|
||||
</button>
|
||||
<button type="submit" className="btn btn-danger" disabled={this.isReadyOnly()} onClick={this.onDelete}>
|
||||
Delete
|
||||
</button>
|
||||
<a className="btn btn-inverse" href="datasources">
|
||||
Back
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
dataSource: state.dataSources.dataSource,
|
||||
dataSourceMeta: state.dataSources.dataSourceMeta,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(DataSourceSettings);
|
@ -29,6 +29,9 @@ export const getMockDataSource = (): DataSource => {
|
||||
return {
|
||||
access: '',
|
||||
basicAuth: false,
|
||||
basicAuthUser: '',
|
||||
basicAuthPassword: '',
|
||||
withCredentials: false,
|
||||
database: '',
|
||||
id: 13,
|
||||
isDefault: false,
|
||||
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import BasicSettings, { Props } from './BasicSettings';
|
||||
|
||||
const setup = () => {
|
||||
const props: Props = {
|
||||
dataSourceName: 'Graphite',
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
return shallow(<BasicSettings {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
34
public/app/features/datasources/settings/BasicSettings.tsx
Normal file
34
public/app/features/datasources/settings/BasicSettings.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { SFC } from 'react';
|
||||
import { Label } from 'app/core/components/Label/Label';
|
||||
|
||||
export interface Props {
|
||||
dataSourceName: string;
|
||||
onChange: (name: string) => void;
|
||||
}
|
||||
|
||||
const BasicSettings: SFC<Props> = ({ dataSourceName, onChange }) => {
|
||||
return (
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form max-width-30">
|
||||
<Label
|
||||
tooltip={
|
||||
'The name is used when you select the data source in panels. The Default data source is' +
|
||||
'preselected in new panels.'
|
||||
}
|
||||
>
|
||||
Name
|
||||
</Label>
|
||||
<input
|
||||
className="gf-form-input max-width-23"
|
||||
type="text"
|
||||
value={dataSourceName}
|
||||
placeholder="Name"
|
||||
onChange={event => onChange(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicSettings;
|
31
public/app/features/datasources/settings/ButtonRow.test.tsx
Normal file
31
public/app/features/datasources/settings/ButtonRow.test.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ButtonRow, { Props } from './ButtonRow';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
isReadOnly: true,
|
||||
onSubmit: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<ButtonRow {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with buttons enabled', () => {
|
||||
const wrapper = setup({
|
||||
isReadOnly: false,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
25
public/app/features/datasources/settings/ButtonRow.tsx
Normal file
25
public/app/features/datasources/settings/ButtonRow.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
export interface Props {
|
||||
isReadOnly: boolean;
|
||||
onDelete: () => void;
|
||||
onSubmit: (event) => void;
|
||||
}
|
||||
|
||||
const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
|
||||
return (
|
||||
<div className="gf-form-button-row">
|
||||
<button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>
|
||||
Save & Test
|
||||
</button>
|
||||
<button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
|
||||
Delete
|
||||
</button>
|
||||
<a className="btn btn-inverse" href="/datasources">
|
||||
Back
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonRow;
|
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataSourceSettings, Props } from './DataSourceSettings';
|
||||
import { DataSource, NavModel } from '../../../types';
|
||||
import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
|
||||
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {} as NavModel,
|
||||
dataSource: getMockDataSource(),
|
||||
dataSourceMeta: getMockPlugin(),
|
||||
pageId: 1,
|
||||
deleteDataSource: jest.fn(),
|
||||
loadDataSource: jest.fn(),
|
||||
setDataSourceName: jest.fn(),
|
||||
updateDataSource: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<DataSourceSettings {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render loader', () => {
|
||||
const wrapper = setup({
|
||||
dataSource: {} as DataSource,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render beta info text', () => {
|
||||
const wrapper = setup({
|
||||
dataSourceMeta: { ...getMockPlugin(), state: 'beta' },
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render alpha info text', () => {
|
||||
const wrapper = setup({
|
||||
dataSourceMeta: { ...getMockPlugin(), state: 'alpha' },
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render is ready only message', () => {
|
||||
const wrapper = setup({
|
||||
dataSource: { ...getMockDataSource(), readOnly: true },
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
245
public/app/features/datasources/settings/DataSourceSettings.tsx
Normal file
245
public/app/features/datasources/settings/DataSourceSettings.tsx
Normal file
@ -0,0 +1,245 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import PluginSettings from './PluginSettings';
|
||||
import BasicSettings from './BasicSettings';
|
||||
import ButtonRow from './ButtonRow';
|
||||
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import { getDataSource, getDataSourceMeta } from '../state/selectors';
|
||||
import { deleteDataSource, loadDataSource, setDataSourceName, updateDataSource } from '../state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||
|
||||
import { DataSource, NavModel, Plugin } from 'app/types/';
|
||||
import { getDataSourceLoadingNav } from '../state/navModel';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
dataSource: DataSource;
|
||||
dataSourceMeta: Plugin;
|
||||
pageId: number;
|
||||
deleteDataSource: typeof deleteDataSource;
|
||||
loadDataSource: typeof loadDataSource;
|
||||
setDataSourceName: typeof setDataSourceName;
|
||||
updateDataSource: typeof updateDataSource;
|
||||
}
|
||||
|
||||
interface State {
|
||||
dataSource: DataSource;
|
||||
isTesting?: boolean;
|
||||
testingMessage?: string;
|
||||
testingStatus?: string;
|
||||
}
|
||||
|
||||
enum DataSourceStates {
|
||||
Alpha = 'alpha',
|
||||
Beta = 'beta',
|
||||
}
|
||||
|
||||
export class DataSourceSettings extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dataSource: {} as DataSource,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { loadDataSource, pageId } = this.props;
|
||||
|
||||
await loadDataSource(pageId);
|
||||
}
|
||||
|
||||
onSubmit = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
await this.props.updateDataSource({ ...this.state.dataSource, name: this.props.dataSource.name });
|
||||
|
||||
this.testDataSource();
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete this data source?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-trash',
|
||||
onConfirm: () => {
|
||||
this.confirmDelete();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
confirmDelete = () => {
|
||||
this.props.deleteDataSource();
|
||||
};
|
||||
|
||||
onModelChange = dataSource => {
|
||||
this.setState({
|
||||
dataSource: dataSource,
|
||||
});
|
||||
};
|
||||
|
||||
isReadOnly() {
|
||||
return this.props.dataSource.readOnly === true;
|
||||
}
|
||||
|
||||
shouldRenderInfoBox() {
|
||||
const { state } = this.props.dataSourceMeta;
|
||||
|
||||
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
|
||||
}
|
||||
|
||||
getInfoText() {
|
||||
const { dataSourceMeta } = this.props;
|
||||
|
||||
switch (dataSourceMeta.state) {
|
||||
case DataSourceStates.Alpha:
|
||||
return (
|
||||
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
|
||||
' will include breaking changes.'
|
||||
);
|
||||
|
||||
case DataSourceStates.Beta:
|
||||
return (
|
||||
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
|
||||
' development and could be missing important features.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderIsReadOnlyMessage() {
|
||||
return (
|
||||
<div className="grafana-info-box span8">
|
||||
This datasource was added by config and cannot be modified using the UI. Please contact your server admin to
|
||||
update this datasource.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async testDataSource() {
|
||||
const dsApi = await getDatasourceSrv().get(this.state.dataSource.name);
|
||||
|
||||
if (!dsApi.testDatasource) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
|
||||
|
||||
getBackendSrv().withNoBackendCache(async () => {
|
||||
try {
|
||||
const result = await dsApi.testDatasource();
|
||||
|
||||
this.setState({
|
||||
isTesting: false,
|
||||
testingStatus: result.status,
|
||||
testingMessage: result.message,
|
||||
});
|
||||
} catch (err) {
|
||||
let message = '';
|
||||
|
||||
if (err.statusText) {
|
||||
message = 'HTTP Error ' + err.statusText;
|
||||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isTesting: false,
|
||||
testingStatus: 'error',
|
||||
testingMessage: message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dataSource, dataSourceMeta, navModel } = this.props;
|
||||
const { testingMessage, testingStatus } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
{Object.keys(dataSource).length === 0 ? (
|
||||
<PageLoader pageName="Data source settings" />
|
||||
) : (
|
||||
<div className="page-container page-body">
|
||||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<BasicSettings
|
||||
dataSourceName={this.props.dataSource.name}
|
||||
onChange={name => this.props.setDataSourceName(name)}
|
||||
/>
|
||||
|
||||
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
|
||||
|
||||
{this.isReadOnly() && this.renderIsReadOnlyMessage()}
|
||||
{dataSourceMeta.module && (
|
||||
<PluginSettings
|
||||
dataSource={dataSource}
|
||||
dataSourceMeta={dataSourceMeta}
|
||||
onModelChange={this.onModelChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="gf-form-group section">
|
||||
{testingMessage && (
|
||||
<div className={`alert-${testingStatus} alert`}>
|
||||
<div className="alert-icon">
|
||||
{testingStatus === 'error' ? (
|
||||
<i className="fa fa-exclamation-triangle" />
|
||||
) : (
|
||||
<i className="fa fa-check" />
|
||||
)}
|
||||
</div>
|
||||
<div className="alert-body">
|
||||
<div className="alert-title">{testingMessage}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ButtonRow
|
||||
onSubmit={event => this.onSubmit(event)}
|
||||
isReadOnly={this.isReadOnly()}
|
||||
onDelete={this.onDelete}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const pageId = getRouteParamsId(state.location);
|
||||
const dataSource = getDataSource(state.dataSources, pageId);
|
||||
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')),
|
||||
dataSource: getDataSource(state.dataSources, pageId),
|
||||
dataSourceMeta: getDataSourceMeta(state.dataSources, dataSource.type),
|
||||
pageId: pageId,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
deleteDataSource,
|
||||
loadDataSource,
|
||||
setDataSourceName,
|
||||
updateDataSource,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettings));
|
63
public/app/features/datasources/settings/PluginSettings.tsx
Normal file
63
public/app/features/datasources/settings/PluginSettings.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { DataSource, Plugin } from 'app/types/';
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
|
||||
export interface Props {
|
||||
dataSource: DataSource;
|
||||
dataSourceMeta: Plugin;
|
||||
onModelChange: (dataSource: DataSource) => void;
|
||||
}
|
||||
|
||||
export class PluginSettings extends PureComponent<Props> {
|
||||
element: any;
|
||||
component: AngularComponent;
|
||||
scopeProps: {
|
||||
ctrl: { datasourceMeta: Plugin; current: DataSource };
|
||||
onModelChanged: (dataSource: DataSource) => void;
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.scopeProps = {
|
||||
ctrl: { datasourceMeta: props.dataSourceMeta, current: _.cloneDeep(props.dataSource) },
|
||||
onModelChanged: this.onModelChanged,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = getAngularLoader();
|
||||
const template = '<plugin-component type="datasource-config-ctrl" />';
|
||||
|
||||
this.component = loader.load(this.element, this.scopeProps, template);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.dataSource !== prevProps.dataSource) {
|
||||
this.scopeProps.ctrl.current = _.cloneDeep(this.props.dataSource);
|
||||
|
||||
this.component.digest();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.component) {
|
||||
this.component.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onModelChanged = (dataSource: DataSource) => {
|
||||
this.props.onModelChange(dataSource);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div ref={element => (this.element = element)} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginSettings;
|
@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form-group"
|
||||
>
|
||||
<div
|
||||
className="gf-form max-width-30"
|
||||
>
|
||||
<Component
|
||||
tooltip="The name is used when you select the data source in panels. The Default data source ispreselected in new panels."
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
<input
|
||||
className="gf-form-input max-width-23"
|
||||
onChange={[Function]}
|
||||
placeholder="Name"
|
||||
required={true}
|
||||
type="text"
|
||||
value="Graphite"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form-button-row"
|
||||
>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Save & Test
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={true}
|
||||
onClick={[MockFunction]}
|
||||
type="submit"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-inverse"
|
||||
href="/datasources"
|
||||
>
|
||||
Back
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render with buttons enabled 1`] = `
|
||||
<div
|
||||
className="gf-form-button-row"
|
||||
>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Save & Test
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="submit"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-inverse"
|
||||
href="/datasources"
|
||||
>
|
||||
Back
|
||||
</a>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,395 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render alpha info text 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<BasicSettings
|
||||
dataSourceName="gdev-cloudwatch"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="grafana-info-box"
|
||||
>
|
||||
This plugin is marked as being in alpha state, which means it is in early development phase and updates will include breaking changes.
|
||||
</div>
|
||||
<PluginSettings
|
||||
dataSource={
|
||||
Object {
|
||||
"access": "",
|
||||
"basicAuth": false,
|
||||
"basicAuthPassword": "",
|
||||
"basicAuthUser": "",
|
||||
"database": "",
|
||||
"id": 13,
|
||||
"isDefault": false,
|
||||
"jsonData": Object {
|
||||
"authType": "credentials",
|
||||
"defaultRegion": "eu-west-2",
|
||||
},
|
||||
"name": "gdev-cloudwatch",
|
||||
"orgId": 1,
|
||||
"password": "",
|
||||
"readOnly": false,
|
||||
"type": "cloudwatch",
|
||||
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
"url": "",
|
||||
"user": "",
|
||||
"withCredentials": false,
|
||||
}
|
||||
}
|
||||
dataSourceMeta={
|
||||
Object {
|
||||
"defaultNavUrl": "some/url",
|
||||
"enabled": false,
|
||||
"hasUpdate": false,
|
||||
"id": "1",
|
||||
"info": Object {
|
||||
"author": Object {
|
||||
"name": "Grafana Labs",
|
||||
"url": "url/to/GrafanaLabs",
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin 1",
|
||||
"pinned": false,
|
||||
"state": "alpha",
|
||||
"type": "",
|
||||
}
|
||||
}
|
||||
onModelChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="gf-form-group section"
|
||||
/>
|
||||
<ButtonRow
|
||||
isReadOnly={false}
|
||||
onDelete={[Function]}
|
||||
onSubmit={[Function]}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render beta info text 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<BasicSettings
|
||||
dataSourceName="gdev-cloudwatch"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="grafana-info-box"
|
||||
>
|
||||
This plugin is marked as being in a beta development state. This means it is in currently in active development and could be missing important features.
|
||||
</div>
|
||||
<PluginSettings
|
||||
dataSource={
|
||||
Object {
|
||||
"access": "",
|
||||
"basicAuth": false,
|
||||
"basicAuthPassword": "",
|
||||
"basicAuthUser": "",
|
||||
"database": "",
|
||||
"id": 13,
|
||||
"isDefault": false,
|
||||
"jsonData": Object {
|
||||
"authType": "credentials",
|
||||
"defaultRegion": "eu-west-2",
|
||||
},
|
||||
"name": "gdev-cloudwatch",
|
||||
"orgId": 1,
|
||||
"password": "",
|
||||
"readOnly": false,
|
||||
"type": "cloudwatch",
|
||||
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
"url": "",
|
||||
"user": "",
|
||||
"withCredentials": false,
|
||||
}
|
||||
}
|
||||
dataSourceMeta={
|
||||
Object {
|
||||
"defaultNavUrl": "some/url",
|
||||
"enabled": false,
|
||||
"hasUpdate": false,
|
||||
"id": "1",
|
||||
"info": Object {
|
||||
"author": Object {
|
||||
"name": "Grafana Labs",
|
||||
"url": "url/to/GrafanaLabs",
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin 1",
|
||||
"pinned": false,
|
||||
"state": "beta",
|
||||
"type": "",
|
||||
}
|
||||
}
|
||||
onModelChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="gf-form-group section"
|
||||
/>
|
||||
<ButtonRow
|
||||
isReadOnly={false}
|
||||
onDelete={[Function]}
|
||||
onSubmit={[Function]}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<BasicSettings
|
||||
dataSourceName="gdev-cloudwatch"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<PluginSettings
|
||||
dataSource={
|
||||
Object {
|
||||
"access": "",
|
||||
"basicAuth": false,
|
||||
"basicAuthPassword": "",
|
||||
"basicAuthUser": "",
|
||||
"database": "",
|
||||
"id": 13,
|
||||
"isDefault": false,
|
||||
"jsonData": Object {
|
||||
"authType": "credentials",
|
||||
"defaultRegion": "eu-west-2",
|
||||
},
|
||||
"name": "gdev-cloudwatch",
|
||||
"orgId": 1,
|
||||
"password": "",
|
||||
"readOnly": false,
|
||||
"type": "cloudwatch",
|
||||
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
"url": "",
|
||||
"user": "",
|
||||
"withCredentials": false,
|
||||
}
|
||||
}
|
||||
dataSourceMeta={
|
||||
Object {
|
||||
"defaultNavUrl": "some/url",
|
||||
"enabled": false,
|
||||
"hasUpdate": false,
|
||||
"id": "1",
|
||||
"info": Object {
|
||||
"author": Object {
|
||||
"name": "Grafana Labs",
|
||||
"url": "url/to/GrafanaLabs",
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin 1",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
"type": "",
|
||||
}
|
||||
}
|
||||
onModelChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="gf-form-group section"
|
||||
/>
|
||||
<ButtonRow
|
||||
isReadOnly={false}
|
||||
onDelete={[Function]}
|
||||
onSubmit={[Function]}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render is ready only message 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<BasicSettings
|
||||
dataSourceName="gdev-cloudwatch"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="grafana-info-box span8"
|
||||
>
|
||||
This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
|
||||
</div>
|
||||
<PluginSettings
|
||||
dataSource={
|
||||
Object {
|
||||
"access": "",
|
||||
"basicAuth": false,
|
||||
"basicAuthPassword": "",
|
||||
"basicAuthUser": "",
|
||||
"database": "",
|
||||
"id": 13,
|
||||
"isDefault": false,
|
||||
"jsonData": Object {
|
||||
"authType": "credentials",
|
||||
"defaultRegion": "eu-west-2",
|
||||
},
|
||||
"name": "gdev-cloudwatch",
|
||||
"orgId": 1,
|
||||
"password": "",
|
||||
"readOnly": true,
|
||||
"type": "cloudwatch",
|
||||
"typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
"url": "",
|
||||
"user": "",
|
||||
"withCredentials": false,
|
||||
}
|
||||
}
|
||||
dataSourceMeta={
|
||||
Object {
|
||||
"defaultNavUrl": "some/url",
|
||||
"enabled": false,
|
||||
"hasUpdate": false,
|
||||
"id": "1",
|
||||
"info": Object {
|
||||
"author": Object {
|
||||
"name": "Grafana Labs",
|
||||
"url": "url/to/GrafanaLabs",
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin 1",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
"type": "",
|
||||
}
|
||||
}
|
||||
onModelChange={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="gf-form-group section"
|
||||
/>
|
||||
<ButtonRow
|
||||
isReadOnly={true}
|
||||
onDelete={[Function]}
|
||||
onSubmit={[Function]}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render loader 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<PageLoader
|
||||
pageName="Data source settings"
|
||||
/>
|
||||
</div>
|
||||
`;
|
@ -1,10 +1,12 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { DataSource, Plugin, StoreState } from 'app/types';
|
||||
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions';
|
||||
import { UpdateLocationAction } from '../../../core/actions/location';
|
||||
import config from '../../../core/config';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
|
||||
import { UpdateLocationAction } from 'app/core/actions/location';
|
||||
import { buildNavModel } from './navModel';
|
||||
import { DataSource, Plugin, StoreState } from 'app/types';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadDataSources = 'LOAD_DATA_SOURCES',
|
||||
@ -14,43 +16,49 @@ export enum ActionTypes {
|
||||
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
|
||||
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
|
||||
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
|
||||
SetDataSourceName = 'SET_DATA_SOURCE_NAME',
|
||||
}
|
||||
|
||||
export interface LoadDataSourcesAction {
|
||||
interface LoadDataSourcesAction {
|
||||
type: ActionTypes.LoadDataSources;
|
||||
payload: DataSource[];
|
||||
}
|
||||
|
||||
export interface SetDataSourcesSearchQueryAction {
|
||||
interface SetDataSourcesSearchQueryAction {
|
||||
type: ActionTypes.SetDataSourcesSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetDataSourcesLayoutModeAction {
|
||||
interface SetDataSourcesLayoutModeAction {
|
||||
type: ActionTypes.SetDataSourcesLayoutMode;
|
||||
payload: LayoutMode;
|
||||
}
|
||||
|
||||
export interface LoadDataSourceTypesAction {
|
||||
interface LoadDataSourceTypesAction {
|
||||
type: ActionTypes.LoadDataSourceTypes;
|
||||
payload: Plugin[];
|
||||
}
|
||||
|
||||
export interface SetDataSourceTypeSearchQueryAction {
|
||||
interface SetDataSourceTypeSearchQueryAction {
|
||||
type: ActionTypes.SetDataSourceTypeSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface LoadDataSourceAction {
|
||||
interface LoadDataSourceAction {
|
||||
type: ActionTypes.LoadDataSource;
|
||||
payload: DataSource;
|
||||
}
|
||||
|
||||
export interface LoadDataSourceMetaAction {
|
||||
interface LoadDataSourceMetaAction {
|
||||
type: ActionTypes.LoadDataSourceMeta;
|
||||
payload: Plugin;
|
||||
}
|
||||
|
||||
interface SetDataSourceNameAction {
|
||||
type: ActionTypes.SetDataSourceName;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
|
||||
type: ActionTypes.LoadDataSources,
|
||||
payload: dataSources,
|
||||
@ -86,6 +94,11 @@ export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSe
|
||||
payload: query,
|
||||
});
|
||||
|
||||
export const setDataSourceName = (name: string) => ({
|
||||
type: ActionTypes.SetDataSourceName,
|
||||
payload: name,
|
||||
});
|
||||
|
||||
export type Action =
|
||||
| LoadDataSourcesAction
|
||||
| SetDataSourcesSearchQueryAction
|
||||
@ -95,7 +108,8 @@ export type Action =
|
||||
| SetDataSourceTypeSearchQueryAction
|
||||
| LoadDataSourceAction
|
||||
| UpdateNavIndexAction
|
||||
| LoadDataSourceMetaAction;
|
||||
| LoadDataSourceMetaAction
|
||||
| SetDataSourceNameAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
|
||||
@ -145,6 +159,23 @@ export function loadDataSourceTypes(): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateDataSource(dataSource: DataSource): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource);
|
||||
await updateFrontendSettings();
|
||||
return dispatch(loadDataSource(dataSource.id));
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteDataSource(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const dataSource = getStore().dataSources.dataSource;
|
||||
|
||||
await getBackendSrv().delete(`/api/datasources/${dataSource.id}`);
|
||||
dispatch(updateLocation({ path: '/datasources' }));
|
||||
};
|
||||
}
|
||||
|
||||
export function nameExits(dataSources, name) {
|
||||
return (
|
||||
dataSources.filter(dataSource => {
|
||||
@ -173,6 +204,16 @@ export function findNewName(dataSources, name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
function updateFrontendSettings() {
|
||||
return getBackendSrv()
|
||||
.get('/api/frontend/settings')
|
||||
.then(settings => {
|
||||
config.datasources = settings.datasources;
|
||||
config.defaultDatasource = settings.defaultDatasource;
|
||||
getDatasourceSrv().init();
|
||||
});
|
||||
}
|
||||
|
||||
function nameHasSuffix(name) {
|
||||
return name.endsWith('-', name.length - 1);
|
||||
}
|
||||
|
@ -48,6 +48,9 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
|
||||
{
|
||||
access: '',
|
||||
basicAuth: false,
|
||||
basicAuthUser: '',
|
||||
basicAuthPassword: '',
|
||||
withCredentials: false,
|
||||
database: '',
|
||||
id: 1,
|
||||
isDefault: false,
|
||||
@ -75,7 +78,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
|
||||
large: '',
|
||||
small: '',
|
||||
},
|
||||
screenshots: '',
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '',
|
||||
},
|
||||
|
@ -10,8 +10,8 @@ const initialState: DataSourcesState = {
|
||||
dataSourcesCount: 0,
|
||||
dataSourceTypes: [] as Plugin[],
|
||||
dataSourceTypeSearchQuery: '',
|
||||
dataSourceMeta: {} as Plugin,
|
||||
hasFetched: false,
|
||||
dataSourceMeta: {} as Plugin,
|
||||
};
|
||||
|
||||
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
|
||||
@ -36,6 +36,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
|
||||
|
||||
case ActionTypes.LoadDataSourceMeta:
|
||||
return { ...state, dataSourceMeta: action.payload };
|
||||
|
||||
case ActionTypes.SetDataSourceName:
|
||||
return { ...state, dataSource: { ...state.dataSource, name: action.payload } };
|
||||
}
|
||||
|
||||
return state;
|
||||
|
@ -20,7 +20,15 @@ export const getDataSource = (state, dataSourceId): DataSource | null => {
|
||||
if (state.dataSource.id === parseInt(dataSourceId, 10)) {
|
||||
return state.dataSource;
|
||||
}
|
||||
return null;
|
||||
return {} as DataSource;
|
||||
};
|
||||
|
||||
export const getDataSourceMeta = (state, type): Plugin => {
|
||||
if (state.dataSourceMeta.id === type) {
|
||||
return state.dataSourceMeta;
|
||||
}
|
||||
|
||||
return {} as Plugin;
|
||||
};
|
||||
|
||||
export const getDataSourcesSearchQuery = state => state.searchQuery;
|
||||
|
@ -44,8 +44,8 @@ const panelTemplate = `
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
|
||||
<i class="fa fa-remove"></i>
|
||||
<button class="panel-editor-tabs__close" ng-click="ctrl.exitFullscreen();">
|
||||
<i class="fa fa-reply"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Plugin } from 'app/types';
|
||||
import { Plugin, PanelPlugin } from 'app/types';
|
||||
|
||||
export const getMockPlugins = (amount: number): Plugin[] => {
|
||||
const plugins = [];
|
||||
@ -17,7 +17,7 @@ export const getMockPlugins = (amount: number): Plugin[] => {
|
||||
description: 'pretty decent plugin',
|
||||
links: ['one link'],
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
screenshots: `screenshot/${i}`,
|
||||
screenshots: [{ path: `screenshot/${i}` }],
|
||||
updated: '2018-09-26',
|
||||
version: '1',
|
||||
},
|
||||
@ -26,12 +26,38 @@ export const getMockPlugins = (amount: number): Plugin[] => {
|
||||
pinned: false,
|
||||
state: '',
|
||||
type: '',
|
||||
module: {},
|
||||
});
|
||||
}
|
||||
|
||||
return plugins;
|
||||
};
|
||||
|
||||
export const getPanelPlugin = (options: { id: string; sort?: number; hideFromList?: boolean }): PanelPlugin => {
|
||||
return {
|
||||
id: options.id,
|
||||
name: options.id,
|
||||
sort: options.sort || 1,
|
||||
info: {
|
||||
author: {
|
||||
name: options.id + 'name',
|
||||
},
|
||||
description: '',
|
||||
links: [],
|
||||
logos: {
|
||||
large: '',
|
||||
small: '',
|
||||
},
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '',
|
||||
},
|
||||
hideFromList: options.hideFromList === true,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockPlugin = () => {
|
||||
return {
|
||||
defaultNavUrl: 'some/url',
|
||||
@ -46,7 +72,7 @@ export const getMockPlugin = () => {
|
||||
description: 'pretty decent plugin',
|
||||
links: ['one link'],
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
screenshots: 'screenshot/1',
|
||||
screenshots: [{ path: `screenshot` }],
|
||||
updated: '2018-09-26',
|
||||
version: '1',
|
||||
},
|
||||
@ -55,5 +81,6 @@ export const getMockPlugin = () => {
|
||||
pinned: false,
|
||||
state: '',
|
||||
type: '',
|
||||
module: {},
|
||||
};
|
||||
};
|
||||
|
@ -28,11 +28,16 @@ exports[`Render should render component 1`] = `
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": "screenshot/0",
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot/0",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1.0",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin-0",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
@ -61,11 +66,16 @@ exports[`Render should render component 1`] = `
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": "screenshot/1",
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot/1",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1.1",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin-1",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
@ -94,11 +104,16 @@ exports[`Render should render component 1`] = `
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": "screenshot/2",
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot/2",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1.2",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin-2",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
@ -127,11 +142,16 @@ exports[`Render should render component 1`] = `
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": "screenshot/3",
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot/3",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1.3",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin-3",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
@ -160,11 +180,16 @@ exports[`Render should render component 1`] = `
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": "screenshot/4",
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot/4",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1.4",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin-4",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
@ -193,11 +218,16 @@ exports[`Render should render component 1`] = `
|
||||
"large": "large/logo",
|
||||
"small": "small/logo",
|
||||
},
|
||||
"screenshots": "screenshot/5",
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"path": "screenshot/5",
|
||||
},
|
||||
],
|
||||
"updated": "2018-09-26",
|
||||
"version": "1",
|
||||
},
|
||||
"latestVersion": "1.5",
|
||||
"module": Object {},
|
||||
"name": "pretty cool plugin-5",
|
||||
"pinned": false,
|
||||
"state": "",
|
||||
|
@ -1,4 +1,3 @@
|
||||
import './plugin_edit_ctrl';
|
||||
import './plugin_page_ctrl';
|
||||
import './import_list/import_list';
|
||||
import './ds_edit_ctrl';
|
||||
|
@ -24,6 +24,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
|
||||
const builtInPlugins = {
|
||||
'app/plugins/datasource/graphite/module': graphitePlugin,
|
||||
@ -52,6 +53,7 @@ const builtInPlugins = {
|
||||
'app/plugins/panel/table/module': tablePanel,
|
||||
'app/plugins/panel/singlestat/module': singlestatPanel,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
};
|
||||
|
||||
export default builtInPlugins;
|
||||
|
@ -1,14 +1,11 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
import { importPluginModule } from './plugin_loader';
|
||||
|
||||
// Types
|
||||
import { DataSourceApi } from 'app/types/series';
|
||||
import { DataSource } from 'app/types';
|
||||
import { DataSource, DataSourceSelectItem } from 'app/types';
|
||||
|
||||
export class DatasourceSrv {
|
||||
datasources: { [name: string]: DataSource };
|
||||
@ -102,8 +99,8 @@ export class DatasourceSrv {
|
||||
return _.sortBy(es, ['name']);
|
||||
}
|
||||
|
||||
getMetricSources(options) {
|
||||
const metricSources = [];
|
||||
getMetricSources(options?) {
|
||||
const metricSources: DataSourceSelectItem[] = [];
|
||||
|
||||
_.each(config.datasources, (value, key) => {
|
||||
if (value.meta && value.meta.metrics) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { coreModule } from 'app/core/core';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { store } from 'app/store/store';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { buildNavModel } from './state/navModel';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import { coreModule, appEvents } from 'app/core/core';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { store } from 'app/store/store';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { buildNavModel } from './state/navModel';
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<h3 class="page-sub-heading">Settings</h3>
|
||||
|
||||
<form name="ctrl.editForm" ng-if="ctrl.current">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Name</span>
|
||||
<input class="gf-form-input max-width-23" type="text" ng-model="ctrl.current.name" placeholder="name" required>
|
||||
<info-popover offset="0px -135px" mode="right-absolute">
|
||||
The name is used when you select the data source in panels.
|
||||
The <em>Default</em> data source is preselected in new
|
||||
panels.
|
||||
</info-popover>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'alpha'">
|
||||
This plugin is marked as being in alpha state, which means it is in early development phase and
|
||||
updates will include breaking changes.
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'beta'">
|
||||
This plugin is marked as being in a beta development state. This means it is in currently in active development and could be
|
||||
missing important features.
|
||||
</div>
|
||||
|
||||
<rebuild-on-change property="ctrl.datasourceMeta.id">
|
||||
<plugin-component type="datasource-config-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
|
||||
<div ng-if="ctrl.hasDashboards">
|
||||
<h3 class="section-heading">Bundled Plugin Dashboards</h3>
|
||||
<div class="section">
|
||||
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.testing" class="gf-form-group section">
|
||||
<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
|
||||
<div class="alert-{{ctrl.testing.status}} alert" ng-show="ctrl.testing.done">
|
||||
<div class="alert-icon">
|
||||
<i class="fa fa-exclamation-triangle" ng-show="ctrl.testing.status === 'error'"></i>
|
||||
<i class="fa fa-check" ng-show="ctrl.testing.status !== 'error'"></i>
|
||||
</div>
|
||||
<div class="alert-body">
|
||||
<div class="alert-title">{{ctrl.testing.message}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span8" ng-if="ctrl.current.readOnly">
|
||||
This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ctrl.current.readOnly" ng-click="ctrl.saveChanges()">Save & Test</button>
|
||||
<button type="submit" class="btn btn-danger" ng-disabled="ctrl.current.readOnly" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">Delete</button>
|
||||
<a class="btn btn-inverse" href="datasources">Back</a>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
</form>
|
||||
</div>
|
@ -5,8 +5,6 @@ import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { importPluginModule } from './plugin_loader';
|
||||
|
||||
import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
|
||||
|
||||
/** @ngInject */
|
||||
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
|
||||
function getTemplate(component) {
|
||||
@ -69,7 +67,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
};
|
||||
|
||||
const panelInfo = config.panels[scope.panel.type];
|
||||
let panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
|
||||
let panelCtrlPromise = Promise.resolve(null);
|
||||
if (panelInfo) {
|
||||
panelCtrlPromise = importPluginModule(panelInfo.module).then(panelModule => {
|
||||
return panelModule.PanelCtrl;
|
||||
@ -149,6 +147,14 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
scope.$watch(
|
||||
'ctrl.current',
|
||||
() => {
|
||||
scope.onModelChanged(scope.ctrl.current);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return {
|
||||
baseUrl: dsMeta.baseUrl,
|
||||
name: 'ds-config-' + dsMeta.id,
|
||||
|
@ -1,179 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import Remarkable from 'remarkable';
|
||||
|
||||
export class PluginEditCtrl {
|
||||
model: any;
|
||||
pluginIcon: string;
|
||||
pluginId: any;
|
||||
includes: any;
|
||||
readmeHtml: any;
|
||||
includedDatasources: any;
|
||||
tab: string;
|
||||
navModel: any;
|
||||
hasDashboards: any;
|
||||
preUpdateHook: () => any;
|
||||
postUpdateHook: () => any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $rootScope, private backendSrv, private $sce, private $routeParams, navModelSrv) {
|
||||
this.pluginId = $routeParams.pluginId;
|
||||
this.preUpdateHook = () => Promise.resolve();
|
||||
this.postUpdateHook = () => Promise.resolve();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
setNavModel(model) {
|
||||
let defaultTab = 'readme';
|
||||
|
||||
this.navModel = {
|
||||
main: {
|
||||
img: model.info.logos.large,
|
||||
subTitle: model.info.author.name,
|
||||
url: '',
|
||||
text: model.name,
|
||||
breadcrumbs: [{ title: 'Plugins', url: 'plugins' }],
|
||||
children: [
|
||||
{
|
||||
icon: 'fa fa-fw fa-file-text-o',
|
||||
id: 'readme',
|
||||
text: 'Readme',
|
||||
url: `plugins/${this.model.id}/edit?tab=readme`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (model.type === 'app') {
|
||||
this.navModel.main.children.push({
|
||||
icon: 'gicon gicon-cog',
|
||||
id: 'config',
|
||||
text: 'Config',
|
||||
url: `plugins/${this.model.id}/edit?tab=config`,
|
||||
});
|
||||
|
||||
const hasDashboards = _.find(model.includes, { type: 'dashboard' });
|
||||
|
||||
if (hasDashboards) {
|
||||
this.navModel.main.children.push({
|
||||
icon: 'gicon gicon-dashboard',
|
||||
id: 'dashboards',
|
||||
text: 'Dashboards',
|
||||
url: `plugins/${this.model.id}/edit?tab=dashboards`,
|
||||
});
|
||||
}
|
||||
|
||||
defaultTab = 'config';
|
||||
}
|
||||
|
||||
this.tab = this.$routeParams.tab || defaultTab;
|
||||
|
||||
for (const tab of this.navModel.main.children) {
|
||||
if (tab.id === this.tab) {
|
||||
tab.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => {
|
||||
this.model = result;
|
||||
this.pluginIcon = this.getPluginIcon(this.model.type);
|
||||
|
||||
this.model.dependencies.plugins.forEach(plug => {
|
||||
plug.icon = this.getPluginIcon(plug.type);
|
||||
});
|
||||
|
||||
this.includes = _.map(result.includes, plug => {
|
||||
plug.icon = this.getPluginIcon(plug.type);
|
||||
return plug;
|
||||
});
|
||||
|
||||
this.setNavModel(this.model);
|
||||
return this.initReadme();
|
||||
});
|
||||
}
|
||||
|
||||
initReadme() {
|
||||
return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
|
||||
const md = new Remarkable({
|
||||
linkify: true,
|
||||
});
|
||||
this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
|
||||
});
|
||||
}
|
||||
|
||||
getPluginIcon(type) {
|
||||
switch (type) {
|
||||
case 'datasource':
|
||||
return 'icon-gf icon-gf-datasources';
|
||||
case 'panel':
|
||||
return 'icon-gf icon-gf-panel';
|
||||
case 'app':
|
||||
return 'icon-gf icon-gf-apps';
|
||||
case 'page':
|
||||
return 'icon-gf icon-gf-endpoint-tiny';
|
||||
case 'dashboard':
|
||||
return 'icon-gf icon-gf-dashboard';
|
||||
default:
|
||||
return 'icon-gf icon-gf-apps';
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.preUpdateHook()
|
||||
.then(() => {
|
||||
const updateCmd = _.extend(
|
||||
{
|
||||
enabled: this.model.enabled,
|
||||
pinned: this.model.pinned,
|
||||
jsonData: this.model.jsonData,
|
||||
secureJsonData: this.model.secureJsonData,
|
||||
},
|
||||
{}
|
||||
);
|
||||
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
|
||||
})
|
||||
.then(this.postUpdateHook)
|
||||
.then(res => {
|
||||
window.location.href = window.location.href;
|
||||
});
|
||||
}
|
||||
|
||||
importDashboards() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
setPreUpdateHook(callback: () => any) {
|
||||
this.preUpdateHook = callback;
|
||||
}
|
||||
|
||||
setPostUpdateHook(callback: () => any) {
|
||||
this.postUpdateHook = callback;
|
||||
}
|
||||
|
||||
updateAvailable() {
|
||||
const modalScope = this.$scope.$new(true);
|
||||
modalScope.plugin = this.model;
|
||||
|
||||
this.$rootScope.appEvent('show-modal', {
|
||||
src: 'public/app/features/plugins/partials/update_instructions.html',
|
||||
scope: modalScope,
|
||||
});
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.model.enabled = true;
|
||||
this.model.pinned = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.model.enabled = false;
|
||||
this.model.pinned = false;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);
|
@ -362,14 +362,9 @@ export default class CloudWatchDatasource {
|
||||
const metricName = 'EstimatedCharges';
|
||||
const dimensions = {};
|
||||
|
||||
return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(
|
||||
() => {
|
||||
return { status: 'success', message: 'Data source is working' };
|
||||
},
|
||||
err => {
|
||||
return { status: 'error', message: err.message };
|
||||
}
|
||||
);
|
||||
return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(() => {
|
||||
return { status: 'success', message: 'Data source is working' };
|
||||
});
|
||||
}
|
||||
|
||||
awsRequest(url, data) {
|
||||
|
23
public/app/plugins/panel/gauge/module.tsx
Normal file
23
public/app/plugins/panel/gauge/module.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Gauge from 'app/viz/Gauge';
|
||||
import { NullValueMode, PanelProps } from 'app/types';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
|
||||
export interface Options {}
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { timeSeries } = this.props;
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
timeSeries: timeSeries,
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
});
|
||||
|
||||
return <Gauge maxValue={100} minValue={0} timeSeries={vmSeries} thresholds={[0, 100]} />;
|
||||
}
|
||||
}
|
||||
|
||||
export { GaugePanel as PanelComponent };
|
18
public/app/plugins/panel/gauge/plugin.json
Normal file
18
public/app/plugins/panel/gauge/plugin.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Gauge",
|
||||
"id": "gauge",
|
||||
|
||||
"state": "alpha",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,11 +61,26 @@ export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Draw Modes</h5>
|
||||
<Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
|
||||
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
|
||||
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
|
||||
<div className="form-option-box">
|
||||
<div className="form-option-box__header">Display Options</div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Draw Modes</h5>
|
||||
<Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
|
||||
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
|
||||
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
|
||||
</div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Test Options</h5>
|
||||
<Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
|
||||
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
|
||||
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-option-box">
|
||||
<div className="form-option-box__header">Axes</div>
|
||||
</div>
|
||||
<div className="form-option-box">
|
||||
<div className="form-option-box__header">Thresholds</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +0,0 @@
|
||||
<div class="text-center" style="padding-top: 2rem">
|
||||
Unknown panel type: <strong>{{ctrl.panel.type}}</strong>
|
||||
</div>
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
||||
|
||||
export class UnknownPanelCtrl extends PanelCtrl {
|
||||
static templateUrl = 'public/app/plugins/panel/unknown/module.html';
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector) {
|
||||
super($scope, $injector);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { store } from 'app/store/store';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
|
@ -14,6 +14,7 @@ import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
|
||||
import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
|
||||
import UsersListPage from 'app/features/users/UsersListPage';
|
||||
import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
|
||||
import DataSourceSettings from '../features/datasources/settings/DataSourceSettings';
|
||||
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
||||
|
||||
/** @ngInject */
|
||||
@ -74,10 +75,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
component: () => DataSourcesListPage,
|
||||
},
|
||||
})
|
||||
.when('/datasources/edit/:id', {
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
|
||||
controller: 'DataSourceEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
.when('/datasources/edit/:id/', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () => DataSourceSettings,
|
||||
},
|
||||
})
|
||||
.when('/datasources/edit/:id/dashboards', {
|
||||
template: '<react-container />',
|
||||
|
@ -11,6 +11,7 @@ import pluginReducers from 'app/features/plugins/state/reducers';
|
||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||
import usersReducers from 'app/features/users/state/reducers';
|
||||
import organizationReducers from 'app/features/org/state/reducers';
|
||||
import { setStore } from './store';
|
||||
|
||||
const rootReducers = {
|
||||
...sharedReducers,
|
||||
@ -25,8 +26,6 @@ const rootReducers = {
|
||||
...organizationReducers,
|
||||
};
|
||||
|
||||
export let store;
|
||||
|
||||
export function addRootReducer(reducers) {
|
||||
Object.assign(rootReducers, ...reducers);
|
||||
}
|
||||
@ -38,8 +37,8 @@ export function configureStore() {
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// DEV builds we had the logger middleware
|
||||
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));
|
||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
|
||||
} else {
|
||||
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)));
|
||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
|
||||
}
|
||||
}
|
||||
|
5
public/app/store/store.ts
Normal file
5
public/app/store/store.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export let store;
|
||||
|
||||
export function setStore(newStore) {
|
||||
store = newStore;
|
||||
}
|
@ -13,15 +13,25 @@ export interface DataSource {
|
||||
user: string;
|
||||
database: string;
|
||||
basicAuth: boolean;
|
||||
basicAuthPassword: string;
|
||||
basicAuthUser: string;
|
||||
isDefault: boolean;
|
||||
jsonData: { authType: string; defaultRegion: string };
|
||||
readOnly: boolean;
|
||||
withCredentials: boolean;
|
||||
meta?: PluginMeta;
|
||||
pluginExports?: PluginExports;
|
||||
init?: () => void;
|
||||
testDatasource?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export interface DataSourceSelectItem {
|
||||
name: string;
|
||||
value: string | null;
|
||||
meta: PluginMeta;
|
||||
sort: string;
|
||||
}
|
||||
|
||||
export interface DataSourcesState {
|
||||
dataSources: DataSource[];
|
||||
searchQuery: string;
|
||||
|
@ -7,7 +7,7 @@ import { DashboardState } from './dashboard';
|
||||
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
||||
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
|
||||
import { Invitee, OrgUser, User, UsersState, UserState } from './user';
|
||||
import { DataSource, DataSourcesState } from './datasources';
|
||||
import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources';
|
||||
import {
|
||||
TimeRange,
|
||||
LoadingState,
|
||||
@ -21,7 +21,7 @@ import {
|
||||
DataQueryOptions,
|
||||
} from './series';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
|
||||
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
|
||||
import { Organization, OrganizationPreferences, OrganizationState } from './organization';
|
||||
import {
|
||||
AppNotification,
|
||||
@ -55,6 +55,7 @@ export {
|
||||
OrgRole,
|
||||
PermissionLevel,
|
||||
DataSource,
|
||||
DataSourceSelectItem,
|
||||
PluginMeta,
|
||||
ApiKey,
|
||||
ApiKeysState,
|
||||
@ -68,6 +69,7 @@ export {
|
||||
UsersState,
|
||||
TimeRange,
|
||||
LoadingState,
|
||||
PanelPlugin,
|
||||
PanelProps,
|
||||
PanelOptionsProps,
|
||||
TimeSeries,
|
||||
|
@ -12,14 +12,13 @@ export interface PluginExports {
|
||||
// Panel plugin
|
||||
PanelCtrl?;
|
||||
PanelComponent?: ComponentClass<PanelProps>;
|
||||
PanelOptionsComponent: ComponentClass<PanelOptionsProps>;
|
||||
PanelOptionsComponent?: ComponentClass<PanelOptionsProps>;
|
||||
}
|
||||
|
||||
export interface PanelPlugin {
|
||||
id: string;
|
||||
name: string;
|
||||
meta: any;
|
||||
hideFromList: boolean;
|
||||
hideFromList?: boolean;
|
||||
module: string;
|
||||
baseUrl: string;
|
||||
info: any;
|
||||
@ -49,7 +48,7 @@ export interface PluginInclude {
|
||||
export interface PluginMetaInfo {
|
||||
author: {
|
||||
name: string;
|
||||
url: string;
|
||||
url?: string;
|
||||
};
|
||||
description: string;
|
||||
links: string[];
|
||||
@ -57,7 +56,7 @@ export interface PluginMetaInfo {
|
||||
large: string;
|
||||
small: string;
|
||||
};
|
||||
screenshots: string;
|
||||
screenshots: any[];
|
||||
updated: string;
|
||||
version: string;
|
||||
}
|
||||
@ -73,6 +72,7 @@ export interface Plugin {
|
||||
pinned: boolean;
|
||||
state: string;
|
||||
type: string;
|
||||
module: any;
|
||||
}
|
||||
|
||||
export interface PluginDashboard {
|
||||
|
@ -88,4 +88,5 @@ export interface DataQueryOptions {
|
||||
|
||||
export interface DataSourceApi {
|
||||
query(options: DataQueryOptions): Promise<DataQueryResponse>;
|
||||
testDatasource(): Promise<any>;
|
||||
}
|
||||
|
133
public/app/viz/Gauge.tsx
Normal file
133
public/app/viz/Gauge.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { withSize } from 'react-sizeme';
|
||||
import { TimeSeriesVMs } from 'app/types';
|
||||
import config from '../core/config';
|
||||
|
||||
interface Props {
|
||||
timeSeries: TimeSeriesVMs;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
showThresholdMarkers?: boolean;
|
||||
thresholds?: number[];
|
||||
showThresholdLables?: boolean;
|
||||
size?: { width: number; height: number };
|
||||
}
|
||||
|
||||
const colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
|
||||
|
||||
export class Gauge extends PureComponent<Props> {
|
||||
parentElement: any;
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLables: false,
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
const { maxValue, minValue, showThresholdLables, size, showThresholdMarkers, timeSeries, thresholds } = this.props;
|
||||
|
||||
const width = size.width;
|
||||
const height = size.height;
|
||||
const dimension = Math.min(width, height * 1.3);
|
||||
|
||||
const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const fontColor = config.bootData.user.lightTheme ? 'rgb(38,38,38)' : 'rgb(230,230,230)';
|
||||
const fontScale = parseInt('80', 10) / 100;
|
||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||
const gaugeWidth = Math.min(dimension / 6, 60);
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const formattedThresholds = [];
|
||||
|
||||
thresholds.forEach((threshold, index) => {
|
||||
formattedThresholds.push({
|
||||
value: threshold,
|
||||
color: colors[index],
|
||||
});
|
||||
});
|
||||
|
||||
const options = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
width: gaugeWidth,
|
||||
},
|
||||
frame: { show: false },
|
||||
label: { show: false },
|
||||
layout: { margin: 0, thresholdWidth: 0 },
|
||||
cell: { border: { width: 0 } },
|
||||
threshold: {
|
||||
values: formattedThresholds,
|
||||
label: {
|
||||
show: showThresholdLables,
|
||||
margin: thresholdMarkersWidth + 1,
|
||||
font: { size: thresholdLabelFontSize },
|
||||
},
|
||||
show: showThresholdMarkers,
|
||||
width: thresholdMarkersWidth,
|
||||
},
|
||||
value: {
|
||||
color: fontColor,
|
||||
formatter: () => {
|
||||
return Math.round(timeSeries[0].stats.avg);
|
||||
},
|
||||
font: {
|
||||
size: fontSize,
|
||||
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plotSeries = {
|
||||
data: [[0, timeSeries[0].stats.avg]],
|
||||
};
|
||||
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
||||
} catch (err) {
|
||||
console.log('Gauge rendering error', err, options, timeSeries);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { height, width } = this.props.size;
|
||||
|
||||
return (
|
||||
<div className="singlestat-panel" ref={element => (this.parentElement = element)}>
|
||||
<div
|
||||
style={{
|
||||
height: `${height * 0.9}px`,
|
||||
width: `${Math.min(width, height * 1.3)}px`,
|
||||
top: '10px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withSize({ monitorHeight: true })(Gauge);
|
16
public/app/viz/GaugeOptions.tsx
Normal file
16
public/app/viz/GaugeOptions.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelOptionsProps } from '../types';
|
||||
|
||||
interface Props {}
|
||||
|
||||
export class GaugeOptions extends PureComponent<PanelOptionsProps<Props>> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Draw Modes</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -97,7 +97,8 @@
|
||||
@import 'components/form_select_box';
|
||||
@import 'components/user-picker';
|
||||
@import 'components/description-picker';
|
||||
@import 'components/viz_editor';
|
||||
@import 'components/panel_editor';
|
||||
@import 'components/toolbar';
|
||||
@import 'components/delete_button';
|
||||
@import 'components/add_data_source.scss';
|
||||
@import 'components/page_loader';
|
||||
|
@ -77,6 +77,7 @@ $brand-gradient: linear-gradient(
|
||||
rgba(255, 68, 0, 0.7) 99%,
|
||||
rgba(255, 68, 0, 0.7) 100%
|
||||
);
|
||||
|
||||
$page-gradient: linear-gradient(180deg, #222426 10px, rgb(22, 23, 25) 100px);
|
||||
|
||||
// Links
|
||||
@ -110,7 +111,6 @@ $divider-border-color: #555;
|
||||
|
||||
// Graphite Target Editor
|
||||
$tight-form-bg: $dark-3;
|
||||
|
||||
$tight-form-func-bg: #333334;
|
||||
$tight-form-func-highlight-bg: #444445;
|
||||
|
||||
@ -128,6 +128,7 @@ $list-item-bg: $card-background;
|
||||
$list-item-hover-bg: lighten($gray-blue, 2%);
|
||||
$list-item-link-color: $text-color;
|
||||
$list-item-shadow: $card-shadow;
|
||||
|
||||
$empty-list-cta-bg: $gray-blue;
|
||||
|
||||
// Scrollbars
|
||||
@ -152,8 +153,8 @@ $table-bg-hover: $dark-3;
|
||||
$btn-primary-bg: #ff6600;
|
||||
$btn-primary-bg-hl: #bc3e06;
|
||||
|
||||
$btn-secondary-bg: $blue-dark;
|
||||
$btn-secondary-bg-hl: lighten($blue-dark, 5%);
|
||||
$btn-secondary-bg: $blue-dark;
|
||||
|
||||
$btn-success-bg: $green;
|
||||
$btn-success-bg-hl: darken($green, 6%);
|
||||
@ -266,6 +267,11 @@ $menu-dropdown-shadow: 5px 5px 20px -5px $black;
|
||||
// -------------------------
|
||||
$tab-border-color: $dark-4;
|
||||
|
||||
// Toolbar
|
||||
$toolbar-bg: $page-header-bg;
|
||||
$toolbar-shadow: 0 0 20px black;
|
||||
$toolbar-tab-bg: $gray-blue;
|
||||
|
||||
// Pagination
|
||||
// -------------------------
|
||||
|
||||
|
@ -31,6 +31,7 @@ $white: #fff;
|
||||
// Accent colors
|
||||
// -------------------------
|
||||
$blue: #0083b3;
|
||||
$blue-dark: #005f81;
|
||||
$blue-light: #00a8e6;
|
||||
$green: #3aa655;
|
||||
$red: #d44939;
|
||||
@ -213,6 +214,11 @@ $menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
|
||||
// -------------------------
|
||||
$tab-border-color: $gray-5;
|
||||
|
||||
// Toolbar
|
||||
$toolbar-bg: linear-gradient(90deg, #ffffff, #e6eef9);
|
||||
$toolbar-shadow: 1px 1px 3px #c7d0d8;
|
||||
$toolbar-tab-bg: $white;
|
||||
|
||||
// search
|
||||
$search-shadow: 0 5px 30px 0 $gray-4;
|
||||
$search-filter-box-bg: $gray-7;
|
||||
|
@ -120,8 +120,8 @@
|
||||
// Info appears as a neutral blue
|
||||
.btn-secondary {
|
||||
@include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl);
|
||||
// Inverse appears as dark gray
|
||||
}
|
||||
// Inverse appears as dark gray
|
||||
.btn-inverse {
|
||||
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow);
|
||||
//background: $card-background;
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
.panel-in-fullscreen {
|
||||
.react-grid-layout {
|
||||
height: 100% !important;
|
||||
height: calc(100% - 20px) !important;
|
||||
}
|
||||
|
||||
.react-grid-item {
|
||||
@ -19,6 +19,10 @@
|
||||
transform: translate(0px, 0px) !important;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Disable grid interaction indicators in fullscreen panels
|
||||
.panel-header:hover {
|
||||
background-color: inherit;
|
||||
|
@ -415,7 +415,27 @@ select.gf-form-input ~ .gf-form-help-icon {
|
||||
}
|
||||
|
||||
.cta-form__close {
|
||||
background: transparent;
|
||||
padding: 4px 8px 4px 9px;
|
||||
border: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
top: -2px;
|
||||
font-size: $font-size-md;
|
||||
|
||||
&:hover {
|
||||
color: $text-color-strong;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-form__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cta-form__bar-header {
|
||||
font-size: $font-size-h4;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
.panel-in-fullscreen {
|
||||
.navbar {
|
||||
@include navbar-alt-look();
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.navbar-button--add-panel,
|
||||
@ -50,10 +50,6 @@
|
||||
.navbar-page-btn .fa-caret-down {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-buttons--close {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-page-btn {
|
||||
@ -98,7 +94,7 @@
|
||||
}
|
||||
|
||||
.navbar-buttons {
|
||||
height: $navbarHeight;
|
||||
// height: $navbarHeight;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
208
public/sass/components/_panel_editor.scss
Normal file
208
public/sass/components/_panel_editor.scss
Normal file
@ -0,0 +1,208 @@
|
||||
.panel-editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-editor-container__panel {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.panel-editor-container__editor {
|
||||
margin-top: $panel-margin*2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 65%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel-editor__scroll {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.panel-editor__content {
|
||||
padding: 40px 15px;
|
||||
}
|
||||
|
||||
.panel-in-fullscreen {
|
||||
.sidemenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.submenu-controls {
|
||||
padding: 0 $dashboard-padding $panel-margin $dashboard-padding;
|
||||
}
|
||||
|
||||
.panel-editor-container__panel {
|
||||
margin: 0 $dashboard-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-editor-resizer {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
top: -23px;
|
||||
text-align: center;
|
||||
border-bottom: 2px dashed transparent;
|
||||
|
||||
&:hover {
|
||||
transition: border-color 0.2s ease-in 0.4s;
|
||||
transition-delay: 0.2s;
|
||||
border-color: $text-color-faint;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-editor-resizer__handle {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
height: 10px;
|
||||
cursor: grabbing;
|
||||
background: $input-label-bg;
|
||||
top: -8px;
|
||||
|
||||
&:hover {
|
||||
transition: background 0.2s ease-in 0.4s;
|
||||
transition-delay: 0.2s;
|
||||
background: linear-gradient(90deg, $orange, $red);
|
||||
.panel-editor-resizer__handle-dots {
|
||||
transition: opacity 0.2s ease-in;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-editor-resizer__handle-dots {
|
||||
border-top: 2px dashed $text-color-faint;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.viz-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.viz-picker__item {
|
||||
background: $card-background;
|
||||
box-shadow: $card-shadow;
|
||||
|
||||
border-radius: 3px;
|
||||
height: 90px;
|
||||
width: 150px;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid transparent;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: $card-background-hover;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
box-shadow: 0 0 12px #ff4d00;
|
||||
}
|
||||
}
|
||||
|
||||
.viz-picker__item-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: $font-size-sm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.viz-picker__item-img {
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.panel-editor-tabs {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-shadow: $page-header-shadow;
|
||||
border-bottom: 1px solid $page-header-border-color;
|
||||
padding: 0 $dashboard-padding;
|
||||
|
||||
@include clearfix();
|
||||
|
||||
.active.gf-tabs-link {
|
||||
background: $toolbar-tab-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-editor-tabs__close {
|
||||
padding: 5px 9px;
|
||||
border-radius: $border-radius;
|
||||
float: right;
|
||||
@include buttonBackground($btn-primary-bg, $btn-primary-bg-hl);
|
||||
}
|
||||
|
||||
.ds-picker-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 13px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ds-picker-list__item {
|
||||
background: $card-background;
|
||||
box-shadow: $card-shadow;
|
||||
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin-bottom: 3px;
|
||||
padding: 5px 15px;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: $card-background-hover;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
.ds-picker-list__name {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ds-picker-list__name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: $font-size-md;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.ds-picker-list__img {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.form-option-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-option-box__header {
|
||||
border-bottom: 2px solid $dark-4;
|
||||
padding: 5px 0px;
|
||||
font-size: $font-size-md;
|
||||
margin-bottom: 20px;
|
||||
}
|
@ -106,78 +106,78 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// Scrollbars
|
||||
// // Scrollbars
|
||||
// //
|
||||
//
|
||||
// ::-webkit-scrollbar {
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// }
|
||||
//
|
||||
// ::-webkit-scrollbar:hover {
|
||||
// height: 8px;
|
||||
// }
|
||||
//
|
||||
// ::-webkit-scrollbar-button:start:decrement,
|
||||
// ::-webkit-scrollbar-button:end:increment {
|
||||
// display: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:horizontal:decrement {
|
||||
// display: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:horizontal:increment {
|
||||
// display: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:vertical:decrement {
|
||||
// display: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:vertical:increment {
|
||||
// display: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:horizontal:decrement:active {
|
||||
// background-image: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:horizontal:increment:active {
|
||||
// background-image: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:vertical:decrement:active {
|
||||
// background-image: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-button:vertical:increment:active {
|
||||
// background-image: none;
|
||||
// }
|
||||
// ::-webkit-scrollbar-track-piece {
|
||||
// background-color: transparent;
|
||||
// }
|
||||
//
|
||||
// ::-webkit-scrollbar-thumb:vertical {
|
||||
// height: 50px;
|
||||
// background: -webkit-gradient(
|
||||
// linear,
|
||||
// left top,
|
||||
// right top,
|
||||
// color-stop(0%, $scrollbarBackground),
|
||||
// color-stop(100%, $scrollbarBackground2)
|
||||
// );
|
||||
// border: 1px solid $scrollbarBorder;
|
||||
// border-top: 1px solid $scrollbarBorder;
|
||||
// border-left: 1px solid $scrollbarBorder;
|
||||
// }
|
||||
//
|
||||
// ::-webkit-scrollbar-thumb:horizontal {
|
||||
// width: 50px;
|
||||
// background: -webkit-gradient(
|
||||
// linear,
|
||||
// left top,
|
||||
// left bottom,
|
||||
// color-stop(0%, $scrollbarBackground),
|
||||
// color-stop(100%, $scrollbarBackground2)
|
||||
// );
|
||||
// border: 1px solid $scrollbarBorder;
|
||||
// border-top: 1px solid $scrollbarBorder;
|
||||
// border-left: 1px solid $scrollbarBorder;
|
||||
// }
|
||||
//
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:hover {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:start:decrement,
|
||||
::-webkit-scrollbar-button:end:increment {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:horizontal:decrement {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:horizontal:increment {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:vertical:decrement {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:vertical:increment {
|
||||
display: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:horizontal:decrement:active {
|
||||
background-image: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:horizontal:increment:active {
|
||||
background-image: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:vertical:decrement:active {
|
||||
background-image: none;
|
||||
}
|
||||
::-webkit-scrollbar-button:vertical:increment:active {
|
||||
background-image: none;
|
||||
}
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:vertical {
|
||||
height: 50px;
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
right top,
|
||||
color-stop(0%, $scrollbarBackground),
|
||||
color-stop(100%, $scrollbarBackground2)
|
||||
);
|
||||
border: 1px solid $scrollbarBorder;
|
||||
border-top: 1px solid $scrollbarBorder;
|
||||
border-left: 1px solid $scrollbarBorder;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:horizontal {
|
||||
width: 50px;
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
left bottom,
|
||||
color-stop(0%, $scrollbarBackground),
|
||||
color-stop(100%, $scrollbarBackground2)
|
||||
);
|
||||
border: 1px solid $scrollbarBorder;
|
||||
border-top: 1px solid $scrollbarBorder;
|
||||
border-left: 1px solid $scrollbarBorder;
|
||||
}
|
||||
|
||||
// Baron styles
|
||||
|
||||
.baron {
|
||||
|
@ -4,8 +4,7 @@
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
margin: 0 0 $panel-margin 0;
|
||||
padding: 0 0 $panel-margin 0;
|
||||
}
|
||||
|
||||
.annotation-disabled,
|
||||
|
@ -2,9 +2,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
&.tabbed-view--new {
|
||||
padding: 25px 0 0 0;
|
||||
padding: 0 0 0 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@ -12,13 +13,14 @@
|
||||
.tabbed-view-header {
|
||||
box-shadow: $page-header-shadow;
|
||||
border-bottom: 1px solid $page-header-border-color;
|
||||
padding: 0 $dashboard-padding;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.tabbed-view-title {
|
||||
float: left;
|
||||
padding-top: 0.5rem;
|
||||
margin: 0 $spacer*3 0 $spacer*1;
|
||||
margin: 0 $spacer*3 0 0;
|
||||
}
|
||||
|
||||
.tabbed-view-panel-title {
|
||||
|
@ -53,6 +53,7 @@
|
||||
background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.active--panel {
|
||||
background: $panel-bg !important;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
background-color: $page-bg;
|
||||
border-radius: 0 0 0 4px;
|
||||
box-shadow: $search-shadow;
|
||||
z-index: $zindex-dropdown;
|
||||
}
|
||||
|
||||
.gf-timepicker-absolute-section {
|
||||
|
59
public/sass/components/_toolbar.scss
Normal file
59
public/sass/components/_toolbar.scss
Normal file
@ -0,0 +1,59 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
background: $toolbar-bg;
|
||||
box-shadow: $toolbar-shadow;
|
||||
padding: 7px 20px 7px 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toolbar__main {
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
font-size: $font-size-md;
|
||||
line-height: $input-line-height;
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
border-radius: $input-border-radius;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.fa {
|
||||
margin-left: 20px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar__main-image {
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toolbar-subview {
|
||||
position: relative;
|
||||
padding: 20px 20px;
|
||||
background-color: $empty-list-cta-bg;
|
||||
top: -45px;
|
||||
margin: 0 30px 20px 0px;
|
||||
}
|
||||
|
||||
.toolbar-subview__close {
|
||||
background: transparent;
|
||||
padding: 4px 8px 4px 9px;
|
||||
border: none;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 20px;
|
||||
font-size: $font-size-md;
|
||||
|
||||
&:hover {
|
||||
color: $text-color-strong;
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
.viz-editor {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.viz-editor-col1 {
|
||||
width: 210px;
|
||||
height: 100%;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.viz-editor-col2 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.viz-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.viz-picker__search {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.viz-picker__items {
|
||||
flex-grow: 1;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.viz-picker__item {
|
||||
background: $card-background;
|
||||
box-shadow: $card-shadow;
|
||||
|
||||
border-radius: 3px;
|
||||
padding: $spacer;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid transparent;
|
||||
@include left-brand-border;
|
||||
|
||||
&:hover {
|
||||
background: $card-background-hover;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
// border: 1px solid $orange;
|
||||
@include left-brand-border-gradient();
|
||||
|
||||
.viz-picker__item-name {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.viz-picker__item-img {
|
||||
filter: saturate(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viz-picker__item-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: $font-size-h5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
padding-left: $spacer;
|
||||
font-size: $font-size-md;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.viz-picker__item-img {
|
||||
height: 100%;
|
||||
filter: saturate(30%);
|
||||
}
|
@ -37,20 +37,6 @@ div.flot-text {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-editor-container__panel {
|
||||
height: 35%;
|
||||
}
|
||||
|
||||
.panel-editor-container__editor {
|
||||
height: 65%;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
background-color: $panel-bg;
|
||||
border: $panel-border;
|
||||
|
37
public/vendor/flot/jquery.flot.gauge.js
vendored
37
public/vendor/flot/jquery.flot.gauge.js
vendored
@ -583,30 +583,31 @@
|
||||
* @param {Number} [a] the angle of the value drawn
|
||||
*/
|
||||
function drawText(x, y, id, text, textOptions, a) {
|
||||
var span = $("." + id, placeholder);
|
||||
var span = $(placeholder).find("#" + id);
|
||||
var exists = span.length;
|
||||
if (!exists) {
|
||||
span = $("<span></span>")
|
||||
span.attr("id", id);
|
||||
span.css("position", "absolute");
|
||||
span.css("top", y + "px");
|
||||
if (textOptions.font.size) {
|
||||
span.css("font-size", textOptions.font.size + "px");
|
||||
}
|
||||
if (textOptions.font.family) {
|
||||
span.css("font-family", textOptions.font.family);
|
||||
}
|
||||
if (textOptions.color) {
|
||||
span.css("color", textOptions.color);
|
||||
}
|
||||
if (textOptions.background.color) {
|
||||
span.css("background-color", textOptions.background.color);
|
||||
}
|
||||
if (textOptions.background.opacity) {
|
||||
span.css("opacity", textOptions.background.opacity);
|
||||
}
|
||||
placeholder.append(span);
|
||||
}
|
||||
|
||||
span.css("position", "absolute");
|
||||
span.css("top", y + "px");
|
||||
if (textOptions.font.size) {
|
||||
span.css("font-size", textOptions.font.size + "px");
|
||||
}
|
||||
if (textOptions.font.family) {
|
||||
span.css("font-family", textOptions.font.family);
|
||||
}
|
||||
if (textOptions.color) {
|
||||
span.css("color", textOptions.color);
|
||||
}
|
||||
if (textOptions.background.color) {
|
||||
span.css("background-color", textOptions.background.color);
|
||||
}
|
||||
if (textOptions.background.opacity) {
|
||||
span.css("opacity", textOptions.background.opacity);
|
||||
}
|
||||
span.text(text);
|
||||
// after append, readjust the left position
|
||||
span.css("left", x + "px"); // for redraw, resetting the left position is needed here
|
||||
|
@ -4,22 +4,19 @@ const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
|
||||
module.exports = merge(common, {
|
||||
entry: {
|
||||
app: [
|
||||
'webpack-dev-server/client?http://localhost:3333',
|
||||
'./public/app/dev.ts',
|
||||
],
|
||||
app: ['webpack-dev-server/client?http://localhost:3333', './public/app/dev.ts'],
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../public/build'),
|
||||
filename: '[name].[hash].js',
|
||||
publicPath: "/public/build/",
|
||||
publicPath: '/public/build/',
|
||||
pathinfo: false,
|
||||
},
|
||||
|
||||
@ -34,8 +31,8 @@ module.exports = merge(common, {
|
||||
hot: true,
|
||||
port: 3333,
|
||||
proxy: {
|
||||
'!/public/build': 'http://localhost:3000'
|
||||
}
|
||||
'!/public/build': 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
|
||||
optimization: {
|
||||
@ -49,38 +46,37 @@ module.exports = merge(common, {
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
babelrc: false,
|
||||
plugins: [
|
||||
'syntax-dynamic-import',
|
||||
'react-hot-loader/babel'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
experimentalWatchApi: true
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
babelrc: false,
|
||||
plugins: ['syntax-dynamic-import', 'react-hot-loader/babel'],
|
||||
},
|
||||
},
|
||||
}],
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
experimentalWatchApi: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
"style-loader", // creates style nodes from JS strings
|
||||
"css-loader", // translates CSS into CommonJS
|
||||
"sass-loader" // compiles Sass to CSS
|
||||
]
|
||||
'style-loader', // creates style nodes from JS strings
|
||||
'css-loader', // translates CSS into CommonJS
|
||||
'sass-loader', // compiles Sass to CSS
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
|
||||
loader: 'file-loader'
|
||||
loader: 'file-loader',
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
@ -89,16 +85,16 @@ module.exports = merge(common, {
|
||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||
inject: 'body',
|
||||
alwaysWriteToDisk: true
|
||||
alwaysWriteToDisk: true,
|
||||
}),
|
||||
new HtmlWebpackHarddiskPlugin(),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'GRAFANA_THEME': JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
|
||||
GRAFANA_THEME: JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('development')
|
||||
}
|
||||
NODE_ENV: JSON.stringify('development'),
|
||||
},
|
||||
}),
|
||||
]
|
||||
],
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user