NewDataSourcePage.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import React, { PureComponent, FC } from 'react';
  2. import { connect } from 'react-redux';
  3. import { hot } from 'react-hot-loader';
  4. import Page from 'app/core/components/Page/Page';
  5. import { StoreState } from 'app/types';
  6. import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
  7. import { getDataSourceTypes } from './state/selectors';
  8. import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
  9. import { DataSourcePluginMeta, List, PluginType } from '@grafana/ui';
  10. import { NavModel } from '@grafana/data';
  11. export interface Props {
  12. navModel: NavModel;
  13. dataSourceTypes: DataSourcePluginMeta[];
  14. isLoading: boolean;
  15. addDataSource: typeof addDataSource;
  16. loadDataSourceTypes: typeof loadDataSourceTypes;
  17. searchQuery: string;
  18. setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
  19. }
  20. interface DataSourceCategories {
  21. [key: string]: DataSourcePluginMeta[];
  22. }
  23. interface DataSourceCategoryInfo {
  24. id: string;
  25. title: string;
  26. }
  27. class NewDataSourcePage extends PureComponent<Props> {
  28. searchInput: HTMLElement;
  29. categoryInfoList: DataSourceCategoryInfo[] = [
  30. { id: 'tsdb', title: 'Time series databases' },
  31. { id: 'logging', title: 'Logging & document databases' },
  32. { id: 'sql', title: 'SQL' },
  33. { id: 'cloud', title: 'Cloud' },
  34. { id: 'other', title: 'Others' },
  35. ];
  36. sortingRules: { [id: string]: number } = {
  37. prometheus: 100,
  38. graphite: 95,
  39. loki: 90,
  40. mysql: 80,
  41. postgres: 79,
  42. gcloud: -1,
  43. };
  44. componentDidMount() {
  45. this.props.loadDataSourceTypes();
  46. this.searchInput.focus();
  47. }
  48. onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => {
  49. this.props.addDataSource(plugin);
  50. };
  51. onSearchQueryChange = (value: string) => {
  52. this.props.setDataSourceTypeSearchQuery(value);
  53. };
  54. renderTypes(types: DataSourcePluginMeta[]) {
  55. if (!types) {
  56. return null;
  57. }
  58. // apply custom sort ranking
  59. types.sort((a, b) => {
  60. const aSort = this.sortingRules[a.id] || 0;
  61. const bSort = this.sortingRules[b.id] || 0;
  62. if (aSort > bSort) {
  63. return -1;
  64. }
  65. if (aSort < bSort) {
  66. return 1;
  67. }
  68. return a.name > b.name ? -1 : 1;
  69. });
  70. return (
  71. <List
  72. items={types}
  73. getItemKey={item => item.id.toString()}
  74. renderItem={item => (
  75. <DataSourceTypeCard
  76. plugin={item}
  77. onClick={() => this.onDataSourceTypeClicked(item)}
  78. onLearnMoreClick={this.onLearnMoreClick}
  79. />
  80. )}
  81. />
  82. );
  83. }
  84. onLearnMoreClick = (evt: React.SyntheticEvent<HTMLElement>) => {
  85. evt.stopPropagation();
  86. };
  87. renderGroupedList() {
  88. const { dataSourceTypes } = this.props;
  89. if (dataSourceTypes.length === 0) {
  90. return null;
  91. }
  92. const categories = dataSourceTypes.reduce(
  93. (accumulator, item) => {
  94. const category = item.category || 'other';
  95. const list = accumulator[category] || [];
  96. list.push(item);
  97. accumulator[category] = list;
  98. return accumulator;
  99. },
  100. {} as DataSourceCategories
  101. );
  102. categories['cloud'].push(getGrafanaCloudPhantomPlugin());
  103. return (
  104. <>
  105. {this.categoryInfoList.map(category => (
  106. <div className="add-data-source-category" key={category.id}>
  107. <div className="add-data-source-category__header">{category.title}</div>
  108. {this.renderTypes(categories[category.id])}
  109. </div>
  110. ))}
  111. <div className="add-data-source-more">
  112. <a
  113. className="btn btn-inverse"
  114. href="https://grafana.com/plugins?type=datasource&utm_source=new-data-source"
  115. target="_blank"
  116. rel="noopener"
  117. >
  118. Find more data source plugins on grafana.com
  119. </a>
  120. </div>
  121. </>
  122. );
  123. }
  124. render() {
  125. const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props;
  126. return (
  127. <Page navModel={navModel}>
  128. <Page.Contents isLoading={isLoading}>
  129. <div className="page-action-bar">
  130. <div className="gf-form gf-form--grow">
  131. <FilterInput
  132. ref={elem => (this.searchInput = elem)}
  133. labelClassName="gf-form--has-input-icon"
  134. inputClassName="gf-form-input width-30"
  135. value={searchQuery}
  136. onChange={this.onSearchQueryChange}
  137. placeholder="Filter by name or type"
  138. />
  139. </div>
  140. <div className="page-action-bar__spacer" />
  141. <a className="btn btn-secondary" href="datasources">
  142. Cancel
  143. </a>
  144. </div>
  145. <div>
  146. {searchQuery && this.renderTypes(dataSourceTypes)}
  147. {!searchQuery && this.renderGroupedList()}
  148. </div>
  149. </Page.Contents>
  150. </Page>
  151. );
  152. }
  153. }
  154. interface DataSourceTypeCardProps {
  155. plugin: DataSourcePluginMeta;
  156. onClick: () => void;
  157. onLearnMoreClick: (evt: React.SyntheticEvent<HTMLElement>) => void;
  158. }
  159. const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
  160. const { plugin, onLearnMoreClick } = props;
  161. const canSelect = plugin.id !== 'gcloud';
  162. const onClick = canSelect ? props.onClick : () => {};
  163. // find first plugin info link
  164. const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0].url : null;
  165. return (
  166. <div className="add-data-source-item" onClick={onClick} aria-label={`${plugin.name} datasource plugin`}>
  167. <img className="add-data-source-item-logo" src={plugin.info.logos.small} />
  168. <div className="add-data-source-item-text-wrapper">
  169. <span className="add-data-source-item-text">{plugin.name}</span>
  170. {plugin.info.description && <span className="add-data-source-item-desc">{plugin.info.description}</span>}
  171. </div>
  172. <div className="add-data-source-item-actions">
  173. {learnMoreLink && (
  174. <a
  175. className="btn btn-inverse"
  176. href={`${learnMoreLink}?utm_source=grafana_add_ds`}
  177. target="_blank"
  178. rel="noopener"
  179. onClick={onLearnMoreClick}
  180. >
  181. Learn more <i className="fa fa-external-link add-datasource-item-actions__btn-icon" />
  182. </a>
  183. )}
  184. {canSelect && <button className="btn btn-primary">Select</button>}
  185. </div>
  186. </div>
  187. );
  188. };
  189. function getGrafanaCloudPhantomPlugin(): DataSourcePluginMeta {
  190. return {
  191. id: 'gcloud',
  192. name: 'Grafana Cloud',
  193. type: PluginType.datasource,
  194. module: '',
  195. baseUrl: '',
  196. info: {
  197. description: 'Hosted Graphite, Prometheus and Loki',
  198. logos: { small: 'public/img/grafana_icon.svg', large: 'asd' },
  199. author: { name: 'Grafana Labs' },
  200. links: [
  201. {
  202. url: 'https://grafana.com/cloud',
  203. name: 'Learn more',
  204. },
  205. ],
  206. screenshots: [],
  207. updated: '2019-05-10',
  208. version: '1.0.0',
  209. },
  210. };
  211. }
  212. export function getNavModel(): NavModel {
  213. const main = {
  214. icon: 'gicon gicon-add-datasources',
  215. id: 'datasource-new',
  216. text: 'Add data source',
  217. href: 'datasources/new',
  218. subTitle: 'Choose a data source type',
  219. };
  220. return {
  221. main: main,
  222. node: main,
  223. };
  224. }
  225. function mapStateToProps(state: StoreState) {
  226. return {
  227. navModel: getNavModel(),
  228. dataSourceTypes: getDataSourceTypes(state.dataSources),
  229. searchQuery: state.dataSources.dataSourceTypeSearchQuery,
  230. isLoading: state.dataSources.isLoadingDataSources,
  231. };
  232. }
  233. const mapDispatchToProps = {
  234. addDataSource,
  235. loadDataSourceTypes,
  236. setDataSourceTypeSearchQuery,
  237. };
  238. export default hot(module)(
  239. connect(
  240. mapStateToProps,
  241. mapDispatchToProps
  242. )(NewDataSourcePage)
  243. );