diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index fee2c2bff0a..5a4f58626c6 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -33,6 +33,7 @@ export { UnitPicker } from './UnitPicker/UnitPicker'; export { StatsPicker } from './StatsPicker/StatsPicker'; export { Input, InputStatus } from './Input/Input'; export { RefreshPicker } from './RefreshPicker/RefreshPicker'; +export { List } from './List/List'; // Renderless export { SetInterval } from './SetInterval/SetInterval'; diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index c50c04063e5..f9a2d39dd1f 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -79,7 +79,9 @@ export interface DataSourcePluginMeta extends PluginMeta { annotations?: boolean; mixed?: boolean; hasQueryHelp?: boolean; + category?: string; queryOptions?: PluginMetaQueryOptions; + sort?: number; } interface PluginMetaQueryOptions { diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index edc9d96d1ac..8b32bea41dc 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -34,6 +34,7 @@ type PluginListItem struct { LatestVersion string `json:"latestVersion"` HasUpdate bool `json:"hasUpdate"` DefaultNavUrl string `json:"defaultNavUrl"` + Category string `json:"category"` State plugins.PluginState `json:"state"` } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bec33313d0d..92b5076de2e 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -47,6 +47,7 @@ func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response { Id: pluginDef.Id, Name: pluginDef.Name, Type: pluginDef.Type, + Category: pluginDef.Category, Info: &pluginDef.Info, LatestVersion: pluginDef.GrafanaNetVersion, HasUpdate: pluginDef.GrafanaNetHasUpdate, diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index feeae936680..132bb9e2658 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -45,6 +45,7 @@ type PluginBase struct { Includes []*PluginInclude `json:"includes"` Module string `json:"module"` BaseUrl string `json:"baseUrl"` + Category string `json:"category"` HideFromList bool `json:"hideFromList,omitempty"` Preload bool `json:"preload"` State PluginState `json:"state,omitempty"` diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index 716c96280ad..542e4289bb8 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -1,13 +1,12 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, FC } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import Page from 'app/core/components/Page/Page'; import { StoreState } from 'app/types'; import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions'; -import { getNavModel } from 'app/core/selectors/navModel'; import { getDataSourceTypes } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; -import { NavModel, DataSourcePluginMeta } from '@grafana/ui'; +import { NavModel, DataSourcePluginMeta, List } from '@grafana/ui'; export interface Props { navModel: NavModel; @@ -15,13 +14,40 @@ export interface Props { isLoading: boolean; addDataSource: typeof addDataSource; loadDataSourceTypes: typeof loadDataSourceTypes; - dataSourceTypeSearchQuery: string; + searchQuery: string; setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery; } +interface DataSourceCategories { + [key: string]: DataSourcePluginMeta[]; +} + +interface DataSourceCategoryInfo { + id: string; + title: string; +} + class NewDataSourcePage extends PureComponent { + 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() { this.props.loadDataSourceTypes(); + this.searchInput.focus(); } onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => { @@ -32,35 +58,108 @@ class NewDataSourcePage extends PureComponent { 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 ( + item.id.toString()} + renderItem={item => ( + this.onDataSourceTypeClicked(item)} + onLearnMoreClick={this.onLearnMoreClick} + /> + )} + /> + ); + } + + onLearnMoreClick = (evt: React.SyntheticEvent) => { + 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 => ( +
+
{category.title}
+ {this.renderTypes(categories[category.id])} +
+ ))} + + + ); + } + render() { - const { navModel, dataSourceTypes, dataSourceTypeSearchQuery, isLoading } = this.props; + const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props; + return ( -

Choose data source type

-
- +
+
+ (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" + /> +
+ -
- {dataSourceTypes.map((plugin, index) => { - return ( -
this.onDataSourceTypeClicked(plugin)} - className="add-data-source-grid-item" - key={`${plugin.id}-${index}`} - aria-label={`${plugin.name} datasource plugin`} - > - - {plugin.name} -
- ); - })} +
+ {searchQuery && this.renderTypes(dataSourceTypes)} + {!searchQuery && this.renderGroupedList()}
@@ -68,11 +167,57 @@ class NewDataSourcePage extends PureComponent { } } +interface DataSourceTypeCardProps { + plugin: DataSourcePluginMeta; + onClick: () => void; + onLearnMoreClick: (evt: React.SyntheticEvent) => void; +} + +const DataSourceTypeCard: FC = 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 ( +
+ +
+ {plugin.name} + {plugin.info.description && {plugin.info.description}} +
+
+ {learnMoreLink && ( + + Learn more + + )} + +
+
+ ); +}; + +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) { return { - navModel: getNavModel(state.navIndex, 'datasources'), + navModel: getNavModel(), dataSourceTypes: getDataSourceTypes(state.dataSources), - dataSourceTypeSearchQuery: state.dataSources.dataSourceTypeSearchQuery, + searchQuery: state.dataSources.dataSourceTypeSearchQuery, isLoading: state.dataSources.isLoadingDataSources, }; } diff --git a/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts index 44f831af0c2..58ad4d8aac9 100644 --- a/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts +++ b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts @@ -5,7 +5,6 @@ import angular from 'angular'; const template = ` `; @@ -20,7 +19,7 @@ export function uploadDashboardDirective(timer, $location) { btnText: '@?', }, link: (scope, elem) => { - scope.btnText = angular.isDefined(scope.btnText) ? scope.btnText : 'Upload .json File'; + scope.btnText = angular.isDefined(scope.btnText) ? scope.btnText : 'Upload .json file'; function file_selected(evt) { const files = evt.target.files; // FileList object diff --git a/public/app/plugins/datasource/cloudwatch/plugin.json b/public/app/plugins/datasource/cloudwatch/plugin.json index c131c768880..212bb20a059 100644 --- a/public/app/plugins/datasource/cloudwatch/plugin.json +++ b/public/app/plugins/datasource/cloudwatch/plugin.json @@ -2,13 +2,14 @@ "type": "datasource", "name": "CloudWatch", "id": "cloudwatch", + "category": "cloud", "metrics": true, "alerting": true, "annotations": true, "info": { - "description": "Cloudwatch Data Source for Grafana", + "description": "Data source for Amazon AWS monitoring service", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index 26d88092c48..b34dcce9838 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -2,9 +2,10 @@ "type": "datasource", "name": "Elasticsearch", "id": "elasticsearch", + "category": "logging", "info": { - "description": "Elasticsearch Data Source for Grafana", + "description": "Open source logging & analytics database", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -14,7 +15,7 @@ "small": "img/elasticsearch.svg", "large": "img/elasticsearch.svg" }, - "links": [{ "name": "elastic.co", "url": "https://www.elastic.co/products/elasticsearch" }] + "links": [{ "name": "Learn more", "url": "https://grafana.com/docs/features/datasources/elasticsearch/" }] }, "alerting": true, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json b/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json index e4f48c581e3..fe1b88bfb85 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json @@ -2,9 +2,10 @@ "type": "datasource", "name": "Azure Monitor", "id": "grafana-azure-monitor-datasource", + "category": "cloud", "info": { - "description": "Grafana data source for Azure Monitor/Application Insights", + "description": "Data source for Microsoft Azure Monitor & Application Insights", "author": { "name": "Grafana Labs", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index a28ef8249a1..f66c7fb202f 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -2,6 +2,7 @@ "name": "Graphite", "type": "datasource", "id": "graphite", + "category": "tsdb", "includes": [{ "type": "dashboard", "name": "Graphite Carbon Metrics", "path": "dashboards/carbon_metrics.json" }], @@ -16,7 +17,7 @@ }, "info": { - "description": "Graphite Data Source for Grafana", + "description": "Open source time series database", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index c666a2085ea..fa660ee1232 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -2,6 +2,7 @@ "type": "datasource", "name": "InfluxDB", "id": "influxdb", + "category": "tsdb", "defaultMatchFormat": "regex values", "metrics": true, @@ -14,7 +15,7 @@ }, "info": { - "description": "InfluxDB Data Source for Grafana", + "description": "Open source time series database", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/input/plugin.json b/public/app/plugins/datasource/input/plugin.json index 21e251915e0..91782a34806 100644 --- a/public/app/plugins/datasource/input/plugin.json +++ b/public/app/plugins/datasource/input/plugin.json @@ -11,7 +11,7 @@ "explore": false, "info": { - "description": "User Input Data Source for Grafana", + "description": "Data source that supports manual table & CSV input", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/loki/plugin.json b/public/app/plugins/datasource/loki/plugin.json index 94ea089d938..66e0f1027e9 100644 --- a/public/app/plugins/datasource/loki/plugin.json +++ b/public/app/plugins/datasource/loki/plugin.json @@ -2,6 +2,7 @@ "type": "datasource", "name": "Loki", "id": "loki", + "category": "logging", "metrics": true, "alerting": false, @@ -11,7 +12,7 @@ "tables": false, "info": { - "description": "Loki Logging Data Source for Grafana", + "description": "Like Prometheus but for logs. OSS logging solution from Grafana Labs", "author": { "name": "Grafana Project", "url": "https://grafana.com" @@ -22,7 +23,11 @@ }, "links": [ { - "name": "Loki", + "name": "Learn more", + "url": "https://grafana.com/loki" + }, + { + "name": "GitHub Project", "url": "https://github.com/grafana/loki" } ], diff --git a/public/app/plugins/datasource/mssql/plugin.json b/public/app/plugins/datasource/mssql/plugin.json index 80d70daada4..b3269b91100 100644 --- a/public/app/plugins/datasource/mssql/plugin.json +++ b/public/app/plugins/datasource/mssql/plugin.json @@ -2,9 +2,10 @@ "type": "datasource", "name": "Microsoft SQL Server", "id": "mssql", + "category": "sql", "info": { - "description": "Microsoft SQL Server Data Source for Grafana", + "description": "Data source for Microsoft SQL Server compatible databases", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/mysql/plugin.json b/public/app/plugins/datasource/mysql/plugin.json index cbb22130c13..49d1996332f 100644 --- a/public/app/plugins/datasource/mysql/plugin.json +++ b/public/app/plugins/datasource/mysql/plugin.json @@ -2,9 +2,10 @@ "type": "datasource", "name": "MySQL", "id": "mysql", + "category": "sql", "info": { - "description": "MySQL Data Source for Grafana", + "description": "Data source for MySQL databases", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index 4dbd677af84..e7cae327c5b 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -2,6 +2,7 @@ "type": "datasource", "name": "OpenTSDB", "id": "opentsdb", + "category": "tsdb", "metrics": true, "defaultMatchFormat": "pipe", @@ -10,7 +11,7 @@ "tables": false, "info": { - "description": "OpenTSDB Data Source for Grafana", + "description": "Open source time series database", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/postgres/plugin.json b/public/app/plugins/datasource/postgres/plugin.json index bce485264ea..994578a7f2c 100644 --- a/public/app/plugins/datasource/postgres/plugin.json +++ b/public/app/plugins/datasource/postgres/plugin.json @@ -2,9 +2,10 @@ "type": "datasource", "name": "PostgreSQL", "id": "postgres", + "category": "sql", "info": { - "description": "PostgreSQL Data Source for Grafana", + "description": "Data source for PostgreSQL and compatible databases", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index 6d2b526a7c7..fb9ebbb52b1 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -2,6 +2,8 @@ "type": "datasource", "name": "Prometheus", "id": "prometheus", + "category": "tsdb", + "includes": [ { "type": "dashboard", @@ -28,7 +30,7 @@ "minInterval": true }, "info": { - "description": "Prometheus Data Source for Grafana", + "description": "Open source time series database & alerting", "author": { "name": "Grafana Project", "url": "https://grafana.com" diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json index 1ee3d57e9b1..620a7b1c8ce 100644 --- a/public/app/plugins/datasource/stackdriver/plugin.json +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -2,6 +2,8 @@ "name": "Stackdriver", "type": "datasource", "id": "stackdriver", + "category": "cloud", + "metrics": true, "alerting": true, "annotations": true, @@ -10,8 +12,9 @@ "maxDataPoints": true, "cacheTimeout": true }, + "info": { - "description": "Google Stackdriver Datasource for Grafana", + "description": "Data source for Google's monitoring service", "version": "1.0.0", "logos": { "small": "img/stackdriver_logo.svg", diff --git a/public/app/plugins/datasource/testdata/img/testdata.svg b/public/app/plugins/datasource/testdata/img/testdata.svg new file mode 100644 index 00000000000..13b413dc85f --- /dev/null +++ b/public/app/plugins/datasource/testdata/img/testdata.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/app/plugins/datasource/testdata/plugin.json b/public/app/plugins/datasource/testdata/plugin.json index 63a9b261de7..96c23b19665 100644 --- a/public/app/plugins/datasource/testdata/plugin.json +++ b/public/app/plugins/datasource/testdata/plugin.json @@ -12,13 +12,14 @@ }, "info": { + "description": "Generates test data in different forms", "author": { "name": "Grafana Project", "url": "https://grafana.com" }, "logos": { - "small": "../../../../img/grafana_icon.svg", - "large": "../../../../img/grafana_icon.svg" + "small": "img/testdata.svg", + "large": "img/testdata.svg" } }, diff --git a/public/sass/components/_add_data_source.scss b/public/sass/components/_add_data_source.scss index 4046be2a723..c14455d35c1 100644 --- a/public/sass/components/_add_data_source.scss +++ b/public/sass/components/_add_data_source.scss @@ -10,37 +10,74 @@ margin-bottom: $space-lg; } -.add-data-source-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-row-gap: 10px; - grid-column-gap: 10px; - - @include media-breakpoint-up(md) { - grid-template-columns: repeat(3, 1fr); - } +.add-data-source-category { + margin-bottom: $space-md; } -.add-data-source-grid-item { - padding: 15px; +.add-data-source-category__header { + font-size: $font-size-h5; + margin-bottom: $space-sm; +} + +.add-data-source-item { + padding: $space-md; display: flex; align-items: center; cursor: pointer; - background: $card-background; box-shadow: $card-shadow; - color: $text-color; + background: $panel-editor-viz-item-bg; + border: 1px solid transparent; + border-radius: 3px; + margin-bottom: $space-xxs; &:hover { - background: $card-background-hover; + box-shadow: $panel-editor-viz-item-shadow-hover; + background: $panel-editor-viz-item-bg-hover; + border: $panel-editor-viz-item-border-hover; color: $text-color-strong; + + .add-data-source-item-actions { + opacity: 1; + transition: 0.15s opacity ease-in-out; + } } } -.add-data-source-grid-item-text { +.add-data-source-item-text-wrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.add-data-source-item-desc { + font-size: $font-size-sm; + color: $text-color-weak; +} + +.add-data-source-item-text { font-size: $font-size-h5; } -.add-data-source-grid-item-logo { - margin: 0 $space-md; +.add-data-source-item-logo { + margin-right: $space-lg; + margin-left: $space-sm; width: 55px; + max-height: 55px; +} + +.add-data-source-item-actions { + opacity: 0; + padding-left: $space-md; + display: flex; + align-items: center; + + > button { + margin-left: $space-md; + cursor: pointer; + } +} + +.add-data-source-more { + text-align: center; + margin: $space-xl; }