From 3e6104f45a934a25f006749fa2959fe1f164e127 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 2 May 2019 21:20:31 -0700 Subject: [PATCH 01/11] TestDataDatasource: Add config editor (#16861) --- .../plugins/datasource/testdata/ConfigEditor.tsx | 15 +++++++++++++++ public/app/plugins/datasource/testdata/module.tsx | 2 ++ 2 files changed, 17 insertions(+) create mode 100644 public/app/plugins/datasource/testdata/ConfigEditor.tsx diff --git a/public/app/plugins/datasource/testdata/ConfigEditor.tsx b/public/app/plugins/datasource/testdata/ConfigEditor.tsx new file mode 100644 index 0000000000000..ec4b2a667a28c --- /dev/null +++ b/public/app/plugins/datasource/testdata/ConfigEditor.tsx @@ -0,0 +1,15 @@ +// Libraries +import React, { PureComponent } from 'react'; + +import { DataSourcePluginOptionsEditorProps } from '@grafana/ui'; + +type Props = DataSourcePluginOptionsEditorProps; + +/** + * Empty Config Editor -- settings to save + */ +export class ConfigEditor extends PureComponent { + render() { + return
; + } +} diff --git a/public/app/plugins/datasource/testdata/module.tsx b/public/app/plugins/datasource/testdata/module.tsx index d4cef1df16cbc..738149451b978 100644 --- a/public/app/plugins/datasource/testdata/module.tsx +++ b/public/app/plugins/datasource/testdata/module.tsx @@ -1,6 +1,7 @@ import { DataSourcePlugin } from '@grafana/ui'; import { TestDataDatasource } from './datasource'; import { TestDataQueryCtrl } from './query_ctrl'; +import { ConfigEditor } from './ConfigEditor'; class TestDataAnnotationsQueryCtrl { annotation: any; @@ -9,5 +10,6 @@ class TestDataAnnotationsQueryCtrl { } export const plugin = new DataSourcePlugin(TestDataDatasource) + .setConfigEditor(ConfigEditor) .setQueryCtrl(TestDataQueryCtrl) .setAnnotationQueryCtrl(TestDataAnnotationsQueryCtrl); From ece4d2201ce436803c052d915a18a72fd7b2d8de Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 2 May 2019 21:26:30 -0700 Subject: [PATCH 02/11] DataSources: minor typescript cleanups and comments (#16860) * datasource interface cleanup * more types * use PluginInclude --- packages/grafana-ui/src/types/datasource.ts | 6 +- .../datasources/settings/ButtonRow.tsx | 4 +- .../datasources/settings/PluginSettings.tsx | 4 +- .../app/features/datasources/state/actions.ts | 16 +++-- .../features/datasources/state/navModel.ts | 10 +-- public/app/features/plugins/state/navModel.ts | 61 ------------------- 6 files changed, 23 insertions(+), 78 deletions(-) delete mode 100644 public/app/features/plugins/state/navModel.ts diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 3803bf7eca457..299545a0cdae5 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -330,7 +330,8 @@ export interface QueryHint { } /** - * Data Source instance edit model + * Data Source instance edit model. This is returned from: + * /api/datasources */ export interface DataSourceSettings { id: number; @@ -354,7 +355,8 @@ export interface DataSourceSettings { /** * Frontend settings model that is passed to Datasource constructor. This differs a bit from the model above - * as this data model is available to every user who has access to a data source (Viewers+). + * as this data model is available to every user who has access to a data source (Viewers+). This is loaded + * in bootData (on page load), or from: /api/frontend/settings */ export interface DataSourceInstanceSettings { id: number; diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx index 5bae5e7f98dbd..2c048a33e7aff 100644 --- a/public/app/features/datasources/settings/ButtonRow.tsx +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -4,8 +4,8 @@ import config from 'app/core/config'; export interface Props { isReadOnly: boolean; onDelete: () => void; - onSubmit: (event) => void; - onTest: (event) => void; + onSubmit: (event: any) => void; + onTest: (event: any) => void; } const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit, onTest }) => { diff --git a/public/app/features/datasources/settings/PluginSettings.tsx b/public/app/features/datasources/settings/PluginSettings.tsx index bef5b52434c18..5b5efe95d5630 100644 --- a/public/app/features/datasources/settings/PluginSettings.tsx +++ b/public/app/features/datasources/settings/PluginSettings.tsx @@ -14,11 +14,11 @@ export class PluginSettings extends PureComponent { element: any; component: AngularComponent; scopeProps: { - ctrl: { datasourceMeta: Plugin; current: DataSourceSettings }; + ctrl: { datasourceMeta: DataSourcePluginMeta; current: DataSourceSettings }; onModelChanged: (dataSource: DataSourceSettings) => void; }; - constructor(props) { + constructor(props: Props) { super(props); this.scopeProps = { diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 705b28c7bcc62..a74943aa9ecf7 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -105,7 +105,11 @@ export function deleteDataSource(): ThunkResult { }; } -export function nameExits(dataSources, name) { +interface ItemWithName { + name: string; +} + +export function nameExits(dataSources: ItemWithName[], name: string) { return ( dataSources.filter(dataSource => { return dataSource.name.toLowerCase() === name.toLowerCase(); @@ -113,7 +117,7 @@ export function nameExits(dataSources, name) { ); } -export function findNewName(dataSources, name) { +export function findNewName(dataSources: ItemWithName[], name: string) { // Need to loop through current data sources to make sure // the name doesn't exist while (nameExits(dataSources, name)) { @@ -143,18 +147,18 @@ function updateFrontendSettings() { }); } -function nameHasSuffix(name) { +function nameHasSuffix(name: string) { return name.endsWith('-', name.length - 1); } -function getLastDigit(name) { +function getLastDigit(name: string) { return parseInt(name.slice(-1), 10); } -function incrementLastDigit(digit) { +function incrementLastDigit(digit: number) { return isNaN(digit) ? 1 : digit + 1; } -function getNewName(name) { +function getNewName(name: string) { return name.slice(0, name.length - 1); } diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 0ec10cee19e22..6fdc6f5762f80 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -1,4 +1,4 @@ -import { PluginMeta, DataSourceSettings, PluginType, NavModel, NavModelItem } from '@grafana/ui'; +import { PluginMeta, DataSourceSettings, PluginType, NavModel, NavModelItem, PluginInclude } from '@grafana/ui'; import config from 'app/core/config'; export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: PluginMeta): NavModelItem { @@ -15,7 +15,7 @@ export function buildNavModel(dataSource: DataSourceSettings, pluginMeta: Plugin icon: 'fa fa-fw fa-sliders', id: `datasource-settings-${dataSource.id}`, text: 'Settings', - url: `datasources/edit/${dataSource.id}`, + url: `datasources/edit/${dataSource.id}/`, }, ], }; @@ -106,10 +106,10 @@ export function getDataSourceLoadingNav(pageName: string): NavModel { }; } -function hasDashboards(includes) { +function hasDashboards(includes: PluginInclude[]): boolean { return ( - includes.filter(include => { + includes.find(include => { return include.type === 'dashboard'; - }).length > 0 + }) !== undefined ); } diff --git a/public/app/features/plugins/state/navModel.ts b/public/app/features/plugins/state/navModel.ts deleted file mode 100644 index 6b759bba99e7b..0000000000000 --- a/public/app/features/plugins/state/navModel.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Libraries -import _ from 'lodash'; - -// Utils & Services -import config from 'app/core/config'; - -// Types -import { NavModel, PluginMeta, DataSourceSettings } from '@grafana/ui'; - -export function buildNavModel(ds: DataSourceSettings, plugin: PluginMeta, currentPage: string): NavModel { - let title = 'New'; - const subTitle = `Type: ${plugin.name}`; - - if (ds.id) { - title = ds.name; - } - - const main = { - img: plugin.info.logos.large, - id: 'ds-edit-' + plugin.id, - subTitle: subTitle, - url: '', - text: title, - breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }], - children: [ - { - active: currentPage === 'datasource-settings', - icon: 'fa fa-fw fa-sliders', - id: 'datasource-settings', - text: 'Settings', - url: `datasources/edit/${ds.id}`, - }, - ], - }; - - const hasDashboards: any = _.find(plugin.includes, { type: 'dashboard' }) !== undefined; - if (hasDashboards && ds.id) { - main.children.push({ - active: currentPage === 'datasource-dashboards', - icon: 'fa fa-fw fa-th-large', - id: 'datasource-dashboards', - text: 'Dashboards', - url: `datasources/edit/${ds.id}/dashboards`, - }); - } - - if (config.buildInfo.isEnterprise) { - main.children.push({ - active: currentPage === 'datasource-permissions', - icon: 'fa fa-fw fa-lock', - id: 'datasource-permissions', - text: 'Permissions', - url: `datasources/edit/${ds.id}/permissions`, - }); - } - - return { - main: main, - node: _.find(main.children, { active: true }), - }; -} From 0d02ceb4527fcdfb532166bef8b82a9afec52153 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 2 May 2019 21:56:54 -0700 Subject: [PATCH 03/11] Input Datasource: convert from angular config to react ConfigEditor (#16856) * use ConfigEditor * use ConfigEditor * update readme * make the jsondata generic --- packages/grafana-ui/src/types/datasource.ts | 32 +++++--- .../datasource/input/InputConfigEditor.tsx | 74 +++++++++++++++++++ ...source.test.ts => InputDatasource.test.ts} | 6 +- .../{datasource.ts => InputDatasource.ts} | 5 +- .../datasource/input/InputQueryEditor.tsx | 4 +- public/app/plugins/datasource/input/README.md | 2 +- .../input/legacy/CSVInputWrapper.tsx | 45 ----------- .../input/legacy/InputConfigCtrl.ts | 21 ------ .../datasource/input/legacy/config.html | 16 ---- public/app/plugins/datasource/input/module.ts | 13 +++- public/app/plugins/datasource/input/types.ts | 7 +- 11 files changed, 118 insertions(+), 107 deletions(-) create mode 100644 public/app/plugins/datasource/input/InputConfigEditor.tsx rename public/app/plugins/datasource/input/{datasource.test.ts => InputDatasource.test.ts} (82%) rename public/app/plugins/datasource/input/{datasource.ts => InputDatasource.ts} (95%) delete mode 100644 public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx delete mode 100644 public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts delete mode 100644 public/app/plugins/datasource/input/legacy/config.html diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 299545a0cdae5..f918063d06bcf 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -4,13 +4,16 @@ import { PluginMeta, GrafanaPlugin } from './plugin'; import { TableData, TimeSeries, SeriesData, LoadingState } from './data'; import { PanelData } from './panel'; +// NOTE: this seems more general than just DataSource export interface DataSourcePluginOptionsEditorProps { options: TOptions; onOptionsChange: (options: TOptions) => void; } -export class DataSourcePlugin extends GrafanaPlugin< - DataSourcePluginMeta -> { + +export class DataSourcePlugin< + TOptions extends DataSourceJsonData = DataSourceJsonData, + TQuery extends DataQuery = DataQuery +> extends GrafanaPlugin { DataSourceClass: DataSourceConstructor; components: DataSourcePluginComponents; @@ -20,7 +23,7 @@ export class DataSourcePlugin>) { + setConfigEditor(editor: React.ComponentType>>) { this.components.ConfigEditor = editor; return this; } @@ -85,14 +88,17 @@ interface PluginMetaQueryOptions { minInterval?: boolean; } -export interface DataSourcePluginComponents { +export interface DataSourcePluginComponents< + TOptions extends DataSourceJsonData = DataSourceJsonData, + TQuery extends DataQuery = DataQuery +> { QueryCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; QueryEditor?: ComponentClass>; ExploreQueryField?: ComponentClass>; ExploreStartPage?: ComponentClass; - ConfigEditor?: React.ComponentType>; + ConfigEditor?: React.ComponentType>>; } export interface DataSourceConstructor { @@ -329,11 +335,16 @@ export interface QueryHint { fix?: QueryFix; } +export interface DataSourceJsonData { + authType?: string; + defaultRegion?: string; +} + /** * Data Source instance edit model. This is returned from: * /api/datasources */ -export interface DataSourceSettings { +export interface DataSourceSettings { id: number; orgId: number; name: string; @@ -348,7 +359,8 @@ export interface DataSourceSettings { basicAuthPassword: string; basicAuthUser: string; isDefault: boolean; - jsonData: { authType: string; defaultRegion: string }; + jsonData: T; + secureJsonData?: S; readOnly: boolean; withCredentials: boolean; } @@ -358,13 +370,13 @@ export interface DataSourceSettings { * as this data model is available to every user who has access to a data source (Viewers+). This is loaded * in bootData (on page load), or from: /api/frontend/settings */ -export interface DataSourceInstanceSettings { +export interface DataSourceInstanceSettings { id: number; type: string; name: string; meta: DataSourcePluginMeta; url?: string; - jsonData: { [str: string]: any }; + jsonData: T; username?: string; password?: string; // when access is direct, for some legacy datasources diff --git a/public/app/plugins/datasource/input/InputConfigEditor.tsx b/public/app/plugins/datasource/input/InputConfigEditor.tsx new file mode 100644 index 0000000000000..47faf67bd4880 --- /dev/null +++ b/public/app/plugins/datasource/input/InputConfigEditor.tsx @@ -0,0 +1,74 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Types +import { InputOptions } from './types'; + +import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SeriesData, TableInputCSV, toCSV } from '@grafana/ui'; + +type InputSettings = DataSourceSettings; + +interface Props extends DataSourcePluginOptionsEditorProps {} + +interface State { + text: string; +} + +export class InputConfigEditor extends PureComponent { + state = { + text: '', + }; + + componentDidMount() { + const { options } = this.props; + if (options.jsonData.data) { + const text = toCSV(options.jsonData.data); + this.setState({ text }); + } + } + + onSeriesParsed = (data: SeriesData[], text: string) => { + const { options, onOptionsChange } = this.props; + if (!data) { + data = [ + { + fields: [], + rows: [], + }, + ]; + } + // data is a property on 'jsonData' + const jsonData = { + ...options.jsonData, + data, + }; + + onOptionsChange({ + ...options, + jsonData, + }); + this.setState({ text }); + }; + + render() { + const { text } = this.state; + return ( +
+
+

Shared Data:

+ Enter CSV + +
+ +
+ This data is stored in the datasource json and is returned to every user in the initial request for any + datasource. This is an appropriate place to enter a few values. Large datasets will perform better in other + datasources. +
+
+ NOTE: Changes to this data will only be reflected after a browser refresh. +
+
+ ); + } +} diff --git a/public/app/plugins/datasource/input/datasource.test.ts b/public/app/plugins/datasource/input/InputDatasource.test.ts similarity index 82% rename from public/app/plugins/datasource/input/datasource.test.ts rename to public/app/plugins/datasource/input/InputDatasource.test.ts index 164ba2d05c92a..2a8412dca88d9 100644 --- a/public/app/plugins/datasource/input/datasource.test.ts +++ b/public/app/plugins/datasource/input/InputDatasource.test.ts @@ -1,11 +1,11 @@ -import InputDatasource from './datasource'; -import { InputQuery } from './types'; +import InputDatasource from './InputDatasource'; +import { InputQuery, InputOptions } from './types'; import { readCSV, DataSourceInstanceSettings, PluginMeta } from '@grafana/ui'; import { getQueryOptions } from 'test/helpers/getQueryOptions'; describe('InputDatasource', () => { const data = readCSV('a,b,c\n1,2,3\n4,5,6'); - const instanceSettings: DataSourceInstanceSettings = { + const instanceSettings: DataSourceInstanceSettings = { id: 1, type: 'x', name: 'xxx', diff --git a/public/app/plugins/datasource/input/datasource.ts b/public/app/plugins/datasource/input/InputDatasource.ts similarity index 95% rename from public/app/plugins/datasource/input/datasource.ts rename to public/app/plugins/datasource/input/InputDatasource.ts index bf78bfb6173db..72b6190a67080 100644 --- a/public/app/plugins/datasource/input/datasource.ts +++ b/public/app/plugins/datasource/input/InputDatasource.ts @@ -6,7 +6,7 @@ import { DataSourceApi, DataSourceInstanceSettings, } from '@grafana/ui/src/types'; -import { InputQuery } from './types'; +import { InputQuery, InputOptions } from './types'; export class InputDatasource implements DataSourceApi { data: SeriesData[]; @@ -17,8 +17,7 @@ export class InputDatasource implements DataSourceApi { // Filled in by grafana plugin system id?: number; - /** @ngInject */ - constructor(instanceSettings: DataSourceInstanceSettings) { + constructor(instanceSettings: DataSourceInstanceSettings) { if (instanceSettings.jsonData) { this.data = instanceSettings.jsonData.data; } diff --git a/public/app/plugins/datasource/input/InputQueryEditor.tsx b/public/app/plugins/datasource/input/InputQueryEditor.tsx index c424443528837..1bbfb76f50b73 100644 --- a/public/app/plugins/datasource/input/InputQueryEditor.tsx +++ b/public/app/plugins/datasource/input/InputQueryEditor.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; // Types -import { InputDatasource } from './datasource'; +import { InputDatasource } from './InputDatasource'; import { InputQuery } from './types'; import { FormLabel, Select, QueryEditorProps, SelectOptionItem, SeriesData, TableInputCSV, toCSV } from '@grafana/ui'; @@ -94,5 +94,3 @@ export class InputQueryEditor extends PureComponent { ); } } - -export default InputQueryEditor; diff --git a/public/app/plugins/datasource/input/README.md b/public/app/plugins/datasource/input/README.md index facfeca8faf1f..fbe38ae66cc27 100644 --- a/public/app/plugins/datasource/input/README.md +++ b/public/app/plugins/datasource/input/README.md @@ -1,3 +1,3 @@ -# Table Datasource - Native Plugin +# Direct Input Datasource - Native Plugin This datasource lets you define results directly in CSV. The values are stored either in a shared datasource, or directly in panels. diff --git a/public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx b/public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx deleted file mode 100644 index 0974a9f44c141..0000000000000 --- a/public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Component } from 'react'; - -import coreModule from 'app/core/core_module'; - -import { TableInputCSV, SeriesData, toCSV } from '@grafana/ui'; - -interface Props { - data: SeriesData[]; - onParsed: (data: SeriesData[]) => void; -} - -interface State { - data: SeriesData[]; - text: string; -} - -/** - * Angular wrapper around TableInputCSV - */ -class Wraper extends Component { - constructor(props) { - super(props); - this.state = { - text: toCSV(props.data), - data: props.data, - }; - } - - onSeriesParsed = (data: SeriesData[], text: string) => { - this.setState({ data, text }); - this.props.onParsed(data); - }; - - render() { - const { text } = this.state; - return ; - } -} - -coreModule.directive('csvInput', [ - 'reactDirective', - reactDirective => { - return reactDirective(Wraper, ['data', 'onParsed']); - }, -]); diff --git a/public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts b/public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts deleted file mode 100644 index acca7e943272d..0000000000000 --- a/public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SeriesData } from '@grafana/ui'; - -// Loads the angular wrapping directive -import './CSVInputWrapper'; - -export class TableConfigCtrl { - static templateUrl = 'legacy/config.html'; - - current: any; // the Current Configuration (set by the plugin infra) - - /** @ngInject */ - constructor($scope: any, $injector: any) { - console.log('TableConfigCtrl Init', this); - } - - onParsed = (data: SeriesData[]) => { - this.current.jsonData.data = data; - }; -} - -export default TableConfigCtrl; diff --git a/public/app/plugins/datasource/input/legacy/config.html b/public/app/plugins/datasource/input/legacy/config.html deleted file mode 100644 index 7760879e4b76e..0000000000000 --- a/public/app/plugins/datasource/input/legacy/config.html +++ /dev/null @@ -1,16 +0,0 @@ -
-

Shared Data:

- Enter CSV - -
- -
- This data is stored in the datasource json and is returned to every user - in the initial request for any datasource. This is an appropriate place - to enter a few values. Large datasets will perform better in other datasources. -

- NOTE: Changes to this data will only be reflected after a browser refresh. -
diff --git a/public/app/plugins/datasource/input/module.ts b/public/app/plugins/datasource/input/module.ts index f7cfceeaa02c6..05a48c965ca97 100644 --- a/public/app/plugins/datasource/input/module.ts +++ b/public/app/plugins/datasource/input/module.ts @@ -1,6 +1,11 @@ -import Datasource from './datasource'; +import { DataSourcePlugin } from '@grafana/ui'; -import InputQueryEditor from './InputQueryEditor'; -import InputConfigCtrl from './legacy/InputConfigCtrl'; +import { InputDatasource } from './InputDatasource'; -export { Datasource, InputQueryEditor as QueryEditor, InputConfigCtrl as ConfigCtrl }; +import { InputQueryEditor } from './InputQueryEditor'; +import { InputConfigEditor } from './InputConfigEditor'; +import { InputOptions, InputQuery } from './types'; + +export const plugin = new DataSourcePlugin(InputDatasource) + .setConfigEditor(InputConfigEditor) + .setQueryEditor(InputQueryEditor); diff --git a/public/app/plugins/datasource/input/types.ts b/public/app/plugins/datasource/input/types.ts index 7aedb8e1192da..15b19f2dca25e 100644 --- a/public/app/plugins/datasource/input/types.ts +++ b/public/app/plugins/datasource/input/types.ts @@ -1,6 +1,11 @@ -import { DataQuery, SeriesData } from '@grafana/ui/src/types'; +import { DataQuery, SeriesData, DataSourceJsonData } from '@grafana/ui/src/types'; export interface InputQuery extends DataQuery { // Data saved in the panel data?: SeriesData[]; } + +export interface InputOptions extends DataSourceJsonData { + // Saved in the datasource and download with bootData + data?: SeriesData[]; +} From 86729f37c8251c7aff6109ab45929c0a31a8819a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 2 May 2019 21:57:30 -0700 Subject: [PATCH 04/11] TestData: Add dashboards to testdata (#16855) --- .../testdata/dashboards/streaming.json | 114 ++++++++++++++++++ .../plugins/datasource/testdata/plugin.json | 10 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/testdata/dashboards/streaming.json diff --git a/public/app/plugins/datasource/testdata/dashboards/streaming.json b/public/app/plugins/datasource/testdata/dashboards/streaming.json new file mode 100644 index 0000000000000..b2f2e9ecb6bb5 --- /dev/null +++ b/public/app/plugins/datasource/testdata/dashboards/streaming.json @@ -0,0 +1,114 @@ +{ + "__inputs": [ + { + "name": "DS_TESTDATA_DB", + "label": "TestData DB", + "description": "", + "type": "datasource", + "pluginId": "testdata", + "pluginName": "TestData DB" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "6.2.0-pre" + }, + { + "type": "panel", + "id": "graph2", + "name": "React Graph", + "version": "" + }, + { + "type": "datasource", + "id": "testdata", + "name": "TestData DB", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": "${DS_TESTDATA_DB}", + "description": "", + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "links": [], + "options": { + "graph": { + "showBars": false, + "showLines": true, + "showPoints": false + }, + "legend": { + "asTable": true, + "decimals": 2, + "isVisible": true, + "placement": "right", + "stats": ["last"] + }, + "series": {} + }, + "targets": [ + { + "refId": "A", + "scenarioId": "streaming_client", + "stream": { + "noise": 10, + "speed": 100, + "spread": 20, + "type": "signal" + }, + "stringInput": "" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Simple dummy streaming example", + "type": "graph2" + } + ], + "schemaVersion": 18, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "", + "title": "Simple Streaming Example", + "uid": "TXSTREZ", + "version": 2 +} diff --git a/public/app/plugins/datasource/testdata/plugin.json b/public/app/plugins/datasource/testdata/plugin.json index 3108fb44a1936..63a9b261de7f5 100644 --- a/public/app/plugins/datasource/testdata/plugin.json +++ b/public/app/plugins/datasource/testdata/plugin.json @@ -20,5 +20,13 @@ "small": "../../../../img/grafana_icon.svg", "large": "../../../../img/grafana_icon.svg" } - } + }, + + "includes": [ + { + "type": "dashboard", + "name": "Streaming Example", + "path": "dashboards/streaming.json" + } + ] } From f3f762065aee3f83d245b4f4d2ca8bf33d250bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 3 May 2019 07:11:23 +0200 Subject: [PATCH 05/11] Chore: Lowered implicit anys limit to 5623 Progress: #14714 --- scripts/ci-frontend-metrics.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 42f57fcf3c699..8d4f32f8913bf 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/scripts/ci-frontend-metrics.sh @@ -2,7 +2,7 @@ echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=5668 +ERROR_COUNT_LIMIT=5623 DIRECTIVES_LIMIT=175 CONTROLLERS_LIMIT=140 From 20dada01595cf254a4bea92c678d5e8857310eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 3 May 2019 08:30:09 +0200 Subject: [PATCH 06/11] DataSourceSettings: Minor fix for uncontrolled input (#16863) --- .../features/datasources/settings/DataSourceSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index fd2fa3ebf2629..1ef8fef702694 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -171,7 +171,7 @@ export class DataSourceSettingsPage extends PureComponent { } get hasDataSource() { - return Object.keys(this.props.dataSource).length > 0; + return this.state.dataSource.id > 0; } render() { From a05ee6cd6e9cbd6c56e5d6e57c09b58441bbd409 Mon Sep 17 00:00:00 2001 From: Mark Goodwin Date: Fri, 3 May 2019 17:51:53 +1000 Subject: [PATCH 07/11] Chore: fix modes for non-executable files (#16864) Fix file modes for various html, svg and ts files. These should not be executable (rpmlint complains). --- .../dashboard/components/DashboardSettings/SettingsCtrl.ts | 0 public/app/features/folders/services/FolderPageLoader.ts | 0 public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg | 0 public/app/plugins/datasource/graphite/partials/query.editor.html | 0 .../app/plugins/datasource/stackdriver/partials/query.editor.html | 0 public/app/plugins/panel/graph/graph.ts | 0 public/fonts/grafana-icons.svg | 0 7 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts mode change 100755 => 100644 public/app/features/folders/services/FolderPageLoader.ts mode change 100755 => 100644 public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg mode change 100755 => 100644 public/app/plugins/datasource/graphite/partials/query.editor.html mode change 100755 => 100644 public/app/plugins/datasource/stackdriver/partials/query.editor.html mode change 100755 => 100644 public/app/plugins/panel/graph/graph.ts mode change 100755 => 100644 public/fonts/grafana-icons.svg diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts old mode 100755 new mode 100644 diff --git a/public/app/features/folders/services/FolderPageLoader.ts b/public/app/features/folders/services/FolderPageLoader.ts old mode 100755 new mode 100644 diff --git a/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg b/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg old mode 100755 new mode 100644 diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html old mode 100755 new mode 100644 diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html old mode 100755 new mode 100644 diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts old mode 100755 new mode 100644 diff --git a/public/fonts/grafana-icons.svg b/public/fonts/grafana-icons.svg old mode 100755 new mode 100644 From 40b771c04db29c705e0edc027d90c56a021fb5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 3 May 2019 10:29:22 +0200 Subject: [PATCH 08/11] Panels: Fixed issue with panel type change and data updates (#16871) --- public/app/features/dashboard/state/PanelModel.test.ts | 8 ++++++++ public/app/features/dashboard/state/PanelModel.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index ca88584ad08dd..ceda847adf602 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -87,7 +87,10 @@ describe('PanelModel', () => { }); describe('when changing panel type', () => { + let panelQueryRunner: any; + beforeEach(() => { + panelQueryRunner = model.getQueryRunner(); model.changePlugin(getPanelPlugin({ id: 'graph' })); model.alert = { id: 2 }; }); @@ -105,6 +108,11 @@ describe('PanelModel', () => { model.changePlugin(getPanelPlugin({ id: 'table' })); expect(model.alert).toBe(undefined); }); + + it('getQueryRunner() should return same instance after plugin change', () => { + const sameQueryRunner = model.getQueryRunner(); + expect(panelQueryRunner).toBe(sameQueryRunner); + }); }); describe('when changing from angular panel', () => { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index a09540adc9ab8..c785cd3630a7e 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -33,7 +33,6 @@ const notPersistedProperties: { [str: string]: boolean } = { // To make sure the change happens without strange bugs happening when panels use same // named property with different type / value expectations // This is not required for react panels - const mustKeepProps: { [str: string]: boolean } = { id: true, gridPos: true, @@ -63,6 +62,7 @@ const mustKeepProps: { [str: string]: boolean } = { cachedPluginOptions: true, transparent: true, pluginVersion: true, + queryRunner: true, }; const defaults: any = { From 16d5df1cbaca459f24c1df598f8b17d43739fd4c Mon Sep 17 00:00:00 2001 From: Fernando Kramer Date: Fri, 3 May 2019 12:19:01 +0100 Subject: [PATCH 09/11] PluginsList: Removed icons and updated snapshots (#16872) Fixes #16833 --- public/app/features/plugins/PluginListItem.tsx | 14 +------------- .../__snapshots__/PluginListItem.test.tsx.snap | 6 ------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/public/app/features/plugins/PluginListItem.tsx b/public/app/features/plugins/PluginListItem.tsx index bafd7d63633d2..f0438109b7fc7 100644 --- a/public/app/features/plugins/PluginListItem.tsx +++ b/public/app/features/plugins/PluginListItem.tsx @@ -7,24 +7,12 @@ interface Props { const PluginListItem: FC = props => { const { plugin } = props; - let icon; - - if (plugin.type === 'datasource') { - icon = 'gicon gicon-datasources'; - } else if (plugin.type === 'panel') { - icon = 'icon-gf icon-gf-panel'; - } else { - icon = 'icon-gf icon-gf-apps'; - } return (
  • -
    - - {plugin.type} -
    +
    {plugin.type}
    {plugin.hasUpdate && (
    Update available! diff --git a/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap index 694e8f0415656..fcbc53d1a4260 100644 --- a/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap @@ -14,9 +14,6 @@ exports[`Render should render component 1`] = `
    - panel
    @@ -63,9 +60,6 @@ exports[`Render should render has plugin section 1`] = `
    - panel
    Date: Fri, 3 May 2019 22:53:07 +1000 Subject: [PATCH 10/11] LDAP: Added reload endpoint for LDAP config (#15470) * 4843 - Added reload endpoint for LDAP config closes #4843 * Refactor to make the reload work after master drifted --- pkg/api/admin_ldap.go | 17 +++++ pkg/api/api.go | 1 + pkg/login/ldap_login.go | 10 ++- pkg/login/ldap_login_test.go | 10 +-- pkg/middleware/auth_proxy/auth_proxy.go | 9 ++- pkg/middleware/auth_proxy/auth_proxy_test.go | 12 ++-- pkg/plugins/plugins.go | 2 +- pkg/services/ldap/settings.go | 66 ++++++++++++++------ 8 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 pkg/api/admin_ldap.go diff --git a/pkg/api/admin_ldap.go b/pkg/api/admin_ldap.go new file mode 100644 index 0000000000000..6e1b40bfa3e8f --- /dev/null +++ b/pkg/api/admin_ldap.go @@ -0,0 +1,17 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/services/ldap" +) + +func (server *HTTPServer) ReloadLdapCfg() Response { + if !ldap.IsEnabled() { + return Error(400, "LDAP is not enabled", nil) + } + + err := ldap.ReloadConfig() + if err != nil { + return Error(500, "Failed to reload ldap config.", err) + } + return Success("Ldap config reloaded") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index e095986a229aa..9b5aae105cba8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -393,6 +393,7 @@ func (hs *HTTPServer) registerRoutes() { adminRoute.Post("/provisioning/dashboards/reload", Wrap(hs.AdminProvisioningReloadDasboards)) adminRoute.Post("/provisioning/datasources/reload", Wrap(hs.AdminProvisioningReloadDatasources)) adminRoute.Post("/provisioning/notifications/reload", Wrap(hs.AdminProvisioningReloadNotifications)) + adminRoute.Post("/ldap/reload", Wrap(hs.ReloadLdapCfg)) }, reqGrafanaAdmin) // rendering diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go index b8811158200cd..abd861cbe6ddf 100644 --- a/pkg/login/ldap_login.go +++ b/pkg/login/ldap_login.go @@ -3,12 +3,15 @@ package login import ( "github.com/grafana/grafana/pkg/models" LDAP "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/util/errutil" ) var newLDAP = LDAP.New -var readLDAPConfig = LDAP.ReadConfig +var getLDAPConfig = LDAP.GetConfig var isLDAPEnabled = LDAP.IsEnabled +// loginUsingLdap logs in user using LDAP. It returns whether LDAP is enabled and optional error and query arg will be +// populated with the logged in user if successful. var loginUsingLdap = func(query *models.LoginUserQuery) (bool, error) { enabled := isLDAPEnabled() @@ -16,7 +19,10 @@ var loginUsingLdap = func(query *models.LoginUserQuery) (bool, error) { return false, nil } - config := readLDAPConfig() + config, err := getLDAPConfig() + if err != nil { + return true, errutil.Wrap("Failed to get LDAP config", err) + } if len(config.Servers) == 0 { return true, ErrNoLDAPServers } diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index c6bd30834a9ed..3ea82d0a8ed6d 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -20,12 +20,12 @@ func TestLdapLogin(t *testing.T) { ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) { sc.withLoginResult(false) - readLDAPConfig = func() *LDAP.Config { + getLDAPConfig = func() (*LDAP.Config, error) { config := &LDAP.Config{ Servers: []*LDAP.ServerConfig{}, } - return config + return config, nil } enabled, err := loginUsingLdap(sc.loginUserQuery) @@ -129,7 +129,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { ldapAuthenticatorMock: mock, } - readLDAPConfig = func() *LDAP.Config { + getLDAPConfig = func() (*LDAP.Config, error) { config := &LDAP.Config{ Servers: []*LDAP.ServerConfig{ { @@ -138,7 +138,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { }, } - return config + return config, nil } newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth { @@ -147,7 +147,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { defer func() { newLDAP = LDAP.New - readLDAPConfig = LDAP.ReadConfig + getLDAPConfig = LDAP.GetConfig }() fn(sc) diff --git a/pkg/middleware/auth_proxy/auth_proxy.go b/pkg/middleware/auth_proxy/auth_proxy.go index 7eee806c082b5..b9e71d1b480d4 100644 --- a/pkg/middleware/auth_proxy/auth_proxy.go +++ b/pkg/middleware/auth_proxy/auth_proxy.go @@ -22,8 +22,8 @@ const ( ) var ( - readLDAPConfig = ldap.ReadConfig - isLDAPEnabled = ldap.IsEnabled + getLDAPConfig = ldap.GetConfig + isLDAPEnabled = ldap.IsEnabled ) // AuthProxy struct @@ -219,7 +219,10 @@ func (auth *AuthProxy) GetUserIDViaLDAP() (int64, *Error) { Username: auth.header, } - config := readLDAPConfig() + config, err := getLDAPConfig() + if err != nil { + return 0, newError("Failed to get LDAP config", nil) + } if len(config.Servers) == 0 { return 0, newError("No LDAP servers available", nil) } diff --git a/pkg/middleware/auth_proxy/auth_proxy_test.go b/pkg/middleware/auth_proxy/auth_proxy_test.go index 849c156ac9821..fbddca81d4013 100644 --- a/pkg/middleware/auth_proxy/auth_proxy_test.go +++ b/pkg/middleware/auth_proxy/auth_proxy_test.go @@ -67,18 +67,18 @@ func TestMiddlewareContext(t *testing.T) { return true } - readLDAPConfig = func() *ldap.Config { + getLDAPConfig = func() (*ldap.Config, error) { config := &ldap.Config{ Servers: []*ldap.ServerConfig{ {}, }, } - return config + return config, nil } defer func() { isLDAPEnabled = ldap.IsEnabled - readLDAPConfig = ldap.ReadConfig + getLDAPConfig = ldap.GetConfig }() store := remotecache.NewFakeStore(t) @@ -109,16 +109,16 @@ func TestMiddlewareContext(t *testing.T) { return true } - readLDAPConfig = func() *ldap.Config { + getLDAPConfig = func() (*ldap.Config, error) { config := &ldap.Config{ Servers: []*ldap.ServerConfig{}, } - return config + return config, nil } defer func() { isLDAPEnabled = ldap.IsEnabled - readLDAPConfig = ldap.ReadConfig + getLDAPConfig = ldap.GetConfig }() store := remotecache.NewFakeStore(t) diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index b10e4640f4b96..0a031e3a50417 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -124,7 +124,7 @@ func (pm *PluginManager) Run(ctx context.Context) error { } } - // kil backend plugins + // kill backend plugins for _, p := range DataSources { p.Kill() } diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index c726fff3968e0..633920d0a194a 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -2,9 +2,11 @@ package ldap import ( "fmt" - "os" + "sync" "github.com/BurntSushi/toml" + "github.com/grafana/grafana/pkg/util/errutil" + "golang.org/x/xerrors" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" @@ -56,47 +58,72 @@ type GroupToOrgRole struct { var config *Config var logger = log.New("ldap") +// loadingMutex locks the reading of the config so multiple requests for reloading are sequential. +var loadingMutex = &sync.Mutex{} + // IsEnabled checks if ldap is enabled func IsEnabled() bool { return setting.LdapEnabled } -// ReadConfig reads the config if -// ldap is enabled otherwise it will return nil -func ReadConfig() *Config { +// ReloadConfig reads the config from the disc and caches it. +func ReloadConfig() error { if IsEnabled() == false { return nil } + loadingMutex.Lock() + defer loadingMutex.Unlock() + + var err error + config, err = readConfig(setting.LdapConfigFile) + return err +} + +// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of +// the config or it reads it and caches it first. +func GetConfig() (*Config, error) { + if IsEnabled() == false { + return nil, nil + } // Make it a singleton if config != nil { - return config + return config, nil } - config = getConfig(setting.LdapConfigFile) + loadingMutex.Lock() + defer loadingMutex.Unlock() + + var err error + config, err = readConfig(setting.LdapConfigFile) - return config + return config, err } -func getConfig(configFile string) *Config { + +func readConfig(configFile string) (*Config, error) { result := &Config{} logger.Info("Ldap enabled, reading config file", "file", configFile) _, err := toml.DecodeFile(configFile, result) if err != nil { - logger.Crit("Failed to load ldap config file", "error", err) - os.Exit(1) + return nil, errutil.Wrap("Failed to load ldap config file", err) } if len(result.Servers) == 0 { - logger.Crit("ldap enabled but no ldap servers defined in config file") - os.Exit(1) + return nil, xerrors.New("ldap enabled but no ldap servers defined in config file") } // set default org id for _, server := range result.Servers { - assertNotEmptyCfg(server.SearchFilter, "search_filter") - assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns") + err = assertNotEmptyCfg(server.SearchFilter, "search_filter") + if err != nil { + return nil, errutil.Wrap("Failed to validate SearchFilter section", err) + } + err = assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns") + if err != nil { + return nil, errutil.Wrap("Failed to validate SearchBaseDNs section", err) + } for _, groupMap := range server.Groups { if groupMap.OrgId == 0 { @@ -105,22 +132,21 @@ func getConfig(configFile string) *Config { } } - return result + return result, nil } -func assertNotEmptyCfg(val interface{}, propName string) { +func assertNotEmptyCfg(val interface{}, propName string) error { switch v := val.(type) { case string: if v == "" { - logger.Crit("LDAP config file is missing option", "option", propName) - os.Exit(1) + return xerrors.Errorf("LDAP config file is missing option: %v", propName) } case []string: if len(v) == 0 { - logger.Crit("LDAP config file is missing option", "option", propName) - os.Exit(1) + return xerrors.Errorf("LDAP config file is missing option: %v", propName) } default: fmt.Println("unknown") } + return nil } From c3a520493341049513e35900ffe03eb05a879231 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 3 May 2019 06:35:37 -0700 Subject: [PATCH 11/11] Dashboard: Lazy load out of view panels (#15554) * try this again * use element rather than grid position * adding back console output to debug gridPos alternative * less logging * simplify * subscribe/unsubscribe to event streams when view changes * Panels: Minor change to lazy loading --- .../dashboard/containers/DashboardPage.tsx | 10 ++- .../dashboard/containers/SoloPanelPage.tsx | 2 +- .../__snapshots__/DashboardPage.test.tsx.snap | 2 + .../dashboard/dashgrid/DashboardGrid.tsx | 57 ++++++++++++++-- .../dashboard/dashgrid/DashboardPanel.tsx | 19 +++++- .../dashboard/dashgrid/PanelChrome.tsx | 67 ++++++++++++------- 6 files changed, 123 insertions(+), 34 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 878b53e282404..d05e4e9e0521a 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -270,6 +270,9 @@ export class DashboardPage extends PureComponent { 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, }); + // Only trigger render when the scroll has moved by 25 + const approximateScrollTop = Math.round(scrollTop / 25) * 25; + return (
    {
    {dashboard.meta.submenuEnabled && } - +
    diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 30088d73cb5d1..59f364b28f0ca 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -89,7 +89,7 @@ export class SoloPanelPage extends Component { return (
    - +
    ); } diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index 745bac2a20d43..20fc00cafc01f 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -211,6 +211,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` } isEditing={false} isFullscreen={false} + scrollTop={0} />
    @@ -540,6 +541,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti } isEditing={false} isFullscreen={false} + scrollTop={0} />
    diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 601341d265720..4169270fd3580 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -83,11 +83,12 @@ export interface Props { dashboard: DashboardModel; isEditing: boolean; isFullscreen: boolean; + scrollTop: number; } export class DashboardGrid extends PureComponent { - gridToPanelMap: any; panelMap: { [id: string]: PanelModel }; + panelRef: { [id: string]: HTMLElement } = {}; componentDidMount() { const { dashboard } = this.props; @@ -149,6 +150,9 @@ export class DashboardGrid extends PureComponent { } this.props.dashboard.sortPanelsByGridPos(); + + // Call render() after any changes. This is called when the layour loads + this.forceUpdate(); }; triggerForceUpdate = () => { @@ -174,7 +178,6 @@ export class DashboardGrid extends PureComponent { }; onResize: ItemCallback = (layout, oldItem, newItem) => { - console.log(); this.panelMap[newItem.i].updateGridPos(newItem); }; @@ -187,18 +190,64 @@ export class DashboardGrid extends PureComponent { this.updateGridPos(newItem, layout); }; + isInView = (panel: PanelModel): boolean => { + if (panel.fullscreen || panel.isEditing) { + return true; + } + + // elem is set *after* the first render + const elem = this.panelRef[panel.id.toString()]; + if (!elem) { + // NOTE the gridPos is also not valid until after the first render + // since it is passed to the layout engine and made to be valid + // for example, you can have Y=0 for everything and it will stack them + // down vertically in the second call + return false; + } + + const top = parseInt(elem.style.top.replace('px', ''), 10); + const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40; + const bottom = top + height; + + // Show things that are almost in the view + const buffer = 250; + + const viewTop = this.props.scrollTop; + if (viewTop > bottom + buffer) { + return false; // The panel is above the viewport + } + + // Use the whole browser height (larger than real value) + // TODO? is there a better way + const viewHeight = isNaN(window.innerHeight) ? (window as any).clientHeight : window.innerHeight; + const viewBot = viewTop + viewHeight; + if (top > viewBot + buffer) { + return false; + } + + return !this.props.dashboard.otherPanelInFullscreen(panel); + }; + renderPanels() { const panelElements = []; - for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen }); + const id = panel.id.toString(); panelElements.push( -
    +
    { + this.panelRef[id] = elem; + }} + >
    ); diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index e3b0b93f31d27..8b5b6138c757c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -23,11 +23,13 @@ export interface Props { dashboard: DashboardModel; isEditing: boolean; isFullscreen: boolean; + isInView: boolean; } export interface State { plugin: PanelPlugin; angularPanel: AngularComponent; + isLazy: boolean; } export class DashboardPanel extends PureComponent { @@ -40,6 +42,7 @@ export class DashboardPanel extends PureComponent { this.state = { plugin: null, angularPanel: null, + isLazy: !props.isInView, }; this.specialPanels['row'] = this.renderRow.bind(this); @@ -90,7 +93,11 @@ export class DashboardPanel extends PureComponent { this.loadPlugin(this.props.panel.type); } - componentDidUpdate() { + componentDidUpdate(prevProps: Props, prevState: State) { + if (this.state.isLazy && this.props.isInView) { + this.setState({ isLazy: false }); + } + if (!this.element || this.state.angularPanel) { return; } @@ -123,7 +130,7 @@ export class DashboardPanel extends PureComponent { }; renderReactPanel() { - const { dashboard, panel, isFullscreen } = this.props; + const { dashboard, panel, isFullscreen, isInView } = this.props; const { plugin } = this.state; return ( @@ -138,6 +145,7 @@ export class DashboardPanel extends PureComponent { panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} + isInView={isInView} width={width} height={height} /> @@ -153,7 +161,7 @@ export class DashboardPanel extends PureComponent { render() { const { panel, dashboard, isFullscreen, isEditing } = this.props; - const { plugin, angularPanel } = this.state; + const { plugin, angularPanel, isLazy } = this.state; if (this.isSpecial(panel.type)) { return this.specialPanels[panel.type](); @@ -164,6 +172,11 @@ export class DashboardPanel extends PureComponent { return null; } + // If we are lazy state don't render anything + if (isLazy) { + return null; + } + const containerClass = classNames({ 'panel-editor-container': isEditing, 'panel-height-helper': !isEditing }); const panelWrapperClass = classNames({ 'panel-wrapper': true, diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b6f9bed46aa43..23ffc5c88ca48 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -31,6 +31,7 @@ export interface Props { dashboard: DashboardModel; plugin: PanelPlugin; isFullscreen: boolean; + isInView: boolean; width: number; height: number; } @@ -39,6 +40,7 @@ export interface State { isFirstLoad: boolean; renderCounter: number; errorMessage: string | null; + refreshWhenInView: boolean; // Current state of all events data: PanelData; @@ -47,7 +49,6 @@ export interface State { export class PanelChrome extends PureComponent { timeSrv: TimeSrv = getTimeSrv(); querySubscription: Unsubscribable; - delayedStateUpdate: Partial; constructor(props: Props) { super(props); @@ -55,6 +56,7 @@ export class PanelChrome extends PureComponent { isFirstLoad: true, renderCounter: 0, errorMessage: null, + refreshWhenInView: false, data: { state: LoadingState.NotStarted, series: [], @@ -90,17 +92,46 @@ export class PanelChrome extends PureComponent { } } + componentDidUpdate(prevProps: Props) { + const { isInView } = this.props; + + // View state has changed + if (isInView !== prevProps.isInView) { + if (isInView) { + // Subscribe will kick of a notice of the last known state + if (!this.querySubscription && this.wantsQueryExecution) { + const runner = this.props.panel.getQueryRunner(); + this.querySubscription = runner.subscribe(this.panelDataObserver); + } + + // Check if we need a delayed refresh + if (this.state.refreshWhenInView) { + this.onRefresh(); + } + } else if (this.querySubscription) { + this.querySubscription.unsubscribe(); + this.querySubscription = null; + } + } + } + // Updates the response with information from the stream // The next is outside a react synthetic event so setState is not batched // So in this context we can only do a single call to setState panelDataObserver = { next: (data: PanelData) => { + if (!this.props.isInView) { + // Ignore events when not visible. + // The call will be repeated when the panel comes into view + return; + } + let { errorMessage, isFirstLoad } = this.state; if (data.state === LoadingState.Error) { const { error } = data; if (error) { - if (this.state.errorMessage !== error.message) { + if (errorMessage !== error.message) { errorMessage = error.message; } } @@ -113,30 +144,26 @@ export class PanelChrome extends PureComponent { if (this.props.dashboard.snapshot) { this.props.panel.snapshotData = data.series; } - if (this.state.isFirstLoad) { + if (isFirstLoad) { isFirstLoad = false; } } - const stateUpdate = { isFirstLoad, errorMessage, data }; - - if (this.isVisible) { - this.setState(stateUpdate); - } else { - // if we are getting data while another panel is in fullscreen / edit mode - // we need to store the data but not update state yet - this.delayedStateUpdate = stateUpdate; - } + this.setState({ isFirstLoad, errorMessage, data }); }, }; onRefresh = () => { - console.log('onRefresh'); - if (!this.isVisible) { + const { panel, isInView, width } = this.props; + + console.log('onRefresh', panel.id); + + if (!isInView) { + console.log('Refresh when panel is visible', panel.id); + this.setState({ refreshWhenInView: true }); return; } - const { panel, width } = this.props; const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange()); // Issue Query @@ -172,12 +199,6 @@ export class PanelChrome extends PureComponent { onRender = () => { const stateUpdate = { renderCounter: this.state.renderCounter + 1 }; - // If we have received a data update while hidden copy over that state as well - if (this.delayedStateUpdate) { - Object.assign(stateUpdate, this.delayedStateUpdate); - this.delayedStateUpdate = null; - } - this.setState(stateUpdate); }; @@ -199,10 +220,6 @@ export class PanelChrome extends PureComponent { } }; - get isVisible() { - return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); - } - get hasPanelSnapshot() { const { panel } = this.props; return panel.snapshotData && panel.snapshotData.length;