NewDataSourcePage.tsx 7.5 KB

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