Explorar el Código

AddDataSource: Updated page design & categories (#16971)

* minor refactoring

* Added category

* Minor progress

* Progres

* Updated descriptions

* Added custom sort

* NewDataSource: progress

* Updated design

* NewDataSource: Updated design

* Updated link

* Feedback changes
Torkel Ödegaard hace 6 años
padre
commit
e1d408a66f

+ 1 - 0
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';

+ 2 - 0
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 {

+ 1 - 0
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"`
 }
 

+ 1 - 0
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,

+ 1 - 0
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"`

+ 175 - 30
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<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() {
     this.props.loadDataSourceTypes();
+    this.searchInput.focus();
   }
 
   onDataSourceTypeClicked = (plugin: DataSourcePluginMeta) => {
@@ -32,35 +58,108 @@ class NewDataSourcePage extends PureComponent<Props> {
     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() {
-    const { navModel, dataSourceTypes, dataSourceTypeSearchQuery, isLoading } = this.props;
+    const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props;
+
     return (
       <Page navModel={navModel}>
         <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 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>
         </Page.Contents>
       </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) {
   return {
-    navModel: getNavModel(state.navIndex, 'datasources'),
+    navModel: getNavModel(),
     dataSourceTypes: getDataSourceTypes(state.dataSources),
-    dataSourceTypeSearchQuery: state.dataSources.dataSourceTypeSearchQuery,
+    searchQuery: state.dataSources.dataSourceTypeSearchQuery,
     isLoading: state.dataSources.isLoadingDataSources,
   };
 }

+ 1 - 2
public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts

@@ -5,7 +5,6 @@ import angular from 'angular';
 const template = `
 <input type="file" id="dashupload" name="dashupload" class="hide" onchange="angular.element(this).scope().file_selected"/>
 <label class="btn btn-primary" for="dashupload">
-  <i class="fa fa-upload"></i>
   {{btnText}}
 </label>
 `;
@@ -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

+ 2 - 1
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"

+ 3 - 2
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,

+ 2 - 1
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"

+ 2 - 1
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"

+ 2 - 1
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"

+ 1 - 1
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"

+ 7 - 2
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"
       }
     ],

+ 2 - 1
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"

+ 2 - 1
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"

+ 2 - 1
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"

+ 2 - 1
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"

+ 3 - 1
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"

+ 4 - 1
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",

+ 1 - 0
public/app/plugins/datasource/testdata/img/testdata.svg

@@ -0,0 +1 @@
+<?xml version="1.0" ?><!DOCTYPE svg  PUBLIC '-//W3C//DTD SVG 1.1//EN'  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="100%" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;" version="1.1" viewBox="0 0 64 64" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" xmlns:xlink="http://www.w3.org/1999/xlink"><rect height="64" id="comparison--data--analytics" style="fill:none;" width="64" x="0" y="0"/><path d="M22,11c0,-0.552 -0.448,-1 -1,-1c-1.916,0 -6.084,0 -8,0c-0.552,0 -1,0.448 -1,1c0,4.3 0,21 0,21l10,0c0,0 0,-16.7 0,-21Z" style="fill:#ccff90;"/><path d="M42,43c0,0.552 0.448,1 1,1c1.916,0 6.084,0 8,0c0.552,0 1,-0.448 1,-1c0,-2.882 0,-11 0,-11l-10,0c0,0 0,8.118 0,11Z" style="fill:#6ae573;"/><path d="M37,18c0,-0.552 -0.448,-1 -1,-1c-1.916,0 -6.084,0 -8,0c-0.552,0 -1,0.448 -1,1c0,3.355 0,14 0,14l10,0c0,0 0,-10.645 0,-14Z" style="fill:#ccff90;"/><path d="M27,39c0,0.552 0.448,1 1,1c1.916,0 6.084,0 8,0c0.552,0 1,-0.448 1,-1c0,-2.146 0,-7 0,-7l-10,0c0,0 0,4.854 0,7Z" style="fill:#6ae573;"/><path d="M52,23c0,-0.552 -0.448,-1 -1,-1c-1.916,0 -6.084,0 -8,0c-0.552,0 -1,0.448 -1,1c0,2.533 0,9 0,9l10,0c0,0 0,-6.467 0,-9Z" style="fill:#ccff90;"/><path d="M12,47c0,0.552 0.448,1 1,1c1.916,0 6.084,0 8,0c0.552,0 1,-0.448 1,-1c0,-3.502 0,-15 0,-15l-10,0c0,0 0,11.498 0,15Z" style="fill:#6ae573;"/><rect height="2" style="fill:#00c853;" width="48" x="8" y="31"/></svg>

+ 3 - 2
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"
     }
   },
 

+ 54 - 17
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;
 }