DataSourceSettingsPage.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import React, { PureComponent } from 'react';
  2. import { connect, ConnectedProps } from 'react-redux';
  3. import { DataSourceSettings, urlUtil } from '@grafana/data';
  4. import { selectors } from '@grafana/e2e-selectors';
  5. import { Alert, Button } from '@grafana/ui';
  6. import { cleanUpAction } from 'app/core/actions/cleanUp';
  7. import appEvents from 'app/core/app_events';
  8. import Page from 'app/core/components/Page/Page';
  9. import { contextSrv } from 'app/core/core';
  10. import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
  11. import { getNavModel } from 'app/core/selectors/navModel';
  12. import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
  13. import { StoreState, AccessControlAction } from 'app/types/';
  14. import { ShowConfirmModalEvent } from '../../../types/events';
  15. import {
  16. deleteDataSource,
  17. initDataSourceSettings,
  18. loadDataSource,
  19. testDataSource,
  20. updateDataSource,
  21. } from '../state/actions';
  22. import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from '../state/navModel';
  23. import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
  24. import { getDataSource, getDataSourceMeta } from '../state/selectors';
  25. import BasicSettings from './BasicSettings';
  26. import ButtonRow from './ButtonRow';
  27. import { CloudInfoBox } from './CloudInfoBox';
  28. import { PluginSettings } from './PluginSettings';
  29. export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
  30. function mapStateToProps(state: StoreState, props: OwnProps) {
  31. const dataSourceId = props.match.params.uid;
  32. const params = new URLSearchParams(props.location.search);
  33. const dataSource = getDataSource(state.dataSources, dataSourceId);
  34. const { plugin, loadError, loading, testingStatus } = state.dataSourceSettings;
  35. const page = params.get('page');
  36. const nav = plugin
  37. ? getDataSourceNav(buildNavModel(dataSource, plugin), page || 'settings')
  38. : getDataSourceLoadingNav('settings');
  39. const navModel = getNavModel(
  40. state.navIndex,
  41. page ? `datasource-page-${page}` : `datasource-settings-${dataSourceId}`,
  42. nav
  43. );
  44. return {
  45. dataSource: getDataSource(state.dataSources, dataSourceId),
  46. dataSourceMeta: getDataSourceMeta(state.dataSources, dataSource.type),
  47. dataSourceId: dataSourceId,
  48. page,
  49. plugin,
  50. loadError,
  51. loading,
  52. testingStatus,
  53. navModel,
  54. };
  55. }
  56. const mapDispatchToProps = {
  57. deleteDataSource,
  58. loadDataSource,
  59. setDataSourceName,
  60. updateDataSource,
  61. setIsDefault,
  62. dataSourceLoaded,
  63. initDataSourceSettings,
  64. testDataSource,
  65. cleanUpAction,
  66. };
  67. const connector = connect(mapStateToProps, mapDispatchToProps);
  68. export type Props = OwnProps & ConnectedProps<typeof connector>;
  69. export class DataSourceSettingsPage extends PureComponent<Props> {
  70. componentDidMount() {
  71. const { initDataSourceSettings, dataSourceId } = this.props;
  72. initDataSourceSettings(dataSourceId);
  73. }
  74. componentWillUnmount() {
  75. this.props.cleanUpAction({
  76. stateSelector: (state) => state.dataSourceSettings,
  77. });
  78. }
  79. onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
  80. evt.preventDefault();
  81. await this.props.updateDataSource({ ...this.props.dataSource });
  82. this.testDataSource();
  83. };
  84. onTest = async (evt: React.FormEvent<HTMLFormElement>) => {
  85. evt.preventDefault();
  86. this.testDataSource();
  87. };
  88. onDelete = () => {
  89. appEvents.publish(
  90. new ShowConfirmModalEvent({
  91. title: 'Delete',
  92. text: `Are you sure you want to delete the "${this.props.dataSource.name}" data source?`,
  93. yesText: 'Delete',
  94. icon: 'trash-alt',
  95. onConfirm: () => {
  96. this.confirmDelete();
  97. },
  98. })
  99. );
  100. };
  101. confirmDelete = () => {
  102. this.props.deleteDataSource();
  103. };
  104. onModelChange = (dataSource: DataSourceSettings) => {
  105. this.props.dataSourceLoaded(dataSource);
  106. };
  107. isReadOnly() {
  108. return this.props.dataSource.readOnly === true;
  109. }
  110. renderIsReadOnlyMessage() {
  111. return (
  112. <Alert aria-label={selectors.pages.DataSource.readOnly} severity="info" title="Provisioned data source">
  113. This data source was added by config and cannot be modified using the UI. Please contact your server admin to
  114. update this data source.
  115. </Alert>
  116. );
  117. }
  118. renderMissingEditRightsMessage() {
  119. return (
  120. <Alert severity="info" title="Missing rights">
  121. You are not allowed to modify this data source. Please contact your server admin to update this data source.
  122. </Alert>
  123. );
  124. }
  125. testDataSource() {
  126. const { dataSource, testDataSource } = this.props;
  127. testDataSource(dataSource.name);
  128. }
  129. get hasDataSource() {
  130. return this.props.dataSource.id > 0;
  131. }
  132. onNavigateToExplore() {
  133. const { dataSource } = this.props;
  134. const exploreState = JSON.stringify({ datasource: dataSource.name, context: 'explore' });
  135. const url = urlUtil.renderUrl('/explore', { left: exploreState });
  136. return url;
  137. }
  138. renderLoadError() {
  139. const { loadError, dataSource } = this.props;
  140. const canDeleteDataSource =
  141. !this.isReadOnly() && contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesDelete, dataSource);
  142. const node = {
  143. text: loadError!,
  144. subTitle: 'Data Source Error',
  145. icon: 'exclamation-triangle',
  146. };
  147. const nav = {
  148. node: node,
  149. main: node,
  150. };
  151. return (
  152. <Page navModel={nav}>
  153. <Page.Contents isLoading={this.props.loading}>
  154. {this.isReadOnly() && this.renderIsReadOnlyMessage()}
  155. <div className="gf-form-button-row">
  156. {canDeleteDataSource && (
  157. <Button type="submit" variant="destructive" onClick={this.onDelete}>
  158. Delete
  159. </Button>
  160. )}
  161. <Button variant="secondary" fill="outline" type="button" onClick={() => history.back()}>
  162. Back
  163. </Button>
  164. </div>
  165. </Page.Contents>
  166. </Page>
  167. );
  168. }
  169. renderConfigPageBody(page: string) {
  170. const { plugin } = this.props;
  171. if (!plugin || !plugin.configPages) {
  172. return null; // still loading
  173. }
  174. for (const p of plugin.configPages) {
  175. if (p.id === page) {
  176. // Investigate is any plugins using this? We should change this interface
  177. return <p.body plugin={plugin} query={{}} />;
  178. }
  179. }
  180. return <div>Page not found: {page}</div>;
  181. }
  182. renderAlertDetails() {
  183. const { testingStatus } = this.props;
  184. return (
  185. <>
  186. {testingStatus?.details?.message}
  187. {testingStatus?.details?.verboseMessage ? (
  188. <details style={{ whiteSpace: 'pre-wrap' }}>{testingStatus?.details?.verboseMessage}</details>
  189. ) : null}
  190. </>
  191. );
  192. }
  193. renderSettings() {
  194. const { dataSourceMeta, setDataSourceName, setIsDefault, dataSource, plugin, testingStatus } = this.props;
  195. const canWriteDataSource = contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesWrite, dataSource);
  196. const canDeleteDataSource = contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesDelete, dataSource);
  197. return (
  198. <form onSubmit={this.onSubmit}>
  199. {!canWriteDataSource && this.renderMissingEditRightsMessage()}
  200. {this.isReadOnly() && this.renderIsReadOnlyMessage()}
  201. {dataSourceMeta.state && (
  202. <div className="gf-form">
  203. <label className="gf-form-label width-10">Plugin state</label>
  204. <label className="gf-form-label gf-form-label--transparent">
  205. <PluginStateInfo state={dataSourceMeta.state} />
  206. </label>
  207. </div>
  208. )}
  209. <CloudInfoBox dataSource={dataSource} />
  210. <BasicSettings
  211. dataSourceName={dataSource.name}
  212. isDefault={dataSource.isDefault}
  213. onDefaultChange={(state) => setIsDefault(state)}
  214. onNameChange={(name) => setDataSourceName(name)}
  215. />
  216. {plugin && (
  217. <PluginSettings
  218. plugin={plugin}
  219. dataSource={dataSource}
  220. dataSourceMeta={dataSourceMeta}
  221. onModelChange={this.onModelChange}
  222. />
  223. )}
  224. {testingStatus?.message && (
  225. <div className="gf-form-group p-t-2">
  226. <Alert
  227. severity={testingStatus.status === 'error' ? 'error' : 'success'}
  228. title={testingStatus.message}
  229. aria-label={selectors.pages.DataSource.alert}
  230. >
  231. {testingStatus.details && this.renderAlertDetails()}
  232. </Alert>
  233. </div>
  234. )}
  235. <ButtonRow
  236. onSubmit={(event) => this.onSubmit(event)}
  237. canSave={!this.isReadOnly() && canWriteDataSource}
  238. canDelete={!this.isReadOnly() && canDeleteDataSource}
  239. onDelete={this.onDelete}
  240. onTest={(event) => this.onTest(event)}
  241. exploreUrl={this.onNavigateToExplore()}
  242. />
  243. </form>
  244. );
  245. }
  246. render() {
  247. const { navModel, page, loadError, loading } = this.props;
  248. if (loadError) {
  249. return this.renderLoadError();
  250. }
  251. return (
  252. <Page navModel={navModel}>
  253. <Page.Contents isLoading={loading}>
  254. {this.hasDataSource ? <div>{page ? this.renderConfigPageBody(page) : this.renderSettings()}</div> : null}
  255. </Page.Contents>
  256. </Page>
  257. );
  258. }
  259. }
  260. export default connector(DataSourceSettingsPage);