diff --git a/.gitignore b/.gitignore index 05ae4907e89..d599f762840 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ debug.test /devenv/bulk_alerting_dashboards/*.json /scripts/build/release_publisher/release_publisher +*.patch diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx new file mode 100644 index 00000000000..98eb3b682c0 --- /dev/null +++ b/public/app/core/components/Animations/FadeIn.tsx @@ -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 => { + 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 ( + + {state => ( +
+ {props.children} +
+ )} +
+ ); +}; diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx index 70dacd73849..6e8995298dc 100644 --- a/public/app/core/components/Animations/SlideDown.tsx +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -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 }, }; diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 37124eb7d4b..1fb93d29cf3 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -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) { diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index c9fe478dd9d..a42bd813782 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -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 { diff --git a/public/app/core/services/AngularLoader.ts b/public/app/core/services/AngularLoader.ts index e3a7dec4351..3f5c535297f 100644 --- a/public/app/core/services/AngularLoader.ts +++ b/public/app/core/services/AngularLoader.ts @@ -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(); + }, }; } } diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 1c91673495d..37f71946364 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -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'; diff --git a/public/app/core/utils/connectWithReduxStore.tsx b/public/app/core/utils/connectWithReduxStore.tsx index 92c61db4e77..be91958f8cd 100644 --- a/public/app/core/utils/connectWithReduxStore.tsx +++ b/public/app/core/utils/connectWithReduxStore.tsx @@ -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); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 12b4809f51a..55249a4716f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -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 { } onViewModeChanged(payload) { + ignoreNextWidthChange = true; this.setState({ animated: !payload.fullscreen }); } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 6853ade474b..d5c97a12e90 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -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 { 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 { 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 { } 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 { return (
- +
- {this.props.panel.isEditing && ( -
- -
+ {panel.isEditing && ( + )}
); } 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(); } diff --git a/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx new file mode 100644 index 00000000000..c40ca753a8e --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx @@ -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 { + 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 ( +
+ +
{ds.name}
+
+ ); + }; + + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + + renderFilters() { + return ( + <> + +
+ + +
+ + ); + } + + render() { + return ( + <> +
+ {this.renderFilters()} +
+
+
{this.getDataSources().map(this.renderDataSource)}
+ + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx new file mode 100644 index 00000000000..86ebdf86b67 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx @@ -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 { + 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 ( +
this.onToggleToolBarView(view)} key={view.title}> + +
{view.title}
+ +
+ ); + } + + renderButton(view: EditorToolBarView) { + return ( +
+ +
+ ); + } + + renderOpenView(view: EditorToolBarView) { + return ( +
+ + {view.render()} +
+ ); + } + + render() { + const { children, toolbarItems, main } = this.props; + const { openView } = this.state; + + return ( + <> +
+ {this.renderMainSelection(main)} +
+ {toolbarItems.map(item => this.renderButton(item))} +
+
+ +
+ + {openView && this.renderOpenView(openView)} + + {children} +
+
+
+ + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 371ed22fff7..09a8c7052e3 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -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 { 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 ; - } - - renderPanelOptions() { - const { pluginExports, panel } = this.props; - - if (pluginExports.PanelOptionsComponent) { - const OptionsComponent = pluginExports.PanelOptionsComponent; - return ; - } else { - return

Visualization has no options

; - } - } - - onPanelOptionsChanged = (options: any) => { - this.props.panel.updateOptions(options); - this.forceUpdate(); - }; - - renderVizTab() { - return ( -
-
- -
-
-
Options
- {this.renderPanelOptions()} -
-
- ); - } - onChangeTab = (tab: PanelEditorTab) => { store.dispatch( updateLocation({ @@ -81,28 +47,44 @@ export class PanelEditor extends PureComponent { 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 ( -
-
+
+
+
+
+
+
+ +
    {this.tabs.map(tab => { return ; })}
-
-
- {activeTab === 'queries' && this.renderQueriesTab()} - {activeTab === 'visualization' && this.renderVizTab()} -
+ {activeTab === 'queries' && } + {activeTab === 'visualization' && ( + + )}
); } @@ -121,8 +103,8 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) { }); return ( -
  • - onClick(tab)}> +
  • onClick(tab)}> + {tab.text}
  • diff --git a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx new file mode 100644 index 00000000000..0e1a336c16b --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx @@ -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 { + constructor(props) { + super(props); + } + + render() { + const style = { + display: 'flex', + 'align-items': 'center', + 'text-align': 'center', + height: '100%', + }; + + return ( +
    +
    + Panel plugin with id {this.props.pluginId} could not be found +
    +
    + ); + } +} + +export function getPanelPluginNotFound(id: string): PanelPlugin { + const NotFound = class NotFound extends PureComponent { + render() { + return ; + } + }; + + return { + id: id, + name: id, + sort: 100, + module: '', + baseUrl: '', + info: { + author: { + name: '', + }, + description: '', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '', + version: '', + }, + + exports: { + PanelComponent: NotFound, + }, + }; +} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index f13f212826a..be8ffe30666 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -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 { } render() { - return
    (this.element = element)} className="panel-height-helper" />; + const currentDataSource = { + title: 'ProductionDB', + imgSrc: 'public/app/plugins/datasource/prometheus/img/prometheus_logo.svg', + render: () => , + }; + + const queryInspector = { + title: 'Query Inspector', + render: () =>

    hello

    , + }; + + const dsHelp = { + title: '', + icon: 'fa fa-question', + render: () =>

    hello

    , + }; + + return ( + +
    (this.element = element)} style={{ width: '100%' }} /> + + ); } } diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx new file mode 100644 index 00000000000..1bf03bb3b23 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx @@ -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 { + constructor(props) { + super(props); + } + + renderPanelOptions() { + const { plugin, panel } = this.props; + const { PanelOptionsComponent } = plugin.exports; + + if (PanelOptionsComponent) { + return ; + } else { + return

    Visualization has no options

    ; + } + } + + 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 ; + }, + }; + + return ( + + {this.renderPanelOptions()} + + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index 9402133df34..4d886c86d74 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -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 { + searchInput: HTMLElement; + constructor(props) { super(props); @@ -36,34 +38,55 @@ export class VizTypePicker extends PureComponent { 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 (
    this.props.onTypeChanged(plugin)} title={plugin.name}> -
    {plugin.name}
    +
    ); }; - render() { + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + + renderFilters() { return ( -
    -
    -
    - -
    + <> + +
    + +
    -
    - -
    {this.state.pluginList.map(this.renderVizPlugin)}
    -
    + + ); + } + + render() { + const { pluginList } = this.state; + + return ( + <> +
    + {this.renderFilters()} +
    -
    + +
    {pluginList.map(this.renderVizPlugin)}
    + ); } } diff --git a/public/app/features/dashboard/specs/AddPanelPanel.test.tsx b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx index c5f66fed32a..4136883880b 100644 --- a/public/app/features/dashboard/specs/AddPanelPanel.test.tsx +++ b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx @@ -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'); }); }); diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 709eef3be38..c1b0a578b68 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -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'; diff --git a/public/app/features/datasources/DataSourceSettings.tsx b/public/app/features/datasources/DataSourceSettings.tsx deleted file mode 100644 index f7d641d34b0..00000000000 --- a/public/app/features/datasources/DataSourceSettings.tsx +++ /dev/null @@ -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 { - 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 ( -
    -

    Settings

    -
    -
    -
    -
    - Name - -
    -
    -
    - {this.shouldRenderInfoBox() &&
    {this.getInfoText()}
    } - {this.isReadyOnly() && ( -
    - This datasource was added by config and cannot be modified using the UI. Please contact your server admin - to update this datasource. -
    - )} -
    - - - - Back - -
    -
    -
    - ); - } -} - -function mapStateToProps(state) { - return { - dataSource: state.dataSources.dataSource, - dataSourceMeta: state.dataSources.dataSourceMeta, - }; -} - -export default connect(mapStateToProps)(DataSourceSettings); diff --git a/public/app/features/datasources/__mocks__/dataSourcesMocks.ts b/public/app/features/datasources/__mocks__/dataSourcesMocks.ts index 97819a18c82..755d8eef74a 100644 --- a/public/app/features/datasources/__mocks__/dataSourcesMocks.ts +++ b/public/app/features/datasources/__mocks__/dataSourcesMocks.ts @@ -29,6 +29,9 @@ export const getMockDataSource = (): DataSource => { return { access: '', basicAuth: false, + basicAuthUser: '', + basicAuthPassword: '', + withCredentials: false, database: '', id: 13, isDefault: false, diff --git a/public/app/features/datasources/settings/BasicSettings.test.tsx b/public/app/features/datasources/settings/BasicSettings.test.tsx new file mode 100644 index 00000000000..6729830bb75 --- /dev/null +++ b/public/app/features/datasources/settings/BasicSettings.test.tsx @@ -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(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx new file mode 100644 index 00000000000..adaec898dbc --- /dev/null +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -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 = ({ dataSourceName, onChange }) => { + return ( +
    +
    + + onChange(event.target.value)} + required + /> +
    +
    + ); +}; + +export default BasicSettings; diff --git a/public/app/features/datasources/settings/ButtonRow.test.tsx b/public/app/features/datasources/settings/ButtonRow.test.tsx new file mode 100644 index 00000000000..0acab8941ff --- /dev/null +++ b/public/app/features/datasources/settings/ButtonRow.test.tsx @@ -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(); +}; + +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(); + }); +}); diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx new file mode 100644 index 00000000000..cb70b29c706 --- /dev/null +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -0,0 +1,25 @@ +import React, { SFC } from 'react'; + +export interface Props { + isReadOnly: boolean; + onDelete: () => void; + onSubmit: (event) => void; +} + +const ButtonRow: SFC = ({ isReadOnly, onDelete, onSubmit }) => { + return ( +
    + + + + Back + +
    + ); +}; + +export default ButtonRow; diff --git a/public/app/features/datasources/settings/DataSourceSettings.test.tsx b/public/app/features/datasources/settings/DataSourceSettings.test.tsx new file mode 100644 index 00000000000..73c05fdd518 --- /dev/null +++ b/public/app/features/datasources/settings/DataSourceSettings.test.tsx @@ -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(); +}; + +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(); + }); +}); diff --git a/public/app/features/datasources/settings/DataSourceSettings.tsx b/public/app/features/datasources/settings/DataSourceSettings.tsx new file mode 100644 index 00000000000..0f07023b472 --- /dev/null +++ b/public/app/features/datasources/settings/DataSourceSettings.tsx @@ -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 { + 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 ( +
    + This datasource was added by config and cannot be modified using the UI. Please contact your server admin to + update this datasource. +
    + ); + } + + 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 ( +
    + + {Object.keys(dataSource).length === 0 ? ( + + ) : ( +
    +
    +
    + this.props.setDataSourceName(name)} + /> + + {this.shouldRenderInfoBox() &&
    {this.getInfoText()}
    } + + {this.isReadOnly() && this.renderIsReadOnlyMessage()} + {dataSourceMeta.module && ( + + )} + +
    + {testingMessage && ( +
    +
    + {testingStatus === 'error' ? ( + + ) : ( + + )} +
    +
    +
    {testingMessage}
    +
    +
    + )} +
    + + this.onSubmit(event)} + isReadOnly={this.isReadOnly()} + onDelete={this.onDelete} + /> + +
    +
    + )} +
    + ); + } +} + +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)); diff --git a/public/app/features/datasources/settings/PluginSettings.tsx b/public/app/features/datasources/settings/PluginSettings.tsx new file mode 100644 index 00000000000..e0b742985cc --- /dev/null +++ b/public/app/features/datasources/settings/PluginSettings.tsx @@ -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 { + 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 = ''; + + 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
    (this.element = element)} />; + } +} + +export default PluginSettings; diff --git a/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap new file mode 100644 index 00000000000..0c3e8c86dd4 --- /dev/null +++ b/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    + + Name + + +
    +
    +`; diff --git a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap new file mode 100644 index 00000000000..bd190f60b03 --- /dev/null +++ b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + + + + Back + +
    +`; + +exports[`Render should render with buttons enabled 1`] = ` +
    + + + + Back + +
    +`; diff --git a/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap new file mode 100644 index 00000000000..f95055c9095 --- /dev/null +++ b/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap @@ -0,0 +1,395 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render alpha info text 1`] = ` +
    + +
    +
    +
    + +
    + This plugin is marked as being in alpha state, which means it is in early development phase and updates will include breaking changes. +
    + +
    + + +
    +
    +
    +`; + +exports[`Render should render beta info text 1`] = ` +
    + +
    +
    +
    + +
    + 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. +
    + +
    + + +
    +
    +
    +`; + +exports[`Render should render component 1`] = ` +
    + +
    +
    +
    + + +
    + + +
    +
    +
    +`; + +exports[`Render should render is ready only message 1`] = ` +
    + +
    +
    +
    + +
    + This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource. +
    + +
    + + +
    +
    +
    +`; + +exports[`Render should render loader 1`] = ` +
    + + +
    +`; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index bb8fce8424a..c03860e1f01 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -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 = ThunkAction; @@ -145,6 +159,23 @@ export function loadDataSourceTypes(): ThunkResult { }; } +export function updateDataSource(dataSource: DataSource): ThunkResult { + return async dispatch => { + await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource); + await updateFrontendSettings(); + return dispatch(loadDataSource(dataSource.id)); + }; +} + +export function deleteDataSource(): ThunkResult { + 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); } diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index e0b6b39588e..3f59812a522 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -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: '', }, diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 7e235f5ea0a..33feae6770a 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -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; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index eef176eb49a..2466e465d1d 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/public/app/features/datasources/state/selectors.ts @@ -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; diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 77ebf754b3a..561663d36d5 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -44,8 +44,8 @@ const panelTemplate = ` -
    diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index d8dd67d5b61..358adb112b3 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -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: {}, }; }; diff --git a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap index 176304b7b11..160c02c6e8e 100644 --- a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap @@ -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": "", diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index c9fb250266c..ce9bcbff3bc 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,4 +1,3 @@ -import './plugin_edit_ctrl'; import './plugin_page_ctrl'; import './import_list/import_list'; import './ds_edit_ctrl'; diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index b9779190a8b..a0cedb3ebfd 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -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; diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index fed455472c9..d3c90c2ae43 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -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) { diff --git a/public/app/features/plugins/ds_dashboards_ctrl.ts b/public/app/features/plugins/ds_dashboards_ctrl.ts index a0324215453..71639702e41 100644 --- a/public/app/features/plugins/ds_dashboards_ctrl.ts +++ b/public/app/features/plugins/ds_dashboards_ctrl.ts @@ -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'; diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index c223f444ef3..5e203a96e8b 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -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'; diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html deleted file mode 100644 index 0b83e69c7d2..00000000000 --- a/public/app/features/plugins/partials/ds_edit.html +++ /dev/null @@ -1,72 +0,0 @@ - - -
    -

    Settings

    - -
    -
    -
    -
    - Name - - - The name is used when you select the data source in panels. - The Default data source is preselected in new - panels. - -
    - -
    -
    - -
    - This plugin is marked as being in alpha state, which means it is in early development phase and - updates will include breaking changes. -
    - -
    - 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. -
    - - - - - - -
    -

    Bundled Plugin Dashboards

    -
    - -
    -
    - -
    -
    Testing....
    -
    -
    - - -
    -
    -
    {{ctrl.testing.message}}
    -
    -
    -
    - -
    - This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource. -
    - -
    - - - Back -
    - -
    -
    -
    - -
    -
    diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 142eb942a30..fc11d8d98ec 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -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, diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts deleted file mode 100644 index 44d3d31f996..00000000000 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ /dev/null @@ -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); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 3eb8eff2d09..d9fb3450524 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -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) { diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx new file mode 100644 index 00000000000..48dc4d809f3 --- /dev/null +++ b/public/app/plugins/panel/gauge/module.tsx @@ -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 {} + +export class GaugePanel extends PureComponent { + render() { + const { timeSeries } = this.props; + + const vmSeries = getTimeSeriesVMs({ + timeSeries: timeSeries, + nullValueMode: NullValueMode.Ignore, + }); + + return ; + } +} + +export { GaugePanel as PanelComponent }; diff --git a/public/app/plugins/panel/gauge/plugin.json b/public/app/plugins/panel/gauge/plugin.json new file mode 100644 index 00000000000..1fff76fdd3b --- /dev/null +++ b/public/app/plugins/panel/gauge/plugin.json @@ -0,0 +1,18 @@ +{ + "type": "panel", + "name": "Gauge", + "id": "gauge", + + "state": "alpha", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + + } + } +} + diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index b132d3374f1..194f07823ad 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -61,11 +61,26 @@ export class GraphOptions extends PureComponent> { return (
    -
    -
    Draw Modes
    - - - +
    +
    Display Options
    +
    +
    Draw Modes
    + + + +
    +
    +
    Test Options
    + + + +
    +
    +
    +
    Axes
    +
    +
    +
    Thresholds
    ); diff --git a/public/app/plugins/panel/unknown/module.html b/public/app/plugins/panel/unknown/module.html deleted file mode 100644 index 02c4dc97fc1..00000000000 --- a/public/app/plugins/panel/unknown/module.html +++ /dev/null @@ -1,5 +0,0 @@ -
    - Unknown panel type: {{ctrl.panel.type}} -
    - - diff --git a/public/app/plugins/panel/unknown/module.ts b/public/app/plugins/panel/unknown/module.ts deleted file mode 100644 index d7229913bc5..00000000000 --- a/public/app/plugins/panel/unknown/module.ts +++ /dev/null @@ -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); - } -} diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index ed4d2d21827..807608e6960 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -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'; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 6770011278f..d3b3488f1fe 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -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: '', + resolve: { + component: () => DataSourceSettings, + }, }) .when('/datasources/edit/:id/dashboards', { template: '', diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index e6d86eaa9c6..943aff80a70 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -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)))); } } diff --git a/public/app/store/store.ts b/public/app/store/store.ts new file mode 100644 index 00000000000..c8134ae77f3 --- /dev/null +++ b/public/app/store/store.ts @@ -0,0 +1,5 @@ +export let store; + +export function setStore(newStore) { + store = newStore; +} diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 5522f3a11ce..a7b4ea7ac20 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -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; } +export interface DataSourceSelectItem { + name: string; + value: string | null; + meta: PluginMeta; + sort: string; +} + export interface DataSourcesState { dataSources: DataSource[]; searchQuery: string; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index fc176fed7e2..49c74a7de28 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -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, diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 817777669d8..3875305ea9d 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -12,14 +12,13 @@ export interface PluginExports { // Panel plugin PanelCtrl?; PanelComponent?: ComponentClass; - PanelOptionsComponent: ComponentClass; + PanelOptionsComponent?: ComponentClass; } 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 { diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 5396880611b..997090bf541 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -88,4 +88,5 @@ export interface DataQueryOptions { export interface DataSourceApi { query(options: DataQueryOptions): Promise; + testDatasource(): Promise; } diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx new file mode 100644 index 00000000000..5e90fd870db --- /dev/null +++ b/public/app/viz/Gauge.tsx @@ -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 { + 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 ( +
    (this.parentElement = element)}> +
    (this.canvasElement = element)} + /> +
    + ); + } +} + +export default withSize({ monitorHeight: true })(Gauge); diff --git a/public/app/viz/GaugeOptions.tsx b/public/app/viz/GaugeOptions.tsx new file mode 100644 index 00000000000..031855e1c78 --- /dev/null +++ b/public/app/viz/GaugeOptions.tsx @@ -0,0 +1,16 @@ +import React, { PureComponent } from 'react'; +import { PanelOptionsProps } from '../types'; + +interface Props {} + +export class GaugeOptions extends PureComponent> { + render() { + return ( +
    +
    +
    Draw Modes
    +
    +
    + ); + } +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index e4c7a9c59e1..55069163783 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -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'; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 1896e74d640..8aa01cf245d 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -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 // ------------------------- diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 45021a210fd..ec34c963344 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -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; diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 916c141a5ec..dcb4686701d 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -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; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index da1f140d252..5f142dfd02f 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -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; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 6d83fc6cf0b..f2756e16203 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -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; } diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index fe4bd9e719e..dcf3b1f0aa9 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -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; diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss new file mode 100644 index 00000000000..6d4022b6d3a --- /dev/null +++ b/public/sass/components/_panel_editor.scss @@ -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; +} diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 00bd5f7c94c..5dbb4518d52 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -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 { diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index 1efd275bfad..a6844b9e66b 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -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, diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index 87b43a31142..c1cf78716bb 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -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 { diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index eb3c8ce13f5..ea557b985a7 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -53,6 +53,7 @@ background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); } } + &.active--panel { background: $panel-bg !important; } diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index e12835d31c1..51a730926ba 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -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 { diff --git a/public/sass/components/_toolbar.scss b/public/sass/components/_toolbar.scss new file mode 100644 index 00000000000..aa1c46e0fb4 --- /dev/null +++ b/public/sass/components/_toolbar.scss @@ -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; + } +} diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss deleted file mode 100644 index 048e513cfbb..00000000000 --- a/public/sass/components/_viz_editor.scss +++ /dev/null @@ -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%); -} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 125edac500f..79fe0b2569b 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -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; diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js index 14866dbef20..db1cb1ffe24 100644 --- a/public/vendor/flot/jquery.flot.gauge.js +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -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.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 diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index dd3cc8c1190..def878ca1ea 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -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'), + }, }), - ] + ], });