|
@@ -1,13 +1,12 @@
|
|
|
-import React, { PureComponent } from 'react';
|
|
|
|
|
|
|
+import React, { PureComponent, FC } from 'react';
|
|
|
import { connect } from 'react-redux';
|
|
import { connect } from 'react-redux';
|
|
|
import { hot } from 'react-hot-loader';
|
|
import { hot } from 'react-hot-loader';
|
|
|
import Page from 'app/core/components/Page/Page';
|
|
import Page from 'app/core/components/Page/Page';
|
|
|
import { StoreState } from 'app/types';
|
|
import { StoreState } from 'app/types';
|
|
|
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
|
|
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
|
|
|
-import { getNavModel } from 'app/core/selectors/navModel';
|
|
|
|
|
import { getDataSourceTypes } from './state/selectors';
|
|
import { getDataSourceTypes } from './state/selectors';
|
|
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
|
|
-import { NavModel, DataSourcePluginMeta } from '@grafana/ui';
|
|
|
|
|
|
|
+import { NavModel, DataSourcePluginMeta, List } from '@grafana/ui';
|
|
|
|
|
|
|
|
export interface Props {
|
|
export interface Props {
|
|
|
navModel: NavModel;
|
|
navModel: NavModel;
|
|
@@ -15,13 +14,40 @@ export interface Props {
|
|
|
isLoading: boolean;
|
|
isLoading: boolean;
|
|
|
addDataSource: typeof addDataSource;
|
|
addDataSource: typeof addDataSource;
|
|
|
loadDataSourceTypes: typeof loadDataSourceTypes;
|
|
loadDataSourceTypes: typeof loadDataSourceTypes;
|
|
|
- dataSourceTypeSearchQuery: string;
|
|
|
|
|
|
|
+ searchQuery: string;
|
|
|
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
|
|
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface DataSourceCategories {
|
|
|
|
|
+ [key: string]: DataSourcePluginMeta[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DataSourceCategoryInfo {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ title: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
class NewDataSourcePage extends PureComponent<Props> {
|
|
class NewDataSourcePage extends PureComponent<Props> {
|
|
|
|
|
+ searchInput: HTMLElement;
|
|
|
|
|
+ categoryInfoList: DataSourceCategoryInfo[] = [
|
|
|
|
|
+ { id: 'tsdb', title: 'Time series databases' },
|
|
|
|
|
+ { id: 'logging', title: 'Logging & document databases' },
|
|
|
|
|
+ { id: 'sql', title: 'SQL' },
|
|
|
|
|
+ { id: 'cloud', title: 'Cloud' },
|
|
|
|
|
+ { id: 'other', title: 'Others' },
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ sortingRules: { [id: string]: number } = {
|
|
|
|
|
+ prometheus: 100,
|
|
|
|
|
+ graphite: 95,
|
|
|
|
|
+ loki: 90,
|
|
|
|
|
+ mysql: 80,
|
|
|
|
|
+ postgres: 79,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
componentDidMount() {
|
|
componentDidMount() {
|
|
|
this.props.loadDataSourceTypes();
|
|
this.props.loadDataSourceTypes();
|
|
|
|
|
+ this.searchInput.focus();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => {
|
|
onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => {
|
|
@@ -32,35 +58,108 @@ class NewDataSourcePage extends PureComponent<Props> {
|
|
|
this.props.setDataSourceTypeSearchQuery(value);
|
|
this.props.setDataSourceTypeSearchQuery(value);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ renderTypes(types: DataSourcePluginMeta[]) {
|
|
|
|
|
+ if (!types) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // apply custom sort ranking
|
|
|
|
|
+ types.sort((a, b) => {
|
|
|
|
|
+ const aSort = this.sortingRules[a.id] || 0;
|
|
|
|
|
+ const bSort = this.sortingRules[b.id] || 0;
|
|
|
|
|
+ if (aSort > bSort) {
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (aSort < bSort) {
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return a.name > b.name ? -1 : 1;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <List
|
|
|
|
|
+ items={types}
|
|
|
|
|
+ getItemKey={item => item.id.toString()}
|
|
|
|
|
+ renderItem={item => (
|
|
|
|
|
+ <DataSourceTypeCard
|
|
|
|
|
+ plugin={item}
|
|
|
|
|
+ onClick={() => this.onDataSourceTypeClicked(item)}
|
|
|
|
|
+ onLearnMoreClick={this.onLearnMoreClick}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onLearnMoreClick = (evt: React.SyntheticEvent<HTMLElement>) => {
|
|
|
|
|
+ evt.stopPropagation();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ renderGroupedList() {
|
|
|
|
|
+ const { dataSourceTypes } = this.props;
|
|
|
|
|
+
|
|
|
|
|
+ if (dataSourceTypes.length === 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const categories = dataSourceTypes.reduce(
|
|
|
|
|
+ (accumulator, item) => {
|
|
|
|
|
+ const category = item.category || 'other';
|
|
|
|
|
+ const list = accumulator[category] || [];
|
|
|
|
|
+ list.push(item);
|
|
|
|
|
+ accumulator[category] = list;
|
|
|
|
|
+ return accumulator;
|
|
|
|
|
+ },
|
|
|
|
|
+ {} as DataSourceCategories
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {this.categoryInfoList.map(category => (
|
|
|
|
|
+ <div className="add-data-source-category" key={category.id}>
|
|
|
|
|
+ <div className="add-data-source-category__header">{category.title}</div>
|
|
|
|
|
+ {this.renderTypes(categories[category.id])}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ <div className="add-data-source-more">
|
|
|
|
|
+ <a
|
|
|
|
|
+ className="btn btn-inverse"
|
|
|
|
|
+ href="https://grafana.com/plugins?type=datasource&utm_source=new-data-source"
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ >
|
|
|
|
|
+ Find more data source plugins on grafana.com
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
render() {
|
|
render() {
|
|
|
- const { navModel, dataSourceTypes, dataSourceTypeSearchQuery, isLoading } = this.props;
|
|
|
|
|
|
|
+ const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props;
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Page navModel={navModel}>
|
|
<Page navModel={navModel}>
|
|
|
<Page.Contents isLoading={isLoading}>
|
|
<Page.Contents isLoading={isLoading}>
|
|
|
- <h2 className="add-data-source-header">Choose data source type</h2>
|
|
|
|
|
- <div className="add-data-source-search">
|
|
|
|
|
- <FilterInput
|
|
|
|
|
- labelClassName="gf-form--has-input-icon"
|
|
|
|
|
- inputClassName="gf-form-input width-20"
|
|
|
|
|
- value={dataSourceTypeSearchQuery}
|
|
|
|
|
- onChange={this.onSearchQueryChange}
|
|
|
|
|
- placeholder="Filter by name or type"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div className="page-action-bar">
|
|
|
|
|
+ <div className="gf-form gf-form--grow">
|
|
|
|
|
+ <FilterInput
|
|
|
|
|
+ ref={elem => (this.searchInput = elem)}
|
|
|
|
|
+ labelClassName="gf-form--has-input-icon"
|
|
|
|
|
+ inputClassName="gf-form-input width-30"
|
|
|
|
|
+ value={searchQuery}
|
|
|
|
|
+ onChange={this.onSearchQueryChange}
|
|
|
|
|
+ placeholder="Filter by name or type"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="page-action-bar__spacer" />
|
|
|
|
|
+ <a className="btn btn-secondary" href="datasources">
|
|
|
|
|
+ Cancel
|
|
|
|
|
+ </a>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="add-data-source-grid">
|
|
|
|
|
- {dataSourceTypes.map((plugin, index) => {
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- onClick={() => this.onDataSourceTypeClicked(plugin)}
|
|
|
|
|
- className="add-data-source-grid-item"
|
|
|
|
|
- key={`${plugin.id}-${index}`}
|
|
|
|
|
- aria-label={`${plugin.name} datasource plugin`}
|
|
|
|
|
- >
|
|
|
|
|
- <img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} />
|
|
|
|
|
- <span className="add-data-source-grid-item-text">{plugin.name}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
|
|
+ <div>
|
|
|
|
|
+ {searchQuery && this.renderTypes(dataSourceTypes)}
|
|
|
|
|
+ {!searchQuery && this.renderGroupedList()}
|
|
|
</div>
|
|
</div>
|
|
|
</Page.Contents>
|
|
</Page.Contents>
|
|
|
</Page>
|
|
</Page>
|
|
@@ -68,11 +167,57 @@ class NewDataSourcePage extends PureComponent<Props> {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface DataSourceTypeCardProps {
|
|
|
|
|
+ plugin: DataSourcePluginMeta;
|
|
|
|
|
+ onClick: () => void;
|
|
|
|
|
+ onLearnMoreClick: (evt: React.SyntheticEvent<HTMLElement>) => void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
|
|
|
|
|
+ const { plugin, onClick, onLearnMoreClick } = props;
|
|
|
|
|
+
|
|
|
|
|
+ // find first plugin info link
|
|
|
|
|
+ const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0].url : null;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="add-data-source-item" onClick={onClick} aria-label={`${plugin.name} datasource plugin`}>
|
|
|
|
|
+ <img className="add-data-source-item-logo" src={plugin.info.logos.small} />
|
|
|
|
|
+ <div className="add-data-source-item-text-wrapper">
|
|
|
|
|
+ <span className="add-data-source-item-text">{plugin.name}</span>
|
|
|
|
|
+ {plugin.info.description && <span className="add-data-source-item-desc">{plugin.info.description}</span>}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="add-data-source-item-actions">
|
|
|
|
|
+ {learnMoreLink && (
|
|
|
|
|
+ <a className="btn btn-inverse" href={learnMoreLink} target="_blank" onClick={onLearnMoreClick}>
|
|
|
|
|
+ Learn more
|
|
|
|
|
+ </a>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <button className="btn btn-primary">Select</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export function getNavModel(): NavModel {
|
|
|
|
|
+ const main = {
|
|
|
|
|
+ icon: 'gicon gicon-add-datasources',
|
|
|
|
|
+ id: 'datasource-new',
|
|
|
|
|
+ text: 'New data source',
|
|
|
|
|
+ href: 'datasources/new',
|
|
|
|
|
+ subTitle: 'Choose a data source type',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ main: main,
|
|
|
|
|
+ node: main,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function mapStateToProps(state: StoreState) {
|
|
function mapStateToProps(state: StoreState) {
|
|
|
return {
|
|
return {
|
|
|
- navModel: getNavModel(state.navIndex, 'datasources'),
|
|
|
|
|
|
|
+ navModel: getNavModel(),
|
|
|
dataSourceTypes: getDataSourceTypes(state.dataSources),
|
|
dataSourceTypes: getDataSourceTypes(state.dataSources),
|
|
|
- dataSourceTypeSearchQuery: state.dataSources.dataSourceTypeSearchQuery,
|
|
|
|
|
|
|
+ searchQuery: state.dataSources.dataSourceTypeSearchQuery,
|
|
|
isLoading: state.dataSources.isLoadingDataSources,
|
|
isLoading: state.dataSources.isLoadingDataSources,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|