Commit e0feb726 by Torkel Ödegaard

Merge remote-tracking branch 'origin/data-source-settings-to-react' into develop

parents a2a44589 7799ad81
......@@ -76,3 +76,4 @@ debug.test
/devenv/bulk_alerting_dashboards/*.json
/scripts/build/release_publisher/release_publisher
*.patch
......@@ -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();
},
};
}
}
......
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';
......
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);
......
......@@ -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';
......
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';
......
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { DataSource, Plugin } from 'app/types';
export interface Props {
dataSource: DataSource;
dataSourceMeta: Plugin;
}
interface State {
name: string;
}
enum DataSourceStates {
Alpha = 'alpha',
Beta = 'beta',
}
export class DataSourceSettings extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
name: props.dataSource.name,
};
}
onNameChange = event => {
this.setState({
name: event.target.value,
});
};
onSubmit = event => {
event.preventDefault();
console.log(event);
};
onDelete = event => {
console.log(event);
};
isReadyOnly() {
return this.props.dataSource.readOnly === true;
}
shouldRenderInfoBox() {
const { state } = this.props.dataSourceMeta;
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
}
getInfoText() {
const { dataSourceMeta } = this.props;
switch (dataSourceMeta.state) {
case DataSourceStates.Alpha:
return (
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
' will include breaking changes.'
);
case DataSourceStates.Beta:
return (
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
' development and could be missing important features.'
);
}
return null;
}
render() {
const { name } = this.state;
return (
<div>
<h3 className="page-sub-heading">Settings</h3>
<form onSubmit={this.onSubmit}>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<span className="gf-form-label width-10">Name</span>
<input
className="gf-form-input max-width-23"
type="text"
value={name}
placeholder="name"
onChange={this.onNameChange}
required
/>
</div>
</div>
</div>
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
{this.isReadyOnly() && (
<div className="grafana-info-box span8">
This datasource was added by config and cannot be modified using the UI. Please contact your server admin
to update this datasource.
</div>
)}
<div className="gf-form-button-row">
<button type="submit" className="btn btn-success" disabled={this.isReadyOnly()} onClick={this.onSubmit}>
Save &amp; Test
</button>
<button type="submit" className="btn btn-danger" disabled={this.isReadyOnly()} onClick={this.onDelete}>
Delete
</button>
<a className="btn btn-inverse" href="datasources">
Back
</a>
</div>
</form>
</div>
);
}
}
function mapStateToProps(state) {
return {
dataSource: state.dataSources.dataSource,
dataSourceMeta: state.dataSources.dataSourceMeta,
};
}
export default connect(mapStateToProps)(DataSourceSettings);
......@@ -29,6 +29,9 @@ export const getMockDataSource = (): DataSource => {
return {
access: '',
basicAuth: false,
basicAuthUser: '',
basicAuthPassword: '',
withCredentials: false,
database: '',
id: 13,
isDefault: false,
......
import React from 'react';
import { shallow } from 'enzyme';
import BasicSettings, { Props } from './BasicSettings';
const setup = () => {
const props: Props = {
dataSourceName: 'Graphite',
onChange: jest.fn(),
};
return shallow(<BasicSettings {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});
import React, { SFC } from 'react';
import { Label } from 'app/core/components/Label/Label';
export interface Props {
dataSourceName: string;
onChange: (name: string) => void;
}
const BasicSettings: SFC<Props> = ({ dataSourceName, onChange }) => {
return (
<div className="gf-form-group">
<div className="gf-form max-width-30">
<Label
tooltip={
'The name is used when you select the data source in panels. The Default data source is' +
'preselected in new panels.'
}
>
Name
</Label>
<input
className="gf-form-input max-width-23"
type="text"
value={dataSourceName}
placeholder="Name"
onChange={event => onChange(event.target.value)}
required
/>
</div>
</div>
);
};
export default BasicSettings;
import React from 'react';
import { shallow } from 'enzyme';
import ButtonRow, { Props } from './ButtonRow';
const setup = (propOverrides?: object) => {
const props: Props = {
isReadOnly: true,
onSubmit: jest.fn(),
onDelete: jest.fn(),
};
Object.assign(props, propOverrides);
return shallow(<ButtonRow {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render with buttons enabled', () => {
const wrapper = setup({
isReadOnly: false,
});
expect(wrapper).toMatchSnapshot();
});
});
import React, { SFC } from 'react';
export interface Props {
isReadOnly: boolean;
onDelete: () => void;
onSubmit: (event) => void;
}
const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
return (
<div className="gf-form-button-row">
<button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>
Save &amp; Test
</button>
<button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
Delete
</button>
<a className="btn btn-inverse" href="/datasources">
Back
</a>
</div>
);
};
export default ButtonRow;
import React from 'react';
import { shallow } from 'enzyme';
import { DataSourceSettings, Props } from './DataSourceSettings';
import { DataSource, NavModel } from '../../../types';
import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
dataSource: getMockDataSource(),
dataSourceMeta: getMockPlugin(),
pageId: 1,
deleteDataSource: jest.fn(),
loadDataSource: jest.fn(),
setDataSourceName: jest.fn(),
updateDataSource: jest.fn(),
};
Object.assign(props, propOverrides);
return shallow(<DataSourceSettings {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render loader', () => {
const wrapper = setup({
dataSource: {} as DataSource,
});
expect(wrapper).toMatchSnapshot();
});
it('should render beta info text', () => {
const wrapper = setup({
dataSourceMeta: { ...getMockPlugin(), state: 'beta' },
});
expect(wrapper).toMatchSnapshot();
});
it('should render alpha info text', () => {
const wrapper = setup({
dataSourceMeta: { ...getMockPlugin(), state: 'alpha' },
});
expect(wrapper).toMatchSnapshot();
});
it('should render is ready only message', () => {
const wrapper = setup({
dataSource: { ...getMockDataSource(), readOnly: true },
});
expect(wrapper).toMatchSnapshot();
});
});
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import PluginSettings from './PluginSettings';
import BasicSettings from './BasicSettings';
import ButtonRow from './ButtonRow';
import appEvents from 'app/core/app_events';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getDataSource, getDataSourceMeta } from '../state/selectors';
import { deleteDataSource, loadDataSource, setDataSourceName, updateDataSource } from '../state/actions';
import { getNavModel } from 'app/core/selectors/navModel';
import { getRouteParamsId } from 'app/core/selectors/location';
import { DataSource, NavModel, Plugin } from 'app/types/';
import { getDataSourceLoadingNav } from '../state/navModel';
export interface Props {
navModel: NavModel;
dataSource: DataSource;
dataSourceMeta: Plugin;
pageId: number;
deleteDataSource: typeof deleteDataSource;
loadDataSource: typeof loadDataSource;
setDataSourceName: typeof setDataSourceName;
updateDataSource: typeof updateDataSource;
}
interface State {
dataSource: DataSource;
isTesting?: boolean;
testingMessage?: string;
testingStatus?: string;
}
enum DataSourceStates {
Alpha = 'alpha',
Beta = 'beta',
}
export class DataSourceSettings extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
dataSource: {} as DataSource,
};
}
async componentDidMount() {
const { loadDataSource, pageId } = this.props;
await loadDataSource(pageId);
}
onSubmit = async event => {
event.preventDefault();
await this.props.updateDataSource({ ...this.state.dataSource, name: this.props.dataSource.name });
this.testDataSource();
};
onDelete = () => {
appEvents.emit('confirm-modal', {
title: 'Delete',
text: 'Are you sure you want to delete this data source?',
yesText: 'Delete',
icon: 'fa-trash',
onConfirm: () => {
this.confirmDelete();
},
});
};
confirmDelete = () => {
this.props.deleteDataSource();
};
onModelChange = dataSource => {
this.setState({
dataSource: dataSource,
});
};
isReadOnly() {
return this.props.dataSource.readOnly === true;
}
shouldRenderInfoBox() {
const { state } = this.props.dataSourceMeta;
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
}
getInfoText() {
const { dataSourceMeta } = this.props;
switch (dataSourceMeta.state) {
case DataSourceStates.Alpha:
return (
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
' will include breaking changes.'
);
case DataSourceStates.Beta:
return (
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
' development and could be missing important features.'
);
}
return null;
}
renderIsReadOnlyMessage() {
return (
<div className="grafana-info-box span8">
This datasource was added by config and cannot be modified using the UI. Please contact your server admin to
update this datasource.
</div>
);
}
async testDataSource() {
const dsApi = await getDatasourceSrv().get(this.state.dataSource.name);
if (!dsApi.testDatasource) {
return;
}
this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
getBackendSrv().withNoBackendCache(async () => {
try {
const result = await dsApi.testDatasource();
this.setState({
isTesting: false,
testingStatus: result.status,
testingMessage: result.message,
});
} catch (err) {
let message = '';
if (err.statusText) {
message = 'HTTP Error ' + err.statusText;
} else {
message = err.message;
}
this.setState({
isTesting: false,
testingStatus: 'error',
testingMessage: message,
});
}
});
}
render() {
const { dataSource, dataSourceMeta, navModel } = this.props;
const { testingMessage, testingStatus } = this.state;
return (
<div>
<PageHeader model={navModel} />
{Object.keys(dataSource).length === 0 ? (
<PageLoader pageName="Data source settings" />
) : (
<div className="page-container page-body">
<div>
<form onSubmit={this.onSubmit}>
<BasicSettings
dataSourceName={this.props.dataSource.name}
onChange={name => this.props.setDataSourceName(name)}
/>
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
{this.isReadOnly() && this.renderIsReadOnlyMessage()}
{dataSourceMeta.module && (
<PluginSettings
dataSource={dataSource}
dataSourceMeta={dataSourceMeta}
onModelChange={this.onModelChange}
/>
)}
<div className="gf-form-group section">
{testingMessage && (
<div className={`alert-${testingStatus} alert`}>
<div className="alert-icon">
{testingStatus === 'error' ? (
<i className="fa fa-exclamation-triangle" />
) : (
<i className="fa fa-check" />
)}
</div>
<div className="alert-body">
<div className="alert-title">{testingMessage}</div>
</div>
</div>
)}
</div>
<ButtonRow
onSubmit={event => this.onSubmit(event)}
isReadOnly={this.isReadOnly()}
onDelete={this.onDelete}
/>
</form>
</div>
</div>
)}
</div>
);
}
}
function mapStateToProps(state) {
const pageId = getRouteParamsId(state.location);
const dataSource = getDataSource(state.dataSources, pageId);
return {
navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')),
dataSource: getDataSource(state.dataSources, pageId),
dataSourceMeta: getDataSourceMeta(state.dataSources, dataSource.type),
pageId: pageId,
};
}
const mapDispatchToProps = {
deleteDataSource,
loadDataSource,
setDataSourceName,
updateDataSource,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettings));
import React, { PureComponent } from 'react';
import _ from 'lodash';
import { DataSource, Plugin } from 'app/types/';
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
export interface Props {
dataSource: DataSource;
dataSourceMeta: Plugin;
onModelChange: (dataSource: DataSource) => void;
}
export class PluginSettings extends PureComponent<Props> {
element: any;
component: AngularComponent;
scopeProps: {
ctrl: { datasourceMeta: Plugin; current: DataSource };
onModelChanged: (dataSource: DataSource) => void;
};
constructor(props) {
super(props);
this.scopeProps = {
ctrl: { datasourceMeta: props.dataSourceMeta, current: _.cloneDeep(props.dataSource) },
onModelChanged: this.onModelChanged,
};
}
componentDidMount() {
if (!this.element) {
return;
}
const loader = getAngularLoader();
const template = '<plugin-component type="datasource-config-ctrl" />';
this.component = loader.load(this.element, this.scopeProps, template);
}
componentDidUpdate(prevProps) {
if (this.props.dataSource !== prevProps.dataSource) {
this.scopeProps.ctrl.current = _.cloneDeep(this.props.dataSource);
this.component.digest();
}
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();
}
}
onModelChanged = (dataSource: DataSource) => {
this.props.onModelChange(dataSource);
};
render() {
return <div ref={element => (this.element = element)} />;
}
}
export default PluginSettings;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="gf-form-group"
>
<div
className="gf-form max-width-30"
>
<Component
tooltip="The name is used when you select the data source in panels. The Default data source ispreselected in new panels."
>
Name
</Component>
<input
className="gf-form-input max-width-23"
onChange={[Function]}
placeholder="Name"
required={true}
type="text"
value="Graphite"
/>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="gf-form-button-row"
>
<button
className="btn btn-success"
disabled={true}
onClick={[Function]}
type="submit"
>
Save & Test
</button>
<button
className="btn btn-danger"
disabled={true}
onClick={[MockFunction]}
type="submit"
>
Delete
</button>
<a
className="btn btn-inverse"
href="/datasources"
>
Back
</a>
</div>
`;
exports[`Render should render with buttons enabled 1`] = `
<div
className="gf-form-button-row"
>
<button
className="btn btn-success"
disabled={false}
onClick={[Function]}
type="submit"
>
Save & Test
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[MockFunction]}
type="submit"
>
Delete
</button>
<a
className="btn btn-inverse"
href="/datasources"
>
Back
</a>
</div>
`;
import { ThunkAction } from 'redux-thunk';
import { DataSource, Plugin, StoreState } from 'app/types';
import { getBackendSrv } from '../../../core/services/backend_srv';
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions';
import { UpdateLocationAction } from '../../../core/actions/location';
import config from '../../../core/config';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
import { UpdateLocationAction } from 'app/core/actions/location';
import { buildNavModel } from './navModel';
import { DataSource, Plugin, StoreState } from 'app/types';
export enum ActionTypes {
LoadDataSources = 'LOAD_DATA_SOURCES',
......@@ -14,43 +16,49 @@ export enum ActionTypes {
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
SetDataSourceName = 'SET_DATA_SOURCE_NAME',
}
export interface LoadDataSourcesAction {
interface LoadDataSourcesAction {
type: ActionTypes.LoadDataSources;
payload: DataSource[];
}
export interface SetDataSourcesSearchQueryAction {
interface SetDataSourcesSearchQueryAction {
type: ActionTypes.SetDataSourcesSearchQuery;
payload: string;
}
export interface SetDataSourcesLayoutModeAction {
interface SetDataSourcesLayoutModeAction {
type: ActionTypes.SetDataSourcesLayoutMode;
payload: LayoutMode;
}
export interface LoadDataSourceTypesAction {
interface LoadDataSourceTypesAction {
type: ActionTypes.LoadDataSourceTypes;
payload: Plugin[];
}
export interface SetDataSourceTypeSearchQueryAction {
interface SetDataSourceTypeSearchQueryAction {
type: ActionTypes.SetDataSourceTypeSearchQuery;
payload: string;
}
export interface LoadDataSourceAction {
interface LoadDataSourceAction {
type: ActionTypes.LoadDataSource;
payload: DataSource;
}
export interface LoadDataSourceMetaAction {
interface LoadDataSourceMetaAction {
type: ActionTypes.LoadDataSourceMeta;
payload: Plugin;
}
interface SetDataSourceNameAction {
type: ActionTypes.SetDataSourceName;
payload: string;
}
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
type: ActionTypes.LoadDataSources,
payload: dataSources,
......@@ -86,6 +94,11 @@ export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSe
payload: query,
});
export const setDataSourceName = (name: string) => ({
type: ActionTypes.SetDataSourceName,
payload: name,
});
export type Action =
| LoadDataSourcesAction
| SetDataSourcesSearchQueryAction
......@@ -95,7 +108,8 @@ export type Action =
| SetDataSourceTypeSearchQueryAction
| LoadDataSourceAction
| UpdateNavIndexAction
| LoadDataSourceMetaAction;
| LoadDataSourceMetaAction
| SetDataSourceNameAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
......@@ -145,6 +159,23 @@ export function loadDataSourceTypes(): ThunkResult<void> {
};
}
export function updateDataSource(dataSource: DataSource): ThunkResult<void> {
return async dispatch => {
await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource);
await updateFrontendSettings();
return dispatch(loadDataSource(dataSource.id));
};
}
export function deleteDataSource(): ThunkResult<void> {
return async (dispatch, getStore) => {
const dataSource = getStore().dataSources.dataSource;
await getBackendSrv().delete(`/api/datasources/${dataSource.id}`);
dispatch(updateLocation({ path: '/datasources' }));
};
}
export function nameExits(dataSources, name) {
return (
dataSources.filter(dataSource => {
......@@ -173,6 +204,16 @@ export function findNewName(dataSources, name) {
return name;
}
function updateFrontendSettings() {
return getBackendSrv()
.get('/api/frontend/settings')
.then(settings => {
config.datasources = settings.datasources;
config.defaultDatasource = settings.defaultDatasource;
getDatasourceSrv().init();
});
}
function nameHasSuffix(name) {
return name.endsWith('-', name.length - 1);
}
......
......@@ -48,6 +48,9 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
{
access: '',
basicAuth: false,
basicAuthUser: '',
basicAuthPassword: '',
withCredentials: false,
database: '',
id: 1,
isDefault: false,
......
......@@ -10,8 +10,8 @@ const initialState: DataSourcesState = {
dataSourcesCount: 0,
dataSourceTypes: [] as Plugin[],
dataSourceTypeSearchQuery: '',
dataSourceMeta: {} as Plugin,
hasFetched: false,
dataSourceMeta: {} as Plugin,
};
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
......@@ -36,6 +36,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
case ActionTypes.LoadDataSourceMeta:
return { ...state, dataSourceMeta: action.payload };
case ActionTypes.SetDataSourceName:
return { ...state, dataSource: { ...state.dataSource, name: action.payload } };
}
return state;
......
......@@ -20,7 +20,15 @@ export const getDataSource = (state, dataSourceId): DataSource | null => {
if (state.dataSource.id === parseInt(dataSourceId, 10)) {
return state.dataSource;
}
return null;
return {} as DataSource;
};
export const getDataSourceMeta = (state, type): Plugin => {
if (state.dataSourceMeta.id === type) {
return state.dataSourceMeta;
}
return {} as Plugin;
};
export const getDataSourcesSearchQuery = state => state.searchQuery;
......
......@@ -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: {},
};
};
......@@ -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": "",
......
import './plugin_edit_ctrl';
import './plugin_page_ctrl';
import './import_list/import_list';
import './ds_edit_ctrl';
......
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';
......
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';
......
<page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body">
<h3 class="page-sub-heading">Settings</h3>
<form name="ctrl.editForm" ng-if="ctrl.current">
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-10">Name</span>
<input class="gf-form-input max-width-23" type="text" ng-model="ctrl.current.name" placeholder="name" required>
<info-popover offset="0px -135px" mode="right-absolute">
The name is used when you select the data source in panels.
The <em>Default</em> data source is preselected in new
panels.
</info-popover>
</div>
<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
</div>
</div>
<div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'alpha'">
This plugin is marked as being in alpha state, which means it is in early development phase and
updates will include breaking changes.
</div>
<div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'beta'">
This plugin is marked as being in a beta development state. This means it is in currently in active development and could be
missing important features.
</div>
<rebuild-on-change property="ctrl.datasourceMeta.id">
<plugin-component type="datasource-config-ctrl">
</plugin-component>
</rebuild-on-change>
<div ng-if="ctrl.hasDashboards">
<h3 class="section-heading">Bundled Plugin Dashboards</h3>
<div class="section">
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
</div>
</div>
<div ng-if="ctrl.testing" class="gf-form-group section">
<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
<div class="alert-{{ctrl.testing.status}} alert" ng-show="ctrl.testing.done">
<div class="alert-icon">
<i class="fa fa-exclamation-triangle" ng-show="ctrl.testing.status === 'error'"></i>
<i class="fa fa-check" ng-show="ctrl.testing.status !== 'error'"></i>
</div>
<div class="alert-body">
<div class="alert-title">{{ctrl.testing.message}}</div>
</div>
</div>
</div>
<div class="grafana-info-box span8" ng-if="ctrl.current.readOnly">
This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-success" ng-disabled="ctrl.current.readOnly" ng-click="ctrl.saveChanges()">Save &amp; Test</button>
<button type="submit" class="btn btn-danger" ng-disabled="ctrl.current.readOnly" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">Delete</button>
<a class="btn btn-inverse" href="datasources">Back</a>
</div>
<br />
<br />
<br />
</form>
</div>
......@@ -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,
......
import angular from 'angular';
import _ from 'lodash';
import Remarkable from 'remarkable';
export class PluginEditCtrl {
model: any;
pluginIcon: string;
pluginId: any;
includes: any;
readmeHtml: any;
includedDatasources: any;
tab: string;
navModel: any;
hasDashboards: any;
preUpdateHook: () => any;
postUpdateHook: () => any;
/** @ngInject */
constructor(private $scope, private $rootScope, private backendSrv, private $sce, private $routeParams, navModelSrv) {
this.pluginId = $routeParams.pluginId;
this.preUpdateHook = () => Promise.resolve();
this.postUpdateHook = () => Promise.resolve();
this.init();
}
setNavModel(model) {
let defaultTab = 'readme';
this.navModel = {
main: {
img: model.info.logos.large,
subTitle: model.info.author.name,
url: '',
text: model.name,
breadcrumbs: [{ title: 'Plugins', url: 'plugins' }],
children: [
{
icon: 'fa fa-fw fa-file-text-o',
id: 'readme',
text: 'Readme',
url: `plugins/${this.model.id}/edit?tab=readme`,
},
],
},
};
if (model.type === 'app') {
this.navModel.main.children.push({
icon: 'gicon gicon-cog',
id: 'config',
text: 'Config',
url: `plugins/${this.model.id}/edit?tab=config`,
});
const hasDashboards = _.find(model.includes, { type: 'dashboard' });
if (hasDashboards) {
this.navModel.main.children.push({
icon: 'gicon gicon-dashboard',
id: 'dashboards',
text: 'Dashboards',
url: `plugins/${this.model.id}/edit?tab=dashboards`,
});
}
defaultTab = 'config';
}
this.tab = this.$routeParams.tab || defaultTab;
for (const tab of this.navModel.main.children) {
if (tab.id === this.tab) {
tab.active = true;
}
}
}
init() {
return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => {
this.model = result;
this.pluginIcon = this.getPluginIcon(this.model.type);
this.model.dependencies.plugins.forEach(plug => {
plug.icon = this.getPluginIcon(plug.type);
});
this.includes = _.map(result.includes, plug => {
plug.icon = this.getPluginIcon(plug.type);
return plug;
});
this.setNavModel(this.model);
return this.initReadme();
});
}
initReadme() {
return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
const md = new Remarkable({
linkify: true,
});
this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
});
}
getPluginIcon(type) {
switch (type) {
case 'datasource':
return 'icon-gf icon-gf-datasources';
case 'panel':
return 'icon-gf icon-gf-panel';
case 'app':
return 'icon-gf icon-gf-apps';
case 'page':
return 'icon-gf icon-gf-endpoint-tiny';
case 'dashboard':
return 'icon-gf icon-gf-dashboard';
default:
return 'icon-gf icon-gf-apps';
}
}
update() {
this.preUpdateHook()
.then(() => {
const updateCmd = _.extend(
{
enabled: this.model.enabled,
pinned: this.model.pinned,
jsonData: this.model.jsonData,
secureJsonData: this.model.secureJsonData,
},
{}
);
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
})
.then(this.postUpdateHook)
.then(res => {
window.location.href = window.location.href;
});
}
importDashboards() {
return Promise.resolve();
}
setPreUpdateHook(callback: () => any) {
this.preUpdateHook = callback;
}
setPostUpdateHook(callback: () => any) {
this.postUpdateHook = callback;
}
updateAvailable() {
const modalScope = this.$scope.$new(true);
modalScope.plugin = this.model;
this.$rootScope.appEvent('show-modal', {
src: 'public/app/features/plugins/partials/update_instructions.html',
scope: modalScope,
});
}
enable() {
this.model.enabled = true;
this.model.pinned = true;
this.update();
}
disable() {
this.model.enabled = false;
this.model.pinned = false;
this.update();
}
}
angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);
......@@ -362,14 +362,9 @@ export default class CloudWatchDatasource {
const metricName = 'EstimatedCharges';
const dimensions = {};
return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(
() => {
return { status: 'success', message: 'Data source is working' };
},
err => {
return { status: 'error', message: err.message };
}
);
return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(() => {
return { status: 'success', message: 'Data source is working' };
});
}
awsRequest(url, data) {
......
......@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import coreModule from 'app/core/core_module';
import { store } from 'app/store/configureStore';
import { store } from 'app/store/store';
import { BackendSrv } from 'app/core/services/backend_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ContextSrv } from 'app/core/services/context_srv';
......
......@@ -14,6 +14,7 @@ import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
import UsersListPage from 'app/features/users/UsersListPage';
import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
import DataSourceSettings from '../features/datasources/settings/DataSourceSettings';
import OrgDetailsPage from '../features/org/OrgDetailsPage';
/** @ngInject */
......@@ -74,10 +75,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
component: () => DataSourcesListPage,
},
})
.when('/datasources/edit/:id', {
templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
controller: 'DataSourceEditCtrl',
controllerAs: 'ctrl',
.when('/datasources/edit/:id/', {
template: '<react-container />',
resolve: {
component: () => DataSourceSettings,
},
})
.when('/datasources/edit/:id/dashboards', {
template: '<react-container />',
......
......@@ -11,6 +11,7 @@ import pluginReducers from 'app/features/plugins/state/reducers';
import dataSourcesReducers from 'app/features/datasources/state/reducers';
import usersReducers from 'app/features/users/state/reducers';
import organizationReducers from 'app/features/org/state/reducers';
import { setStore } from './store';
const rootReducers = {
...sharedReducers,
......@@ -25,8 +26,6 @@ const rootReducers = {
...organizationReducers,
};
export let store;
export function addRootReducer(reducers) {
Object.assign(rootReducers, ...reducers);
}
......@@ -38,8 +37,8 @@ export function configureStore() {
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
} else {
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)));
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
}
}
export let store;
export function setStore(newStore) {
store = newStore;
}
......@@ -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;
......
......@@ -73,6 +73,7 @@ export interface Plugin {
pinned: boolean;
state: string;
type: string;
module: any;
}
export interface PluginDashboard {
......
......@@ -88,4 +88,5 @@ export interface DataQueryOptions {
export interface DataSourceApi {
query(options: DataQueryOptions): Promise<DataQueryResponse>;
testDatasource(): Promise<any>;
}
......@@ -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'),
},
}),
]
],
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment