NewDataSourcePage.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 } 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. };
  42. componentDidMount() {
  43. this.props.loadDataSourceTypes();
  44. this.searchInput.focus();
  45. }
  46. onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => {
  47. this.props.addDataSource(plugin);
  48. };
  49. onSearchQueryChange = (value: string) => {
  50. this.props.setDataSourceTypeSearchQuery(value);
  51. };
  52. renderTypes(types: DataSourcePluginMeta[]) {
  53. if (!types) {
  54. return null;
  55. }
  56. // apply custom sort ranking
  57. types.sort((a, b) => {
  58. const aSort = this.sortingRules[a.id] || 0;
  59. const bSort = this.sortingRules[b.id] || 0;
  60. if (aSort > bSort) {
  61. return -1;
  62. }
  63. if (aSort < bSort) {
  64. return 1;
  65. }
  66. return a.name > b.name ? -1 : 1;
  67. });
  68. return (
  69. <List
  70. items={types}
  71. getItemKey={item => item.id.toString()}
  72. renderItem={item => (
  73. <DataSourceTypeCard
  74. plugin={item}
  75. onClick={() => this.onDataSourceTypeClicked(item)}
  76. onLearnMoreClick={this.onLearnMoreClick}
  77. />
  78. )}
  79. />
  80. );
  81. }
  82. onLearnMoreClick = (evt: React.SyntheticEvent<HTMLElement>) => {
  83. evt.stopPropagation();
  84. };
  85. renderGroupedList() {
  86. const { dataSourceTypes } = this.props;
  87. if (dataSourceTypes.length === 0) {
  88. return null;
  89. }
  90. const categories = dataSourceTypes.reduce(
  91. (accumulator, item) => {
  92. const category = item.category || 'other';
  93. const list = accumulator[category] || [];
  94. list.push(item);
  95. accumulator[category] = list;
  96. return accumulator;
  97. },
  98. {} as DataSourceCategories
  99. );
  100. return (
  101. <>
  102. {this.categoryInfoList.map(category => (
  103. <div className="add-data-source-category" key={category.id}>
  104. <div className="add-data-source-category__header">{category.title}</div>
  105. {this.renderTypes(categories[category.id])}
  106. </div>
  107. ))}
  108. <div className="add-data-source-more">
  109. <a
  110. className="btn btn-inverse"
  111. href="https://grafana.com/plugins?type=datasource&utm_source=new-data-source"
  112. target="_blank"
  113. >
  114. Find more data source plugins on grafana.com
  115. </a>
  116. </div>
  117. </>
  118. );
  119. }
  120. render() {
  121. const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props;
  122. return (
  123. <Page navModel={navModel}>
  124. <Page.Contents isLoading={isLoading}>
  125. <div className="page-action-bar">
  126. <div className="gf-form gf-form--grow">
  127. <FilterInput
  128. ref={elem => (this.searchInput = elem)}
  129. labelClassName="gf-form--has-input-icon"
  130. inputClassName="gf-form-input width-30"
  131. value={searchQuery}
  132. onChange={this.onSearchQueryChange}
  133. placeholder="Filter by name or type"
  134. />
  135. </div>
  136. <div className="page-action-bar__spacer" />
  137. <a className="btn btn-secondary" href="datasources">
  138. Cancel
  139. </a>
  140. </div>
  141. <div>
  142. {searchQuery && this.renderTypes(dataSourceTypes)}
  143. {!searchQuery && this.renderGroupedList()}
  144. </div>
  145. </Page.Contents>
  146. </Page>
  147. );
  148. }
  149. }
  150. interface DataSourceTypeCardProps {
  151. plugin: DataSourcePluginMeta;
  152. onClick: () => void;
  153. onLearnMoreClick: (evt: React.SyntheticEvent<HTMLElement>) => void;
  154. }
  155. const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
  156. const { plugin, onClick, onLearnMoreClick } = props;
  157. // find first plugin info link
  158. const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0].url : null;
  159. return (
  160. <div className="add-data-source-item" onClick={onClick} aria-label={`${plugin.name} datasource plugin`}>
  161. <img className="add-data-source-item-logo" src={plugin.info.logos.small} />
  162. <div className="add-data-source-item-text-wrapper">
  163. <span className="add-data-source-item-text">{plugin.name}</span>
  164. {plugin.info.description && <span className="add-data-source-item-desc">{plugin.info.description}</span>}
  165. </div>
  166. <div className="add-data-source-item-actions">
  167. {learnMoreLink && (
  168. <a className="btn btn-inverse" href={learnMoreLink} target="_blank" onClick={onLearnMoreClick}>
  169. Learn more
  170. </a>
  171. )}
  172. <button className="btn btn-primary">Select</button>
  173. </div>
  174. </div>
  175. );
  176. };
  177. export function getNavModel(): NavModel {
  178. const main = {
  179. icon: 'gicon gicon-add-datasources',
  180. id: 'datasource-new',
  181. text: 'New data source',
  182. href: 'datasources/new',
  183. subTitle: 'Choose a data source type',
  184. };
  185. return {
  186. main: main,
  187. node: main,
  188. };
  189. }
  190. function mapStateToProps(state: StoreState) {
  191. return {
  192. navModel: getNavModel(),
  193. dataSourceTypes: getDataSourceTypes(state.dataSources),
  194. searchQuery: state.dataSources.dataSourceTypeSearchQuery,
  195. isLoading: state.dataSources.isLoadingDataSources,
  196. };
  197. }
  198. const mapDispatchToProps = {
  199. addDataSource,
  200. loadDataSourceTypes,
  201. setDataSourceTypeSearchQuery,
  202. };
  203. export default hot(module)(
  204. connect(
  205. mapStateToProps,
  206. mapDispatchToProps
  207. )(NewDataSourcePage)
  208. );