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/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/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 371ed22fff7..8f988ba2b86 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { QueriesTab } from './QueriesTab'; import { VizTypePicker } from './VizTypePicker'; -import { store } from 'app/store/configureStore'; +import { store } from 'app/store/store'; import { updateLocation } from 'app/core/actions'; import { PanelModel } from '../panel_model'; 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..ebb856ca354 --- /dev/null +++ b/public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap @@ -0,0 +1,379 @@ +// 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..7c4ec0ace69 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, 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/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index d8dd67d5b61..4804cbbc594 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -26,6 +26,7 @@ export const getMockPlugins = (amount: number): Plugin[] => { pinned: false, state: '', type: '', + module: {}, }); } @@ -55,5 +56,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..5eb82fbefb6 100644 --- a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap @@ -33,6 +33,7 @@ exports[`Render should render component 1`] = ` "version": "1", }, "latestVersion": "1.0", + "module": Object {}, "name": "pretty cool plugin-0", "pinned": false, "state": "", @@ -66,6 +67,7 @@ exports[`Render should render component 1`] = ` "version": "1", }, "latestVersion": "1.1", + "module": Object {}, "name": "pretty cool plugin-1", "pinned": false, "state": "", @@ -99,6 +101,7 @@ exports[`Render should render component 1`] = ` "version": "1", }, "latestVersion": "1.2", + "module": Object {}, "name": "pretty cool plugin-2", "pinned": false, "state": "", @@ -132,6 +135,7 @@ exports[`Render should render component 1`] = ` "version": "1", }, "latestVersion": "1.3", + "module": Object {}, "name": "pretty cool plugin-3", "pinned": false, "state": "", @@ -165,6 +169,7 @@ exports[`Render should render component 1`] = ` "version": "1", }, "latestVersion": "1.4", + "module": Object {}, "name": "pretty cool plugin-4", "pinned": false, "state": "", @@ -198,6 +203,7 @@ exports[`Render should render component 1`] = ` "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/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..2fe25c4666d 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -149,6 +149,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/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..970ef4f11c8 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -13,9 +13,12 @@ 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; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 817777669d8..5dbde61d8d0 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -73,6 +73,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/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'), + }, }), - ] + ], });