NewDataSourcePage.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { css, cx } from '@emotion/css';
  2. import React, { FC, PureComponent } from 'react';
  3. import { connect, ConnectedProps } from 'react-redux';
  4. import { DataSourcePluginMeta, GrafanaTheme2, NavModel } from '@grafana/data';
  5. import { selectors } from '@grafana/e2e-selectors';
  6. import { Card, LinkButton, List, PluginSignatureBadge, FilterInput, useStyles2 } from '@grafana/ui';
  7. import Page from 'app/core/components/Page/Page';
  8. import { StoreState } from 'app/types';
  9. import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo';
  10. import { addDataSource, loadDataSourcePlugins } from './state/actions';
  11. import { setDataSourceTypeSearchQuery } from './state/reducers';
  12. import { getDataSourcePlugins } from './state/selectors';
  13. function mapStateToProps(state: StoreState) {
  14. return {
  15. navModel: getNavModel(),
  16. plugins: getDataSourcePlugins(state.dataSources),
  17. searchQuery: state.dataSources.dataSourceTypeSearchQuery,
  18. categories: state.dataSources.categories,
  19. isLoading: state.dataSources.isLoadingDataSources,
  20. };
  21. }
  22. const mapDispatchToProps = {
  23. addDataSource,
  24. loadDataSourcePlugins,
  25. setDataSourceTypeSearchQuery,
  26. };
  27. const connector = connect(mapStateToProps, mapDispatchToProps);
  28. type Props = ConnectedProps<typeof connector>;
  29. class NewDataSourcePage extends PureComponent<Props> {
  30. componentDidMount() {
  31. this.props.loadDataSourcePlugins();
  32. }
  33. onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => {
  34. this.props.addDataSource(plugin);
  35. };
  36. onSearchQueryChange = (value: string) => {
  37. this.props.setDataSourceTypeSearchQuery(value);
  38. };
  39. renderPlugins(plugins: DataSourcePluginMeta[], id?: string) {
  40. if (!plugins || !plugins.length) {
  41. return null;
  42. }
  43. return (
  44. <List
  45. items={plugins}
  46. className={css`
  47. > li {
  48. margin-bottom: 2px;
  49. }
  50. `}
  51. getItemKey={(item) => item.id.toString()}
  52. renderItem={(item) => (
  53. <DataSourceTypeCard
  54. plugin={item}
  55. onClick={() => this.onDataSourceTypeClicked(item)}
  56. onLearnMoreClick={this.onLearnMoreClick}
  57. />
  58. )}
  59. aria-labelledby={id}
  60. />
  61. );
  62. }
  63. onLearnMoreClick = (evt: React.SyntheticEvent<HTMLElement>) => {
  64. evt.stopPropagation();
  65. };
  66. renderCategories() {
  67. const { categories } = this.props;
  68. return (
  69. <>
  70. {categories.map((category) => (
  71. <div className="add-data-source-category" key={category.id}>
  72. <div className="add-data-source-category__header" id={category.id}>
  73. {category.title}
  74. </div>
  75. {this.renderPlugins(category.plugins, category.id)}
  76. </div>
  77. ))}
  78. <div className="add-data-source-more">
  79. <LinkButton
  80. variant="secondary"
  81. href="https://grafana.com/plugins?type=datasource&utm_source=grafana_add_ds"
  82. target="_blank"
  83. rel="noopener"
  84. >
  85. Find more data source plugins on grafana.com
  86. </LinkButton>
  87. </div>
  88. </>
  89. );
  90. }
  91. render() {
  92. const { navModel, isLoading, searchQuery, plugins } = this.props;
  93. return (
  94. <Page navModel={navModel}>
  95. <Page.Contents isLoading={isLoading}>
  96. <div className="page-action-bar">
  97. <FilterInput value={searchQuery} onChange={this.onSearchQueryChange} placeholder="Filter by name or type" />
  98. <div className="page-action-bar__spacer" />
  99. <LinkButton href="datasources" fill="outline" variant="secondary" icon="arrow-left">
  100. Cancel
  101. </LinkButton>
  102. </div>
  103. {!searchQuery && <PluginsErrorsInfo />}
  104. <div>
  105. {searchQuery && this.renderPlugins(plugins)}
  106. {!searchQuery && this.renderCategories()}
  107. </div>
  108. </Page.Contents>
  109. </Page>
  110. );
  111. }
  112. }
  113. interface DataSourceTypeCardProps {
  114. plugin: DataSourcePluginMeta;
  115. onClick: () => void;
  116. onLearnMoreClick: (evt: React.SyntheticEvent<HTMLElement>) => void;
  117. }
  118. const DataSourceTypeCard: FC<DataSourceTypeCardProps> = (props) => {
  119. const { plugin, onLearnMoreClick } = props;
  120. const isPhantom = plugin.module === 'phantom';
  121. const onClick = !isPhantom && !plugin.unlicensed ? props.onClick : () => {};
  122. // find first plugin info link
  123. const learnMoreLink = plugin.info?.links?.length > 0 ? plugin.info.links[0] : null;
  124. const styles = useStyles2(getStyles);
  125. return (
  126. <Card className={cx(styles.card, 'card-parent')} onClick={onClick}>
  127. <Card.Heading
  128. className={styles.heading}
  129. aria-label={selectors.pages.AddDataSource.dataSourcePluginsV2(plugin.name)}
  130. >
  131. {plugin.name}
  132. </Card.Heading>
  133. <Card.Figure align="center" className={styles.figure}>
  134. <img className={styles.logo} src={plugin.info.logos.small} alt="" />
  135. </Card.Figure>
  136. <Card.Description className={styles.description}>{plugin.info.description}</Card.Description>
  137. {!isPhantom && (
  138. <Card.Meta className={styles.meta}>
  139. <PluginSignatureBadge status={plugin.signature} />
  140. </Card.Meta>
  141. )}
  142. <Card.Actions className={styles.actions}>
  143. {learnMoreLink && (
  144. <LinkButton
  145. variant="secondary"
  146. href={`${learnMoreLink.url}?utm_source=grafana_add_ds`}
  147. target="_blank"
  148. rel="noopener"
  149. onClick={onLearnMoreClick}
  150. icon="external-link-alt"
  151. aria-label={`${plugin.name}, learn more.`}
  152. >
  153. {learnMoreLink.name}
  154. </LinkButton>
  155. )}
  156. </Card.Actions>
  157. </Card>
  158. );
  159. };
  160. function getStyles(theme: GrafanaTheme2) {
  161. return {
  162. heading: css({
  163. fontSize: theme.v1.typography.heading.h5,
  164. fontWeight: 'inherit',
  165. }),
  166. figure: css({
  167. width: 'inherit',
  168. marginRight: '0px',
  169. '> img': {
  170. width: theme.spacing(7),
  171. },
  172. }),
  173. meta: css({
  174. marginTop: '6px',
  175. position: 'relative',
  176. }),
  177. description: css({
  178. margin: '0px',
  179. fontSize: theme.typography.size.sm,
  180. }),
  181. actions: css({
  182. position: 'relative',
  183. alignSelf: 'center',
  184. marginTop: '0px',
  185. opacity: 0,
  186. '.card-parent:hover &, .card-parent:focus-within &': {
  187. opacity: 1,
  188. },
  189. }),
  190. card: css({
  191. gridTemplateAreas: `
  192. "Figure Heading Actions"
  193. "Figure Description Actions"
  194. "Figure Meta Actions"
  195. "Figure - Actions"`,
  196. }),
  197. logo: css({
  198. marginRight: theme.v1.spacing.lg,
  199. marginLeft: theme.v1.spacing.sm,
  200. width: theme.spacing(7),
  201. maxHeight: theme.spacing(7),
  202. }),
  203. };
  204. }
  205. export function getNavModel(): NavModel {
  206. const main = {
  207. icon: 'database',
  208. id: 'datasource-new',
  209. text: 'Add data source',
  210. href: 'datasources/new',
  211. subTitle: 'Choose a data source type',
  212. };
  213. return {
  214. main: main,
  215. node: main,
  216. };
  217. }
  218. export default connector(NewDataSourcePage);