From e9243215a6145c22417c4b7a16a39171a40d2e75 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 4 May 2020 10:02:34 +0200 Subject: [PATCH 01/99] Remove beta references from Query history (#24114) --- docs/sources/features/explore/index.md | 2 -- .../features/explore/RichHistory/RichHistoryQueriesTab.tsx | 7 ++----- .../features/explore/RichHistory/RichHistoryStarredTab.tsx | 7 ++----- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/sources/features/explore/index.md b/docs/sources/features/explore/index.md index 70616c26d2e..9e4c9d8943d 100755 --- a/docs/sources/features/explore/index.md +++ b/docs/sources/features/explore/index.md @@ -51,8 +51,6 @@ You can close the newly created query by clicking on the Close Split button. ## Query history -> BETA: Query history is a beta feature. - Query history is a list of queries that you have used in Explore. The history is local to your browser and is not shared with others. To open and interact with your history, click the **Query history** button in Explore. ### View query history diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index d7cfca19b1f..d0bf8d2aaa2 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -99,7 +99,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { font-size: ${theme.typography.heading.h4}; margin: ${theme.spacing.md} ${theme.spacing.xxs} ${theme.spacing.sm} ${theme.spacing.xxs}; `, - feedback: css` + footer: css` height: 60px; margin-top: ${theme.spacing.lg}; display: flex; @@ -225,10 +225,7 @@ export function RichHistoryQueriesTab(props: Props) { ); })} -
- Query history is a beta feature. The history is local to your browser and is not shared with others. - Feedback? -
+
The history is local to your browser and is not shared with others.
); diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index aafb94f8ffa..00a0040020b 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -51,7 +51,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { sort: css` width: 170px; `, - feedback: css` + footer: css` height: 60px; margin-top: ${theme.spacing.lg}; display: flex; @@ -129,10 +129,7 @@ export function RichHistoryStarredTab(props: Props) { /> ); })} -
- Query history is a beta feature. The history is local to your browser and is not shared with others. - Feedback? -
+
The history is local to your browser and is not shared with others.
); From 827f99f0cbd57cf27aef018e33c9bad42611788c Mon Sep 17 00:00:00 2001 From: Andreas Opferkuch Date: Mon, 4 May 2020 10:17:57 +0200 Subject: [PATCH 02/99] Prometheus: Refresh query field metrics on data source change (#24116) ... in `componentDidUpdate`, not just `componentDidMount`. Also unify query field behavior of Explore with Dashboard - when the data source changes, it doesn't unmount but instead refreshes its metrics. Fixes #23162 --- public/app/features/explore/QueryRows.tsx | 4 +- public/app/features/explore/state/reducers.ts | 1 - .../components/PromQueryField.test.tsx | 65 ++++++++++++++++++- .../prometheus/components/PromQueryField.tsx | 48 +++++++++----- 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 4b5a16ef781..c5b29238c78 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -20,8 +20,8 @@ export default class QueryRows extends PureComponent { const { className = '', exploreEvents, exploreId, queryKeys } = this.props; return (
- {queryKeys.map((key, index) => { - return ; + {queryKeys.map((_, index) => { + return ; })}
); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index e3bf248952c..aa3d194eba8 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -302,7 +302,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac latency: 0, queryResponse: createEmptyQueryResponse(), loading: false, - queryKeys: [], supportedModes, mode: mode ?? newMode, originPanelId: state.urlState && state.urlState.originPanelId, diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx index 876ed0005fd..5effcdc6c8f 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx @@ -1,4 +1,67 @@ -import { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField'; +import { mount } from 'enzyme'; +// @ts-ignore +import RCCascader from 'rc-cascader'; +import React from 'react'; +import PromQlLanguageProvider, { DEFAULT_LOOKUP_METRICS_THRESHOLD } from '../language_provider'; +import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField'; + +describe('PromQueryField', () => { + beforeAll(() => { + // @ts-ignore + window.getSelection = () => {}; + }); + + it('refreshes metrics when the data source changes', async () => { + const metrics = ['foo', 'bar']; + const languageProvider = ({ + histogramMetrics: [] as any, + metrics, + metricsMetadata: {}, + lookupsDisabled: false, + lookupMetricsThreshold: DEFAULT_LOOKUP_METRICS_THRESHOLD, + start: () => { + return Promise.resolve([]); + }, + } as unknown) as PromQlLanguageProvider; + + const queryField = mount( + {}} + onChange={() => {}} + history={[]} + /> + ); + await Promise.resolve(); + + const cascader = queryField.find(RCCascader); + cascader.simulate('click'); + const cascaderNode: HTMLElement = cascader.instance().getPopupDOMNode(); + + for (const item of Array.from(cascaderNode.getElementsByTagName('li'))) { + expect(metrics.includes(item.innerHTML)).toBe(true); + } + + const changedMetrics = ['baz', 'moo']; + queryField.setProps({ + datasource: { + languageProvider: { + ...languageProvider, + metrics: changedMetrics, + }, + }, + }); + await Promise.resolve(); + + for (const item of Array.from(cascaderNode.getElementsByTagName('li'))) { + expect(changedMetrics.includes(item.innerHTML)).toBe(true); + } + }); +}); describe('groupMetricsByPrefix()', () => { it('returns an empty group for no metrics', () => { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index efa1326d6ae..01d2f4404e6 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -20,7 +20,6 @@ import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/Cancela import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data'; import { DOMUtil, SuggestionsState } from '@grafana/ui'; import { PrometheusDatasource } from '../datasource'; -import PromQlLanguageProvider from '../language_provider'; const HISTOGRAM_GROUP = '__histograms__'; const PRISM_SYNTAX = 'promql'; @@ -121,16 +120,11 @@ interface PromQueryFieldState { class PromQueryField extends React.PureComponent { plugins: Plugin[]; - languageProvider: PromQlLanguageProvider; languageProviderInitializationPromise: CancelablePromise; constructor(props: PromQueryFieldProps, context: React.Context) { super(props, context); - if (props.datasource.languageProvider) { - this.languageProvider = props.datasource.languageProvider; - } - this.plugins = [ BracesPlugin(), SlatePrism({ @@ -147,9 +141,8 @@ class PromQueryField extends React.PureComponent) => { - this.languageProviderInitializationPromise = cancelablePromise; + refreshMetrics = () => { + const { + datasource: { languageProvider }, + } = this.props; + + Prism.languages[PRISM_SYNTAX] = languageProvider.syntax; + this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start()); this.languageProviderInitializationPromise.promise .then(remaining => { remaining.map((task: Promise) => task.then(this.onUpdateLanguage).catch(() => {})); @@ -246,7 +251,8 @@ class PromQueryField extends React.PureComponent => { - if (!this.languageProvider) { + const { + datasource: { languageProvider }, + } = this.props; + + if (!languageProvider) { return { suggestions: [] }; } const { history } = this.props; const { prefix, text, value, wrapperClasses, labelKey } = typeahead; - const result = await this.languageProvider.provideCompletionItems( + const result = await languageProvider.provideCompletionItems( { text, value, prefix, wrapperClasses, labelKey }, { history } ); @@ -293,9 +303,13 @@ class PromQueryField extends React.PureComponent 0); From 96ffcaa134d44b8565b6d2faa3cc9da48b3762c0 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 4 May 2020 10:57:55 +0200 Subject: [PATCH 03/99] Plugins: Require signing of external back-end plugins (#24075) * PluginManager: Require signing of external plugins Co-authored-by: Marcus Efraimsson Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> --- conf/defaults.ini | 2 + conf/sample.ini | 2 + docs/sources/installation/configuration.md | 4 + pkg/infra/fs/exists.go | 18 +++ pkg/infra/fs/exists_test.go | 26 +++ pkg/plugins/models.go | 1 + pkg/plugins/plugins.go | 109 +++++++++---- pkg/plugins/plugins_test.go | 148 +++++++++++++----- .../invalid-signature/plugin/MANIFEST.txt | 1 + .../invalid-signature/plugin/plugin.json | 14 ++ .../testdata/unsigned/plugin/plugin.json | 14 ++ pkg/setting/setting.go | 14 +- 12 files changed, 287 insertions(+), 66 deletions(-) create mode 100644 pkg/infra/fs/exists.go create mode 100644 pkg/infra/fs/exists_test.go create mode 100644 pkg/plugins/testdata/invalid-signature/plugin/MANIFEST.txt create mode 100644 pkg/plugins/testdata/invalid-signature/plugin/plugin.json create mode 100644 pkg/plugins/testdata/unsigned/plugin/plugin.json diff --git a/conf/defaults.ini b/conf/defaults.ini index 8574ab35498..7bdbb97a47d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -695,6 +695,8 @@ disable_sanitize_html = false [plugins] enable_alpha = false app_tls_skip_verify_insecure = false +# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. +allow_loading_unsigned_plugins = #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] diff --git a/conf/sample.ini b/conf/sample.ini index 6d180436415..d0a1fffdff9 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -684,6 +684,8 @@ [plugins] ;enable_alpha = false ;app_tls_skip_verify_insecure = false +# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. +;allow_loading_unsigned_plugins = #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 3a9a9611f48..be7c3a5fc95 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -840,6 +840,10 @@ is false. This settings was introduced in Grafana v6.0. Set to true if you want to test alpha plugins that are not yet ready for general usage. +### allow_loading_unsigned_plugins + +Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature. + ## [feature_toggles] ### enable diff --git a/pkg/infra/fs/exists.go b/pkg/infra/fs/exists.go new file mode 100644 index 00000000000..f5574ebe1e9 --- /dev/null +++ b/pkg/infra/fs/exists.go @@ -0,0 +1,18 @@ +package fs + +import ( + "os" +) + +// Exists determines whether a file/directory exists or not. +func Exists(fpath string) (bool, error) { + _, err := os.Stat(fpath) + if err != nil { + if !os.IsNotExist(err) { + return false, err + } + return false, nil + } + + return true, nil +} diff --git a/pkg/infra/fs/exists_test.go b/pkg/infra/fs/exists_test.go new file mode 100644 index 00000000000..55e1b351ec7 --- /dev/null +++ b/pkg/infra/fs/exists_test.go @@ -0,0 +1,26 @@ +package fs + +import ( + "github.com/stretchr/testify/require" + "io/ioutil" + "os" + "testing" +) + +func TestExists_NonExistent(t *testing.T) { + exists, err := Exists("non-existent") + require.NoError(t, err) + + require.False(t, exists) +} + +func TestExists_Existent(t *testing.T) { + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.Remove(f.Name()) + + exists, err := Exists(f.Name()) + require.NoError(t, err) + + require.True(t, exists) +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 0286be7abfe..d1d47bb8a32 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -60,6 +60,7 @@ type PluginBase struct { Preload bool `json:"preload"` State PluginState `json:"state,omitempty"` Signature PluginSignature `json:"signature"` + Backend bool `json:"backend"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 72f76dbbdc2..923429cb38b 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/plugins/backendplugin" @@ -43,12 +44,15 @@ type PluginScanner struct { errors []error backendPluginManager backendplugin.Manager cfg *setting.Cfg + requireSigned bool + log log.Logger } type PluginManager struct { BackendPluginManager backendplugin.Manager `inject:""` Cfg *setting.Cfg `inject:""` log log.Logger + scanningErrors []error } func init() { @@ -73,33 +77,40 @@ func (pm *PluginManager) Init() error { } pm.log.Info("Starting plugin search") + plugDir := path.Join(setting.StaticRootPath, "app/plugins") - if err := pm.scan(plugDir); err != nil { - return errutil.Wrapf(err, "Failed to scan main plugin directory '%s'", plugDir) + pm.log.Debug("Scanning core plugin directory", "dir", plugDir) + if err := pm.scan(plugDir, false); err != nil { + return errutil.Wrapf(err, "failed to scan core plugin directory '%s'", plugDir) } - pm.log.Info("Checking Bundled Plugins") - plugDir = path.Join(setting.HomePath, "plugins-bundled") - if _, err := os.Stat(plugDir); !os.IsNotExist(err) { - if err := pm.scan(plugDir); err != nil { - return errutil.Wrapf(err, "failed to scan bundled plugin directory '%s'", plugDir) + plugDir = pm.Cfg.BundledPluginsPath + pm.log.Debug("Scanning bundled plugins directory", "dir", plugDir) + exists, err := fs.Exists(plugDir) + if err != nil { + return err + } + if exists { + if err := pm.scan(plugDir, false); err != nil { + return errutil.Wrapf(err, "failed to scan bundled plugins directory '%s'", plugDir) } } // check if plugins dir exists - if _, err := os.Stat(setting.PluginsPath); os.IsNotExist(err) { + exists, err = fs.Exists(setting.PluginsPath) + if err != nil { + return err + } + if !exists { if err = os.MkdirAll(setting.PluginsPath, os.ModePerm); err != nil { - plog.Error("Failed to create plugin dir", "dir", setting.PluginsPath, "error", err) + pm.log.Error("failed to create external plugins directory", "dir", setting.PluginsPath, "error", err) } else { - plog.Info("Plugin dir created", "dir", setting.PluginsPath) - if err := pm.scan(setting.PluginsPath); err != nil { - return errutil.Wrapf(err, "Failed to scan configured plugin directory '%s'", - setting.PluginsPath) - } + pm.log.Info("External plugins directory created", "directory", setting.PluginsPath) } } else { - if err := pm.scan(setting.PluginsPath); err != nil { - return errutil.Wrapf(err, "Failed to scan configured plugin directory '%s'", + pm.log.Debug("Scanning external plugins directory", "dir", setting.PluginsPath) + if err := pm.scan(setting.PluginsPath, true); err != nil { + return errutil.Wrapf(err, "failed to scan external plugins directory '%s'", setting.PluginsPath) } } @@ -163,8 +174,8 @@ func (pm *PluginManager) checkPluginPaths() error { continue } - if err := pm.scan(path); err != nil { - return errutil.Wrapf(err, "Failed to scan directory configured for plugin '%s': '%s'", pluginID, path) + if err := pm.scan(path, false); err != nil { + return errutil.Wrapf(err, "failed to scan directory configured for plugin '%s': '%s'", pluginID, path) } } @@ -172,11 +183,13 @@ func (pm *PluginManager) checkPluginPaths() error { } // scan a directory for plugins. -func (pm *PluginManager) scan(pluginDir string) error { +func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { scanner := &PluginScanner{ pluginPath: pluginDir, backendPluginManager: pm.BackendPluginManager, cfg: pm.Cfg, + requireSigned: requireSigned, + log: pm.log, } if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil { @@ -196,6 +209,7 @@ func (pm *PluginManager) scan(pluginDir string) error { if len(scanner.errors) > 0 { pm.log.Warn("Some plugins failed to load", "errors", scanner.errors) + pm.scanningErrors = scanner.errors } return nil @@ -229,7 +243,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro if f.Name() == "plugin.json" { err := scanner.loadPluginJson(currentPath) if err != nil { - log.Error(3, "Plugins: Failed to load plugin json file: %v, err: %v", currentPath, err) + scanner.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath)) scanner.errors = append(scanner.errors, err) } } @@ -252,21 +266,51 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { } if pluginCommon.Id == "" || pluginCommon.Type == "" { - return errors.New("Did not find type and id property in plugin.json") + return errors.New("did not find type or id properties in plugin.json") + } + + pluginCommon.PluginDir = filepath.Dir(pluginJsonFilePath) + + // For the time being, we choose to only require back-end plugins to be signed + if pluginCommon.Backend && scanner.requireSigned { + scanner.log.Debug("Plugin signature required, validating", "pluginID", pluginCommon.Id, + "pluginDir", pluginCommon.PluginDir) + allowUnsigned := false + for _, plug := range scanner.cfg.PluginsAllowUnsigned { + if plug == pluginCommon.Id { + allowUnsigned = true + break + } + } + if sig := GetPluginSignatureState(&pluginCommon); sig != PluginSignatureValid && !allowUnsigned { + switch sig { + case PluginSignatureUnsigned: + return fmt.Errorf("plugin %q is unsigned", pluginCommon.Id) + case PluginSignatureInvalid: + return fmt.Errorf("plugin %q has an invalid signature", pluginCommon.Id) + case PluginSignatureModified: + return fmt.Errorf("plugin %q's signature has been modified", pluginCommon.Id) + default: + return fmt.Errorf("unrecognized plugin signature state %v", sig) + } + } } - var loader PluginLoader pluginGoType, exists := PluginTypes[pluginCommon.Type] if !exists { - return errors.New("Unknown plugin type " + pluginCommon.Type) + return fmt.Errorf("unknown plugin type %q", pluginCommon.Type) } - loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) + loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) // External plugins need a module.js file for SystemJS to load if !strings.HasPrefix(pluginJsonFilePath, setting.StaticRootPath) && !scanner.IsBackendOnlyPlugin(pluginCommon.Type) { module := filepath.Join(filepath.Dir(pluginJsonFilePath), "module.js") - if _, err := os.Stat(module); os.IsNotExist(err) { - plog.Warn("Plugin missing module.js", + exists, err := fs.Exists(module) + if err != nil { + return err + } + if !exists { + scanner.log.Warn("Plugin missing module.js", "name", pluginCommon.Name, "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.", "path", module) @@ -276,6 +320,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { if _, err := reader.Seek(0, 0); err != nil { return err } + return loader.Load(jsonParser, currentDir, scanner.backendPluginManager) } @@ -290,11 +335,19 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) { } path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name))) - if _, err := os.Stat(path); os.IsNotExist(err) { + exists, err := fs.Exists(path) + if err != nil { + return nil, err + } + if !exists { path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name))) } - if _, err := os.Stat(path); os.IsNotExist(err) { + exists, err = fs.Exists(path) + if err != nil { + return nil, err + } + if !exists { return make([]byte, 0), nil } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 8403ab8b609..8d33c628273 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -1,40 +1,36 @@ package plugins import ( + "context" + "fmt" "path/filepath" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/setting" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/ini.v1" ) -func TestPluginScans(t *testing.T) { - - Convey("When scanning for plugins", t, func() { - setting.StaticRootPath, _ = filepath.Abs("../../public/") - setting.Raw = ini.Empty() - - pm := &PluginManager{ - Cfg: &setting.Cfg{ - FeatureToggles: map[string]bool{}, - }, - } - err := pm.Init() - - So(err, ShouldBeNil) - So(len(DataSources), ShouldBeGreaterThan, 1) - So(len(Panels), ShouldBeGreaterThan, 1) - - Convey("Should set module automatically", func() { - So(DataSources["graphite"].Module, ShouldEqual, "app/plugins/datasource/graphite/module") - }) +func TestPluginManager_Init(t *testing.T) { + origRootPath := setting.StaticRootPath + origRaw := setting.Raw + t.Cleanup(func() { + setting.StaticRootPath = origRootPath + setting.Raw = origRaw }) - Convey("When reading app plugin definition", t, func() { + var err error + setting.StaticRootPath, err = filepath.Abs("../../public/") + require.NoError(t, err) + setting.Raw = ini.Empty() + + t.Run("Base case", func(t *testing.T) { pm := &PluginManager{ Cfg: &setting.Cfg{ - FeatureToggles: map[string]bool{}, PluginSettings: setting.PluginSettings{ "nginx-app": map[string]string{ "path": "testdata/test-app", @@ -43,25 +39,107 @@ func TestPluginScans(t *testing.T) { }, } err := pm.Init() - So(err, ShouldBeNil) + require.NoError(t, err) - So(len(Apps), ShouldBeGreaterThan, 0) - So(Apps["test-app"].Info.Logos.Large, ShouldEqual, "public/plugins/test-app/img/logo_large.png") - So(Apps["test-app"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/test-app/img/screenshot2.png") + assert.Empty(t, pm.scanningErrors) + assert.Greater(t, len(DataSources), 1) + assert.Greater(t, len(Panels), 1) + assert.Equal(t, "app/plugins/datasource/graphite/module", DataSources["graphite"].Module) + assert.NotEmpty(t, Apps) + assert.Equal(t, "public/plugins/test-app/img/logo_large.png", Apps["test-app"].Info.Logos.Large) + assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", Apps["test-app"].Info.Screenshots[1].Path) }) - Convey("When checking if renderer is backend only plugin", t, func() { - pluginScanner := &PluginScanner{} - result := pluginScanner.IsBackendOnlyPlugin("renderer") + t.Run("With external back-end plugin lacking signature", func(t *testing.T) { + origPluginsPath := setting.PluginsPath + t.Cleanup(func() { + setting.PluginsPath = origPluginsPath + }) + setting.PluginsPath = "testdata/unsigned" - So(result, ShouldEqual, true) + pm := &PluginManager{ + Cfg: &setting.Cfg{}, + } + err := pm.Init() + require.NoError(t, err) + + assert.Equal(t, []error{fmt.Errorf(`plugin "test" is unsigned`)}, pm.scanningErrors) }) - Convey("When checking if app is backend only plugin", t, func() { - pluginScanner := &PluginScanner{} - result := pluginScanner.IsBackendOnlyPlugin("app") + t.Run("With external unsigned back-end plugin and configuration disabling signature check of this plugin", func(t *testing.T) { + origPluginsPath := setting.PluginsPath + t.Cleanup(func() { + setting.PluginsPath = origPluginsPath + }) + setting.PluginsPath = "testdata/unsigned" - So(result, ShouldEqual, false) + pm := &PluginManager{ + Cfg: &setting.Cfg{ + PluginsAllowUnsigned: []string{"test"}, + }, + BackendPluginManager: fakeBackendPluginManager{}, + } + err := pm.Init() + require.NoError(t, err) + + assert.Empty(t, pm.scanningErrors) }) + t.Run("With external back-end plugin with invalid signature", func(t *testing.T) { + origPluginsPath := setting.PluginsPath + t.Cleanup(func() { + setting.PluginsPath = origPluginsPath + }) + setting.PluginsPath = "testdata/invalid-signature" + + pm := &PluginManager{ + Cfg: &setting.Cfg{}, + } + err := pm.Init() + require.NoError(t, err) + + assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors) + }) +} + +func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) { + pluginScanner := &PluginScanner{} + + type testCase struct { + name string + isBackendOnly bool + } + + for _, c := range []testCase{ + {name: "renderer", isBackendOnly: true}, + {name: "app", isBackendOnly: false}, + } { + t.Run(fmt.Sprintf("Plugin %s", c.name), func(t *testing.T) { + result := pluginScanner.IsBackendOnlyPlugin(c.name) + + assert.Equal(t, c.isBackendOnly, result) + }) + } +} + +type fakeBackendPluginManager struct { +} + +func (f fakeBackendPluginManager) Register(descriptor backendplugin.PluginDescriptor) error { + return nil +} + +func (f fakeBackendPluginManager) StartPlugin(ctx context.Context, pluginID string) error { + return nil +} + +func (f fakeBackendPluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backendplugin.CollectMetricsResult, error) { + return nil, nil +} + +func (f fakeBackendPluginManager) CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backendplugin.CheckHealthResult, error) { + return nil, nil +} + +func (f fakeBackendPluginManager) CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string) { } diff --git a/pkg/plugins/testdata/invalid-signature/plugin/MANIFEST.txt b/pkg/plugins/testdata/invalid-signature/plugin/MANIFEST.txt new file mode 100644 index 00000000000..3e57ccab0d1 --- /dev/null +++ b/pkg/plugins/testdata/invalid-signature/plugin/MANIFEST.txt @@ -0,0 +1 @@ +Invalid manifest diff --git a/pkg/plugins/testdata/invalid-signature/plugin/plugin.json b/pkg/plugins/testdata/invalid-signature/plugin/plugin.json new file mode 100644 index 00000000000..3e62b3fd5c0 --- /dev/null +++ b/pkg/plugins/testdata/invalid-signature/plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "type": "datasource", + "name": "Test", + "id": "test", + "backend": true, + "state": "alpha", + "info": { + "description": "Test", + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + } + } +} diff --git a/pkg/plugins/testdata/unsigned/plugin/plugin.json b/pkg/plugins/testdata/unsigned/plugin/plugin.json new file mode 100644 index 00000000000..3e62b3fd5c0 --- /dev/null +++ b/pkg/plugins/testdata/unsigned/plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "type": "datasource", + "name": "Test", + "id": "test", + "backend": true, + "state": "alpha", + "info": { + "description": "Test", + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + } + } +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f4183a84e30..cc6d44c277f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -230,9 +230,10 @@ type Cfg struct { ServeFromSubPath bool // Paths - ProvisioningPath string - DataPath string - LogsPath string + ProvisioningPath string + DataPath string + LogsPath string + BundledPluginsPath string // SMTP email settings Smtp SmtpSettings @@ -258,6 +259,7 @@ type Cfg struct { PluginsEnableAlpha bool PluginsAppsSkipVerifyTLS bool PluginSettings PluginSettings + PluginsAllowUnsigned []string DisableSanitizeHtml bool EnterpriseLicensePath string @@ -636,6 +638,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { return err } PluginsPath = makeAbsolute(plugins, HomePath) + cfg.BundledPluginsPath = makeAbsolute("plugins-bundled", HomePath) provisioning, err := valueAsString(iniFile.Section("paths"), "provisioning", "") if err != nil { return err @@ -988,6 +991,11 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false) cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false) cfg.PluginSettings = extractPluginSettings(iniFile.Sections()) + pluginsAllowUnsigned := pluginsSection.Key("allow_loading_unsigned_plugins").MustString("") + for _, plug := range strings.Split(pluginsAllowUnsigned, ",") { + plug = strings.TrimSpace(plug) + cfg.PluginsAllowUnsigned = append(cfg.PluginsAllowUnsigned, plug) + } // Read and populate feature toggles list featureTogglesSection := iniFile.Section("feature_toggles") From e8341a09b270412016b5c3d8b78e92436ddf2e3a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 4 May 2020 11:15:51 +0200 Subject: [PATCH 04/99] Docs: make sure we always use the latest docs image (#24217) --- docs/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index 272e69fd437..84bd02b6089 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,9 @@ IMAGE = grafana/docs-base:latest docs: + docker pull ${IMAGE} docker run -v $(shell pwd)/sources:/hugo/content/docs/grafana/latest -p 3002:3002 --rm -it $(IMAGE) /bin/bash -c 'make server' docs-test: + docker pull ${IMAGE} docker run -v $(shell pwd)/sources:/hugo/content/docs/grafana/latest --rm -it $(IMAGE) /bin/bash -c 'make prod' From 8a88632791a6ddb7e7a7f86bb87ae69e4ad3bdd3 Mon Sep 17 00:00:00 2001 From: Lukas Siatka Date: Mon, 4 May 2020 11:31:52 +0200 Subject: [PATCH 05/99] Chore: changes elastic terms min_doc_count default from 1 to 0 (#24204) --- public/app/plugins/datasource/elasticsearch/bucket_agg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts index 18a5bb70069..7bb0e70ab18 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts @@ -79,7 +79,7 @@ export class ElasticBucketAggCtrl { case 'terms': { settings.order = settings.order || 'desc'; settings.size = settings.size || '10'; - settings.min_doc_count = settings.min_doc_count || 1; + settings.min_doc_count = settings.min_doc_count || 0; settings.orderBy = settings.orderBy || '_term'; if (settings.size !== '0') { From 6768fb367202ee6ac6edd035daa4844dc5f17b9a Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 4 May 2020 12:43:09 +0300 Subject: [PATCH 06/99] Search: Add filterOption prop to Select (#24213) --- packages/grafana-ui/src/components/Select/SelectBase.tsx | 2 ++ packages/grafana-ui/src/components/Select/types.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 8e6e0e4d9ce..7d655117845 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -98,6 +98,7 @@ export function SelectBase({ defaultOptions, defaultValue, disabled = false, + filterOption, formatCreateLabel, getOptionLabel, getOptionValue, @@ -176,6 +177,7 @@ export function SelectBase({ defaultValue, // Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one disabled, + filterOption, getOptionLabel, getOptionValue, inputValue, diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 2acace47c8f..27caf8de145 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -14,6 +14,7 @@ export interface SelectCommonProps { components?: any; defaultValue?: any; disabled?: boolean; + filterOption?: (option: SelectableValue, searchQuery: string) => void; /** Function for formatting the text that is displayed when creating a new value*/ formatCreateLabel?: (input: string) => string; getOptionLabel?: (item: SelectableValue) => string; From 6fb7a60a2b4248e65197b216bcd8c5b0d58393ae Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 4 May 2020 11:59:10 +0200 Subject: [PATCH 07/99] Dockerfile: Move Go step after JS step, since it's faster (#24221) Signed-off-by: Arve Knudsen --- Dockerfile | 34 ++++++++++++++++------------------ Dockerfile.ubuntu | 26 +++++++++++++------------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/Dockerfile b/Dockerfile index a7b60f6e222..f90f08aae7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,3 @@ -# Golang build container -FROM golang:1.14.2-alpine3.11 as go-builder - -RUN apk add --no-cache gcc g++ - -WORKDIR $GOPATH/src/github.com/grafana/grafana - -COPY go.mod go.sum ./ - -RUN go mod verify - -COPY pkg pkg -COPY build.go package.json ./ - -RUN go run build.go build - -# Node build container FROM node:12.16.3-alpine3.11 as js-builder WORKDIR /usr/src/app/ @@ -33,7 +16,22 @@ COPY emails emails ENV NODE_ENV production RUN ./node_modules/.bin/grunt build -# Final container +FROM golang:1.14.2-alpine3.11 as go-builder + +RUN apk add --no-cache gcc g++ + +WORKDIR $GOPATH/src/github.com/grafana/grafana + +COPY go.mod go.sum ./ + +RUN go mod verify + +COPY pkg pkg +COPY build.go package.json ./ + +RUN go run build.go build + +# Final stage FROM alpine:3.11 LABEL maintainer="Grafana team " diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 39e5e5ba7e9..90855b09adc 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -1,16 +1,3 @@ -FROM golang:1.14.2 AS go-builder - -WORKDIR /src/grafana - -COPY go.mod go.sum ./ - -RUN go mod verify - -COPY build.go package.json ./ -COPY pkg pkg/ - -RUN go run build.go build - FROM node:12.16.3-slim AS js-builder WORKDIR /usr/src/app/ @@ -29,6 +16,19 @@ COPY emails emails ENV NODE_ENV production RUN ./node_modules/.bin/grunt build +FROM golang:1.14.2 AS go-builder + +WORKDIR /src/grafana + +COPY go.mod go.sum ./ + +RUN go mod verify + +COPY build.go package.json ./ +COPY pkg pkg/ + +RUN go run build.go build + FROM ubuntu:20.04 LABEL maintainer="Grafana team " From 4a5434bffa6e477c36d8f5f6d0672b67ffc02a03 Mon Sep 17 00:00:00 2001 From: Michael Cristina Date: Mon, 4 May 2020 05:12:32 -0500 Subject: [PATCH 08/99] Graph: align form switch in graph panel (#24202) --- public/app/plugins/panel/graph/tab_legend.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/tab_legend.html b/public/app/plugins/panel/graph/tab_legend.html index 467d276d676..52e66c80129 100644 --- a/public/app/plugins/panel/graph/tab_legend.html +++ b/public/app/plugins/panel/graph/tab_legend.html @@ -48,7 +48,7 @@ @@ -57,7 +57,7 @@ @@ -78,7 +78,7 @@
- + Date: Mon, 4 May 2020 03:13:12 -0700 Subject: [PATCH 09/99] Gauge: apply decimal limits to gauge min/max labels (#24192) * limit label size * fix tests --- .../grafana-ui/src/components/Gauge/Gauge.test.tsx | 4 ++-- packages/grafana-ui/src/components/Gauge/Gauge.tsx | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 300d113b1ac..4f0be7ef19a 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -48,7 +48,7 @@ describe('Get thresholds formatted', () => { thresholds: { mode: ThresholdsMode.Absolute, steps: [{ value: -Infinity, color: '#7EB26D' }] }, }); - expect(instance.getFormattedThresholds()).toEqual([ + expect(instance.getFormattedThresholds(2)).toEqual([ { value: 0, color: '#7EB26D' }, { value: 100, color: '#7EB26D' }, ]); @@ -66,7 +66,7 @@ describe('Get thresholds formatted', () => { }, }); - expect(instance.getFormattedThresholds()).toEqual([ + expect(instance.getFormattedThresholds(2)).toEqual([ { value: 0, color: '#7EB26D' }, { value: 50, color: '#7EB26D' }, { value: 75, color: '#EAB839' }, diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index e05fa68e8b6..8d31ec27a8f 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -52,7 +52,7 @@ export class Gauge extends PureComponent { this.draw(); } - getFormattedThresholds(): Threshold[] { + getFormattedThresholds(decimals: number): Threshold[] { const { field, theme } = this.props; const thresholds = field.thresholds ?? Gauge.defaultProps.field?.thresholds!; const isPercent = thresholds.mode === ThresholdsMode.Percentage; @@ -67,7 +67,7 @@ export class Gauge extends PureComponent { const first = getActiveThreshold(min, steps); const last = getActiveThreshold(max, steps); const formatted: Threshold[] = []; - formatted.push({ value: min, color: getColorFromHexRgbOrName(first.color, theme.type) }); + formatted.push({ value: +min.toFixed(decimals), color: getColorFromHexRgbOrName(first.color, theme.type) }); let skip = true; for (let i = 0; i < steps.length; i++) { const step = steps[i]; @@ -83,7 +83,7 @@ export class Gauge extends PureComponent { break; } } - formatted.push({ value: max, color: getColorFromHexRgbOrName(last.color, theme.type) }); + formatted.push({ value: +max.toFixed(decimals), color: getColorFromHexRgbOrName(last.color, theme.type) }); return formatted; } @@ -129,6 +129,12 @@ export class Gauge extends PureComponent { } } + const decimals = field.decimals === undefined ? 2 : field.decimals!; + if (showThresholdMarkers) { + min = +min.toFixed(decimals); + max = +max.toFixed(decimals); + } + const options: any = { series: { gauges: { @@ -145,7 +151,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0, vMargin: 0 }, cell: { border: { width: 0 } }, threshold: { - values: this.getFormattedThresholds(), + values: this.getFormattedThresholds(decimals), label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From b57802e61fe0d80d840224980c4eeeb100b9f1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2020 12:14:32 +0200 Subject: [PATCH 10/99] NewPanelEdit: Copy untransformed result from source panel (#24211) --- public/app/features/dashboard/state/PanelModel.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index fcd988ac060..042b256726c 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -18,11 +18,8 @@ import { ScopedVars, } from '@grafana/data'; import { EDIT_PANEL_ID } from 'app/core/constants'; - import config from 'app/core/config'; - import { PanelQueryRunner } from './PanelQueryRunner'; -import { take } from 'rxjs/operators'; export const panelAdded = eventFactory('panel-added'); export const panelRemoved = eventFactory('panel-removed'); @@ -426,10 +423,10 @@ export class PanelModel implements DataConfigSource { const sourceQueryRunner = this.getQueryRunner(); // pipe last result to new clone query runner - sourceQueryRunner - .getData() - .pipe(take(1)) - .subscribe(val => clone.getQueryRunner().pipeDataToSubject(val)); + const lastResult = sourceQueryRunner.getLastResult(); + if (lastResult) { + clone.getQueryRunner().pipeDataToSubject(lastResult); + } return clone; } From 2d5e675d4eb5837b9d5a39d5b5a6b90ef004cc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2020 13:37:43 +0200 Subject: [PATCH 11/99] PanelEdit: Fixed scroll pos moved to top when clicking new radio buttons (#24146) --- packages/grafana-data/src/field/fieldOverrides.ts | 1 + .../src/components/Forms/RadioButtonGroup/RadioButton.tsx | 2 -- .../dashboard/components/PanelEditor/OptionsPaneContent.tsx | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index c103e803bc5..866f1552b95 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -46,6 +46,7 @@ export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax { let max = Number.MIN_VALUE; const reducers = [ReducerID.min, ReducerID.max]; + for (const frame of data) { for (const field of frame.fields) { if (field.type === FieldType.number) { diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx index 16b71feeaf2..5ceccd9e7ce 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -44,8 +44,6 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt return { radio: css` position: absolute; - top: 0; - left: -100vw; opacity: 0; z-index: -1000; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx index b5cd598fc04..a05c67dad54 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx @@ -98,7 +98,7 @@ export const OptionsPaneContent: React.FC = ({ /> - + {showMainTab ? ( Date: Mon, 4 May 2020 13:58:05 +0200 Subject: [PATCH 12/99] PanelInspector: hides Query tab for plugins without Query ability (#24216) * PanelInspector: fixes so Query tab is hidden for plugins without Query ability * Refactor: changes after PR comments --- .../components/Inspector/PanelInspector.tsx | 5 +-- .../components/Inspector/QueryInspector.tsx | 5 +++ .../components/PanelEditor/utils.test.ts | 32 ++++++++++++++++++- .../dashboard/components/PanelEditor/utils.ts | 5 +++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 93404b26146..3df631e4ee7 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -27,6 +27,7 @@ import { config } from 'app/core/config'; import { getPanelInspectorStyles } from './styles'; import { StoreState } from 'app/types'; import { InspectDataTab } from './InspectDataTab'; +import { supportsDataQuery } from '../PanelEditor/utils'; interface OwnProps { dashboard: DashboardModel; @@ -269,7 +270,7 @@ export class PanelInspectorUnconnected extends PureComponent { const error = last?.error; const tabs = []; - if (plugin && !plugin.meta.skipDataQuery) { + if (supportsDataQuery(plugin)) { tabs.push({ label: 'Data', value: InspectTab.Data }); tabs.push({ label: 'Stats', value: InspectTab.Stats }); } @@ -284,7 +285,7 @@ export class PanelInspectorUnconnected extends PureComponent { tabs.push({ label: 'Error', value: InspectTab.Error }); } - if (dashboard.meta.canEdit) { + if (dashboard.meta.canEdit && supportsDataQuery(plugin)) { tabs.push({ label: 'Query', value: InspectTab.Query }); } return tabs; diff --git a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx index b4050d66024..0f71b6767c6 100644 --- a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx @@ -8,6 +8,7 @@ import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipb import { CoreEvents } from 'app/types'; import { PanelModel } from 'app/features/dashboard/state'; import { getPanelInspectorStyles } from './styles'; +import { supportsDataQuery } from '../PanelEditor/utils'; interface DsQuery { isLoading: boolean; @@ -188,6 +189,10 @@ export class QueryInspector extends PureComponent { const styles = getPanelInspectorStyles(); const haveData = Object.keys(response).length > 0; + if (!supportsDataQuery(this.props.panel.plugin)) { + return null; + } + return ( <>
diff --git a/public/app/features/dashboard/components/PanelEditor/utils.test.ts b/public/app/features/dashboard/components/PanelEditor/utils.test.ts index a76f77ac16b..66d321a976c 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.test.ts @@ -1,4 +1,5 @@ -import { FieldConfig, standardFieldConfigEditorRegistry } from '@grafana/data'; +import { FieldConfig, PanelPlugin, standardFieldConfigEditorRegistry } from '@grafana/data'; +import { supportsDataQuery } from './utils'; describe('standardFieldConfigEditorRegistry', () => { const dummyConfig: FieldConfig = { @@ -20,3 +21,32 @@ describe('standardFieldConfigEditorRegistry', () => { }); }); }); + +describe('supportsDataQuery', () => { + describe('when called with plugin that supports queries', () => { + it('then it should return true', () => { + const plugin = ({ meta: { skipDataQuery: false } } as unknown) as PanelPlugin; + expect(supportsDataQuery(plugin)).toBe(true); + }); + }); + + describe('when called with plugin that does not support queries', () => { + it('then it should return false', () => { + const plugin = ({ meta: { skipDataQuery: true } } as unknown) as PanelPlugin; + expect(supportsDataQuery(plugin)).toBe(false); + }); + }); + + describe('when called without skipDataQuery', () => { + it('then it should return false', () => { + const plugin = ({ meta: {} } as unknown) as PanelPlugin; + expect(supportsDataQuery(plugin)).toBe(false); + }); + }); + + describe('when called without plugin', () => { + it('then it should return false', () => { + expect(supportsDataQuery(undefined)).toBe(false); + }); + }); +}); diff --git a/public/app/features/dashboard/components/PanelEditor/utils.ts b/public/app/features/dashboard/components/PanelEditor/utils.ts index 636a53558c3..dae7960332e 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.ts @@ -2,6 +2,7 @@ import { CSSProperties } from 'react'; import { PanelModel } from '../../state/PanelModel'; import { DisplayMode } from './types'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; +import { PanelPlugin } from '@grafana/data'; export function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties { if (mode === DisplayMode.Fill) { @@ -24,3 +25,7 @@ export function calculatePanelSize(mode: DisplayMode, width: number, height: num height: pHeight * scale, }; } + +export function supportsDataQuery(plugin: PanelPlugin | undefined): boolean { + return plugin?.meta.skipDataQuery === false; +} From 5f621a736a99ac54d95e8ecc2ae672beb93b29b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2020 14:11:03 +0200 Subject: [PATCH 13/99] Dashboard: Go to explore now works even after discarding dashboard changes (#24149) * Explore: Fix issue with going to explore * removed console log --- public/app/features/dashboard/services/ChangeTracker.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/services/ChangeTracker.ts b/public/app/features/dashboard/services/ChangeTracker.ts index 16d1832355b..0cec93b1796 100644 --- a/public/app/features/dashboard/services/ChangeTracker.ts +++ b/public/app/features/dashboard/services/ChangeTracker.ts @@ -6,7 +6,6 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { AppEventConsumer, CoreEvents } from 'app/types'; import { appEvents } from 'app/core/app_events'; import { UnsavedChangesModal } from '../components/SaveDashboard/UnsavedChangesModal'; -import { getLocationSrv } from '@grafana/runtime'; export class ChangeTracker { current: any; @@ -188,8 +187,9 @@ export class ChangeTracker { gotoNext = () => { const baseLen = this.$location.absUrl().length - this.$location.url().length; const nextUrl = this.next.substring(baseLen); - getLocationSrv().update({ - path: nextUrl, + + this.$timeout(() => { + this.$location.url(nextUrl); }); }; } From 5a207824992d3172c02169f0b96596a76cb0dfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2020 14:11:15 +0200 Subject: [PATCH 14/99] NewPanelEdit: Fixes issue with angular panel clean up, and cleanup after leaving edit mode (#24224) --- .../components/PanelEditor/state/actions.test.ts | 9 ++++++--- public/app/features/dashboard/state/DashboardModel.ts | 1 + public/app/features/panel/panel_directive.ts | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts index 0a17952c866..d124fbb9baf 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts @@ -33,7 +33,7 @@ describe('panelEditor actions', () => { panels: [{ id: 12, type: 'graph' }], }); - const panel = sourcePanel.getEditClone(); + const panel = dashboard.initEditPanel(sourcePanel); panel.updateOptions({ prop: true }); const state: PanelEditorState = { @@ -65,7 +65,7 @@ describe('panelEditor actions', () => { panels: [{ id: 12, type: 'graph' }], }); - const panel = sourcePanel.getEditClone(); + const panel = dashboard.initEditPanel(sourcePanel); panel.type = 'table'; panel.plugin = getPanelPlugin({ id: 'table' }); panel.updateOptions({ prop: true }); @@ -77,6 +77,8 @@ describe('panelEditor actions', () => { querySubscription: { unsubscribe: jest.fn() }, }; + const panelDestroy = (panel.destroy = jest.fn()); + const dispatchedActions = await thunkTester({ panelEditor: state, dashboard: { @@ -89,6 +91,7 @@ describe('panelEditor actions', () => { expect(dispatchedActions.length).toBe(3); expect(dispatchedActions[0].type).toBe(panelModelAndPluginReady.type); expect(sourcePanel.plugin).toEqual(panel.plugin); + expect(panelDestroy.mock.calls.length).toEqual(1); }); it('should discard changes when shouldDiscardChanges is true', async () => { @@ -101,7 +104,7 @@ describe('panelEditor actions', () => { panels: [{ id: 12, type: 'graph' }], }); - const panel = sourcePanel.getEditClone(); + const panel = dashboard.initEditPanel(sourcePanel); panel.updateOptions({ prop: true }); const state: PanelEditorState = { diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index e462b17e805..5c9816c2fac 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -341,6 +341,7 @@ export class DashboardModel { } exitPanelEditor() { + this.panelInEdit.destroy(); this.panelInEdit = undefined; } diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index e3b54b33e89..07b46b76ee0 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -82,6 +82,9 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { scope.$on('$destroy', () => { elem.off(); + panel.events.emit(PanelEvents.panelTeardown); + panel.events.removeAllListeners(); + if (panelScrollbar) { panelScrollbar.dispose(); } From 53328718e195196392b6357187332001145feb0f Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 4 May 2020 05:37:52 -0700 Subject: [PATCH 15/99] Transformers: improve timeseries support (#23978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * extract out the field creation parts * extract out the field creation parts * three math modes * better timeseries support * TestData/Graph: load arrow and zoom to data range (#23764) * Docs: Fix building of docs (#23923) * Docs: Fix building of docs * CircleCI: Fixate grafana/docs-base image revision in job for building docs * Docs: enable packages reference docs for 7-beta (#23953) * added packages reference menu item. * removed the draft flag. * Updated docs by running script. * AlertTab: some ui updates (#23971) * updated the alerting tab. * changed so we use a confirm button. * removed uncommeneted import. * Change to secondary buttons Co-Authored-By: Dominik Prokop * trying to fix issue with panel of undefined. * Fix prettier * Update public/app/features/alerting/AlertTab.tsx Co-authored-by: Dominik Prokop * Docs: Query history 7.0 updates (#23955) * Update docs about query history * Update docs/sources/features/explore/index.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Metrictank: Fix meta inspector consolidator field names (#23838) to match https://github.com/grafana/metrictank/pull/1798 * Chore: Update Grafana version (#23985) * Update Grafana version * Docs: What's new in 7.0 placeholder (#23987) * Docs: What's new in 7.0 placeholder * Updated makefile * Search: minor fixes (#23984) * Search: Use folder id as key when present * Search: Do not render modals if not open * Enterprise: List 7.0 features (#23956) * CircleCI: Fix triggering of jobs for releases (#23999) Signed-off-by: Arve Knudsen * Fix pagination of issues/PR's in changelog generator (#23997) Fix pagination of issues/PR's in changelog generator * Search: Convert time pickers to CSF (#24002) * updated docs for reporting (#23733) * updated docs * peering comments * Added info about what version test mails requires * Tracing: Fix view bounds after trace change (#23994) * Docs: fix image link (#24011) * Update whats new (#24012) * Chore: Put what's new and release notes URLs in package.json (#24006) * Put what's new and release notes URLs in package.json * Upgrade build pipeline tool * Update changelog for v7.0.0-beta1 (#24007) Co-Authored-By: Marcus Efraimsson Co-Authored-By: Andrej Ocenas Co-Authored-By: Hugo Häggmark Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> * verify-repo-update: Fix Dockerfile.deb (#24030) Signed-off-by: Arve Knudsen * CircleCI: Upgrade build pipeline tool (#24021) * CircleCI: Upgrade build pipeline tool * Devenv: ignore enterprise (#24037) * Add header icon to Add data source page (#24033) * latest.json: Update testing version (#24038) Signed-off-by: Arve Knudsen * Fix login page redirected from password reset (#24032) * Storybook: Rewrite stories to CSF (#23989) * ColorPicker to CSF format * Convert stories to CSF * Do not export ClipboardButton * Update ConfirmButton * Remove unused imports * Fix feedback * changelog enterprise 7.0.0-beta1 (#24039) * CircleCI: Bump grafana/build-container revision (#24043) Signed-off-by: Arve Knudsen * Changelog: Updates changelog with more feature details (#24040) * Changelog: Updates changelog with more feature details * spell fix * spell fix * Updates * Readme update * Updates * Select: fixes so component loses focus on selecting value or pressing outside of input. (#24008) * changed the value container to a class component to get it to work with focus (maybe something with context?). * added e2e tests to verify that the select focus is working as it should. * fixed according to feedback. * updated snapshot. * Devenv: add remote renderer to grafana (#24050) * NewPanelEditor: minor UI twekas (#24042) * Forward ref for tabs, use html props * Inspect: add inspect label to drawer title * Add tooltips to sidebar pane tabs, copy changes * Remove unused import * Place tooltips over tabs * Inspector: dont show transformations select if there is only one data frame * Review * Changelog: Add a breaking change (#24051) Signed-off-by: Arve Knudsen * CircleCI: Unpin grafana/docs-base (#24054) Signed-off-by: Arve Knudsen * Search: close overlay on Esc press (#24003) * Search: Close on Esc * Search: Increase bottom padding for the last item in section * Search: Move closing search to keybindingsSrv * Search: Fix folder view * Search: Do not move folders if already in folder * Docs: Adds deprecation notice to changelog and docs for scripted dashboards (#24060) * Update CHANGELOG.md (#24047) Fix typo Co-authored-by: Daniel Lee * Documentation: Alternative Team Sync Wording (#23960) * Alternative wording for team sync docs Signed-off-by: Joe Elliott * Update docs/sources/auth/team-sync.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Fix misspell issues (#23905) * Fix misspell issues See, $ golangci-lint run --timeout 10m --disable-all -E misspell ./... Signed-off-by: Mario Trangoni * Fix codespell issues See, $ codespell -S './.git*' -L 'uint,thru,pres,unknwon,serie,referer,uptodate,durationm' Signed-off-by: Mario Trangoni * ci please? * non-empty commit - ci? * Trigger build Co-authored-by: bergquist Co-authored-by: Kyle Brandt * more tests * remove FieldConfig setting * merged binary and reduce * improve tests * update options after values change * Minor refactoring and polish to UI * Minor fixes Co-authored-by: Arve Knudsen Co-authored-by: Marcus Andersson Co-authored-by: Dominik Prokop Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Dieter Plaetinck Co-authored-by: Torkel Ödegaard Co-authored-by: Alex Khomenko Co-authored-by: Emil Tullstedt Co-authored-by: Marcus Efraimsson Co-authored-by: Jon Gyllenswärd Co-authored-by: Andrej Ocenas Co-authored-by: Hugo Häggmark Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Co-authored-by: Leonard Gram Co-authored-by: Alexander Zobnin Co-authored-by: Richard Hartmann Co-authored-by: Daniel Lee Co-authored-by: Joe Elliott Co-authored-by: Mario Trangoni Co-authored-by: bergquist Co-authored-by: Kyle Brandt --- .../transformers/calculateField.test.ts | 134 +++++++-- .../transformers/calculateField.ts | 259 +++++++++++++++--- .../grafana-data/src/utils/binaryOperators.ts | 39 +++ packages/grafana-data/src/utils/index.ts | 1 + ....test.ts => BinaryOperationVector.test.ts} | 7 +- .../src/vector/BinaryOperationVector.ts | 23 ++ .../grafana-data/src/vector/ScaledVector.ts | 22 -- packages/grafana-data/src/vector/index.ts | 2 +- .../CalculateFieldTransformerEditor.tsx | 253 ++++++++++++++--- 9 files changed, 610 insertions(+), 130 deletions(-) create mode 100644 packages/grafana-data/src/utils/binaryOperators.ts rename packages/grafana-data/src/vector/{ScaledVector.test.ts => BinaryOperationVector.test.ts} (52%) create mode 100644 packages/grafana-data/src/vector/BinaryOperationVector.ts delete mode 100644 packages/grafana-data/src/vector/ScaledVector.ts diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index e51f56bbfa0..73c0d475adf 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -4,19 +4,27 @@ import { FieldType } from '../../types/dataFrame'; import { ReducerID } from '../fieldReducer'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { transformDataFrame } from '../transformDataFrame'; -import { calculateFieldTransformer } from './calculateField'; +import { calculateFieldTransformer, CalculateFieldMode } from './calculateField'; import { DataFrameView } from '../../dataframe'; +import { BinaryOperationID } from '../../utils'; -const seriesToTestWith = toDataFrame({ +const seriesA = toDataFrame({ fields: [ - { name: 'A', type: FieldType.time, values: [1000, 2000] }, - { name: 'B', type: FieldType.number, values: [1, 100] }, - { name: 'C', type: FieldType.number, values: [2, 200] }, + { name: 'TheTime', type: FieldType.time, values: [1000, 2000] }, + { name: 'A', type: FieldType.number, values: [1, 100] }, + ], +}); + +const seriesBC = toDataFrame({ + fields: [ + { name: 'TheTime', type: FieldType.time, values: [1000, 2000] }, + { name: 'B', type: FieldType.number, values: [2, 200] }, + { name: 'C', type: FieldType.number, values: [3, 300] }, { name: 'D', type: FieldType.string, values: ['first', 'second'] }, ], }); -describe('calculateField transformer', () => { +describe('calculateField transformer w/ timeseries', () => { beforeAll(() => { mockTransformationsRegistry([calculateFieldTransformer]); }); @@ -25,28 +33,30 @@ describe('calculateField transformer', () => { const cfg = { id: DataTransformerID.calculateField, options: { - // defautls to sum + // defaults to `sum` ReduceRow alias: 'The Total', }, }; - const filtered = transformDataFrame([cfg], [seriesToTestWith])[0]; + const filtered = transformDataFrame([cfg], [seriesA, seriesBC])[0]; const rows = new DataFrameView(filtered).toArray(); expect(rows).toMatchInlineSnapshot(` Array [ Object { - "A": 1000, - "B": 1, - "C": 2, - "D": "first", - "The Total": 3, + "A {0}": 1, + "B {1}": 2, + "C {1}": 3, + "D {1}": "first", + "The Total": 6, + "TheTime": 1000, }, Object { - "A": 2000, - "B": 100, - "C": 200, - "D": "second", - "The Total": 300, + "A {0}": 100, + "B {1}": 200, + "C {1}": 300, + "D {1}": "second", + "The Total": 600, + "TheTime": 2000, }, ] `); @@ -56,20 +66,25 @@ describe('calculateField transformer', () => { const cfg = { id: DataTransformerID.calculateField, options: { - reducer: ReducerID.mean, + mode: CalculateFieldMode.ReduceRow, + reduce: { + reducer: ReducerID.mean, + }, replaceFields: true, }, }; - const filtered = transformDataFrame([cfg], [seriesToTestWith])[0]; + const filtered = transformDataFrame([cfg], [seriesA, seriesBC])[0]; const rows = new DataFrameView(filtered).toArray(); expect(rows).toMatchInlineSnapshot(` Array [ Object { - "Mean": 1.5, + "Mean": 2, + "TheTime": 1000, }, Object { - "Mean": 150, + "Mean": 200, + "TheTime": 2000, }, ] `); @@ -79,21 +94,86 @@ describe('calculateField transformer', () => { const cfg = { id: DataTransformerID.calculateField, options: { - reducer: ReducerID.mean, + mode: CalculateFieldMode.ReduceRow, + reduce: { + include: 'B', + reducer: ReducerID.mean, + }, replaceFields: true, - include: 'B', }, }; - const filtered = transformDataFrame([cfg], [seriesToTestWith])[0]; + const filtered = transformDataFrame([cfg], [seriesBC])[0]; const rows = new DataFrameView(filtered).toArray(); expect(rows).toMatchInlineSnapshot(` Array [ Object { - "Mean": 1, + "Mean": 2, + "TheTime": 1000, }, Object { - "Mean": 100, + "Mean": 200, + "TheTime": 2000, + }, + ] + `); + }); + + it('binary math', () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: 'B', + operation: BinaryOperationID.Add, + right: 'C', + }, + replaceFields: true, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesBC])[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toMatchInlineSnapshot(` + Array [ + Object { + "B + C": 5, + "TheTime": 1000, + }, + Object { + "B + C": 500, + "TheTime": 2000, + }, + ] + `); + }); + + it('field + static number', () => { + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: 'B', + operation: BinaryOperationID.Add, + right: '2', + }, + replaceFields: true, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesBC])[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toMatchInlineSnapshot(` + Array [ + Object { + "B + 2": 4, + "TheTime": 1000, + }, + Object { + "B + 2": 202, + "TheTime": 2000, }, ] `); diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 68f030932ec..84da616f0d2 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -4,79 +4,248 @@ import { ReducerID, fieldReducers } from '../fieldReducer'; import { getFieldMatcher } from '../matchers'; import { FieldMatcherID } from '../matchers/ids'; import { RowVector } from '../../vector/RowVector'; -import { ArrayVector } from '../../vector'; +import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector'; import { doStandardCalcs } from '../fieldReducer'; +import { seriesToColumnsTransformer } from './seriesToColumns'; +import { getTimeField } from '../../dataframe'; +import defaults from 'lodash/defaults'; +import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators'; -export interface CalculateFieldTransformerOptions { - reducer: ReducerID; +export enum CalculateFieldMode { + ReduceRow = 'reduceRow', + BinaryOperation = 'binary', +} + +interface ReduceOptions { include?: string; // Assume all fields - alias?: string; // The output field name - replaceFields?: boolean; + reducer: ReducerID; nullValueMode?: NullValueMode; } +interface BinaryOptions { + left: string; + operator: BinaryOperationID; + right: string; +} + +const defaultReduceOptions: ReduceOptions = { + reducer: ReducerID.sum, +}; + +const defaultBinaryOptions: BinaryOptions = { + left: '', + operator: BinaryOperationID.Add, + right: '', +}; + +export interface CalculateFieldTransformerOptions { + // True/False or auto + timeSeries?: boolean; + mode: CalculateFieldMode; // defaults to 'reduce' + + // Only one should be filled + reduce?: ReduceOptions; + binary?: BinaryOptions; + + // Remove other fields + replaceFields?: boolean; + + // Output field properties + alias?: string; // The output field name + // TODO: config?: FieldConfig; or maybe field overrides? since the UI exists +} + +type ValuesCreator = (data: DataFrame) => Vector; + export const calculateFieldTransformer: DataTransformerInfo = { id: DataTransformerID.calculateField, name: 'Add field from calculation', description: 'Use the row values to calculate a new field', defaultOptions: { - reducer: ReducerID.sum, + mode: CalculateFieldMode.ReduceRow, + reduce: { + reducer: ReducerID.sum, + }, }, transformer: options => (data: DataFrame[]) => { - let matcher = getFieldMatcher({ - id: FieldMatcherID.numeric, - }); - if (options.include && options.include.length) { - matcher = getFieldMatcher({ - id: FieldMatcherID.byName, - options: options.include, - }); + // Assume timeseries should first be joined by time + const timeFieldName = findConsistentTimeFieldName(data); + + if (data.length > 1 && timeFieldName && options.timeSeries !== false) { + data = seriesToColumnsTransformer.transformer({ + byField: timeFieldName, + })(data); } - const info = fieldReducers.get(options.reducer); - if (!info) { - throw new Error(`Unknown reducer: ${options.reducer}`); + const mode = options.mode ?? CalculateFieldMode.ReduceRow; + let creator: ValuesCreator | undefined = undefined; + + if (mode === CalculateFieldMode.ReduceRow) { + creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions)); + } else if (mode === CalculateFieldMode.BinaryOperation) { + creator = getBinaryCreator(defaults(options.binary, defaultBinaryOptions)); + } + + // Nothing configured + if (!creator) { + return data; } - const reducer = info.reduce ?? doStandardCalcs; - const ignoreNulls = options.nullValueMode === NullValueMode.Ignore; - const nullAsZero = options.nullValueMode === NullValueMode.AsZero; return data.map(frame => { - // Find the columns that should be examined - const columns: Vector[] = []; - frame.fields.forEach(field => { - if (matcher(field)) { - columns.push(field.values); - } - }); - - // Prepare a "fake" field for the row - const iter = new RowVector(columns); - const row: Field = { - name: 'temp', - values: iter, - type: FieldType.number, - config: {}, - }; - const vals: number[] = []; - for (let i = 0; i < frame.length; i++) { - iter.rowIndex = i; - row.calcs = undefined; // bust the cache (just in case) - const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer]; - vals.push(val); + // delegate field creation to the specific function + const values = creator!(frame); + if (!values) { + return frame; } const field = { - name: options.alias || info.name, + name: getResultFieldNameForCalculateFieldTransformerOptions(options), type: FieldType.number, config: {}, - values: new ArrayVector(vals), + values, }; + let fields: Field[] = []; + // Replace all fields with the single field + if (options.replaceFields) { + const { timeField } = getTimeField(frame); + if (timeField && options.timeSeries !== false) { + fields = [timeField, field]; + } else { + fields = [field]; + } + } else { + fields = [...frame.fields, field]; + } return { ...frame, - fields: options.replaceFields ? [field] : [...frame.fields, field], + fields, }; }); }, }; + +function getReduceRowCreator(options: ReduceOptions): ValuesCreator { + let matcher = getFieldMatcher({ + id: FieldMatcherID.numeric, + }); + + if (options.include && options.include.length) { + matcher = getFieldMatcher({ + id: FieldMatcherID.byName, + options: options.include, + }); + } + + const info = fieldReducers.get(options.reducer); + + if (!info) { + throw new Error(`Unknown reducer: ${options.reducer}`); + } + + const reducer = info.reduce ?? doStandardCalcs; + const ignoreNulls = options.nullValueMode === NullValueMode.Ignore; + const nullAsZero = options.nullValueMode === NullValueMode.AsZero; + + return (frame: DataFrame) => { + // Find the columns that should be examined + const columns: Vector[] = []; + for (const field of frame.fields) { + if (matcher(field)) { + columns.push(field.values); + } + } + + // Prepare a "fake" field for the row + const iter = new RowVector(columns); + const row: Field = { + name: 'temp', + values: iter, + type: FieldType.number, + config: {}, + }; + const vals: number[] = []; + + for (let i = 0; i < frame.length; i++) { + iter.rowIndex = i; + row.calcs = undefined; // bust the cache (just in case) + const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer]; + vals.push(val); + } + + return new ArrayVector(vals); + }; +} + +function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string): Vector | undefined { + if (!name) { + return undefined; + } + + for (const f of frame.fields) { + if (f.name === name) { + return f.values; + } + } + + const v = parseFloat(name); + if (!isNaN(v)) { + return new ConstantVector(v, frame.length); + } + + return undefined; +} + +function getBinaryCreator(options: BinaryOptions): ValuesCreator { + const operator = binaryOperators.getIfExists(options.operator); + + return (frame: DataFrame) => { + const left = findFieldValuesWithNameOrConstant(frame, options.left); + const right = findFieldValuesWithNameOrConstant(frame, options.right); + if (!left || !right || !operator) { + return (undefined as unknown) as Vector; + } + + return new BinaryOperationVector(left, right, operator.operation); + }; +} + +/** + * Find the name for the time field used in all frames (if one exists) + */ +function findConsistentTimeFieldName(data: DataFrame[]): string | undefined { + let name: string | undefined = undefined; + for (const frame of data) { + const { timeField } = getTimeField(frame); + if (!timeField) { + return undefined; // Not timeseries + } + if (!name) { + name = timeField.name; + } else if (name !== timeField.name) { + // Second frame has a different time column?! + return undefined; + } + } + return name; +} + +export function getResultFieldNameForCalculateFieldTransformerOptions(options: CalculateFieldTransformerOptions) { + if (options.alias?.length) { + return options.alias; + } + + if (options.mode === CalculateFieldMode.BinaryOperation) { + const { binary } = options; + return `${binary?.left ?? ''} ${binary?.operator ?? ''} ${binary?.right ?? ''}`; + } + + if (options.mode === CalculateFieldMode.ReduceRow) { + const r = fieldReducers.getIfExists(options.reduce?.reducer); + if (r) { + return r.name; + } + } + + return 'math'; +} diff --git a/packages/grafana-data/src/utils/binaryOperators.ts b/packages/grafana-data/src/utils/binaryOperators.ts new file mode 100644 index 00000000000..451b314f4dc --- /dev/null +++ b/packages/grafana-data/src/utils/binaryOperators.ts @@ -0,0 +1,39 @@ +import { RegistryItem, Registry } from './Registry'; + +export enum BinaryOperationID { + Add = '+', + Subtract = '-', + Divide = '/', + Multiply = '*', +} + +export type BinaryOperation = (left: number, right: number) => number; + +interface BinaryOperatorInfo extends RegistryItem { + operation: BinaryOperation; +} + +export const binaryOperators = new Registry(() => { + return [ + { + id: BinaryOperationID.Add, + name: 'Add', + operation: (a: number, b: number) => a + b, + }, + { + id: BinaryOperationID.Subtract, + name: 'Subtract', + operation: (a: number, b: number) => a - b, + }, + { + id: BinaryOperationID.Multiply, + name: 'Multiply', + operation: (a: number, b: number) => a * b, + }, + { + id: BinaryOperationID.Divide, + name: 'Divide', + operation: (a: number, b: number) => a / b, + }, + ]; +}); diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 1a5011ca81b..82f565eb882 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -8,6 +8,7 @@ export * from './labels'; export * from './object'; export * from './namedColorsPalette'; export * from './series'; +export * from './binaryOperators'; export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders'; export { getMappedValue } from './valueMappings'; diff --git a/packages/grafana-data/src/vector/ScaledVector.test.ts b/packages/grafana-data/src/vector/BinaryOperationVector.test.ts similarity index 52% rename from packages/grafana-data/src/vector/ScaledVector.test.ts rename to packages/grafana-data/src/vector/BinaryOperationVector.test.ts index b3951135594..c6d9955cbaf 100644 --- a/packages/grafana-data/src/vector/ScaledVector.test.ts +++ b/packages/grafana-data/src/vector/BinaryOperationVector.test.ts @@ -1,11 +1,14 @@ import { ArrayVector } from './ArrayVector'; -import { ScaledVector } from './ScaledVector'; +import { BinaryOperationVector } from './BinaryOperationVector'; +import { ConstantVector } from './ConstantVector'; +import { binaryOperators, BinaryOperationID } from '../utils/binaryOperators'; describe('ScaledVector', () => { it('should support multiply operations', () => { const source = new ArrayVector([1, 2, 3, 4]); const scale = 2.456; - const v = new ScaledVector(source, scale); + const operation = binaryOperators.get(BinaryOperationID.Multiply).operation; + const v = new BinaryOperationVector(source, new ConstantVector(scale, source.length), operation); expect(v.length).toEqual(source.length); // expect(v.push(10)).toEqual(source.length); // not implemented for (let i = 0; i < 10; i++) { diff --git a/packages/grafana-data/src/vector/BinaryOperationVector.ts b/packages/grafana-data/src/vector/BinaryOperationVector.ts new file mode 100644 index 00000000000..032be4b8ff1 --- /dev/null +++ b/packages/grafana-data/src/vector/BinaryOperationVector.ts @@ -0,0 +1,23 @@ +import { Vector } from '../types/vector'; +import { vectorToArray } from './vectorToArray'; +import { BinaryOperation } from '../utils/binaryOperators'; + +export class BinaryOperationVector implements Vector { + constructor(private left: Vector, private right: Vector, private operation: BinaryOperation) {} + + get length(): number { + return this.left.length; + } + + get(index: number): number { + return this.operation(this.left.get(index), this.right.get(index)); + } + + toArray(): number[] { + return vectorToArray(this); + } + + toJSON(): number[] { + return vectorToArray(this); + } +} diff --git a/packages/grafana-data/src/vector/ScaledVector.ts b/packages/grafana-data/src/vector/ScaledVector.ts deleted file mode 100644 index 0656291d7d0..00000000000 --- a/packages/grafana-data/src/vector/ScaledVector.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Vector } from '../types/vector'; -import { vectorToArray } from './vectorToArray'; - -export class ScaledVector implements Vector { - constructor(private source: Vector, private scale: number) {} - - get length(): number { - return this.source.length; - } - - get(index: number): number { - return this.source.get(index) * this.scale; - } - - toArray(): number[] { - return vectorToArray(this); - } - - toJSON(): number[] { - return vectorToArray(this); - } -} diff --git a/packages/grafana-data/src/vector/index.ts b/packages/grafana-data/src/vector/index.ts index 88ef69542f0..2d19d2cd07b 100644 --- a/packages/grafana-data/src/vector/index.ts +++ b/packages/grafana-data/src/vector/index.ts @@ -2,7 +2,7 @@ export * from './AppendedVectors'; export * from './ArrayVector'; export * from './CircularVector'; export * from './ConstantVector'; -export * from './ScaledVector'; +export * from './BinaryOperationVector'; export * from './SortedVector'; export { vectorator } from './FunctionalVector'; diff --git a/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx b/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx index 6cb54bea70b..cfce662033e 100644 --- a/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx +++ b/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx @@ -2,19 +2,41 @@ import React, { ChangeEvent } from 'react'; import { CalculateFieldTransformerOptions, DataTransformerID, - fieldReducers, FieldType, KeyValue, ReducerID, standardTransformers, TransformerRegistyItem, TransformerUIProps, + NullValueMode, + BinaryOperationID, + SelectableValue, + binaryOperators, } from '@grafana/data'; import { StatsPicker } from '../StatsPicker/StatsPicker'; import { Switch } from '../Forms/Legacy/Switch/Switch'; import { Input } from '../Input/Input'; import { FilterPill } from '../FilterPill/FilterPill'; import { HorizontalGroup } from '../Layout/Layout'; +import { + CalculateFieldMode, + getResultFieldNameForCalculateFieldTransformerOptions, +} from '@grafana/data/src/transformations/transformers/calculateField'; +import { Select } from '../Select/Select'; +import defaults from 'lodash/defaults'; + +// Copied from @grafana/data ;( not sure how to best support his +interface ReduceOptions { + include?: string; // Assume all fields + reducer: ReducerID; + nullValueMode?: NullValueMode; +} + +interface BinaryOptions { + left: string; + operator: BinaryOperationID; + right: string; +} interface CalculateFieldTransformerEditorProps extends TransformerUIProps {} @@ -24,14 +46,20 @@ interface CalculateFieldTransformerEditorState { selected: string[]; } +const calculationModes = [ + { value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' }, + { value: CalculateFieldMode.ReduceRow, label: 'Reduce row' }, +]; + export class CalculateFieldTransformerEditor extends React.PureComponent< CalculateFieldTransformerEditorProps, CalculateFieldTransformerEditorState > { constructor(props: CalculateFieldTransformerEditorProps) { super(props); + this.state = { - include: props.options.include || '', + include: props.options?.reduce?.include || '', names: [], selected: [], }; @@ -41,9 +69,16 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< this.initOptions(); } + componentDidUpdate(oldProps: CalculateFieldTransformerEditorProps) { + if (this.props.input !== oldProps.input) { + this.initOptions(); + } + } + private initOptions() { const { input, options } = this.props; - const configuredOptions = options.include ? options.include.split('|') : []; + const include = options?.reduce?.include || ''; + const configuredOptions = include.split('|'); const allNames: string[] = []; const byName: KeyValue = {}; @@ -78,23 +113,6 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< } } - onFieldToggle = (fieldName: string) => { - const { selected } = this.state; - if (selected.indexOf(fieldName) > -1) { - this.onChange(selected.filter(s => s !== fieldName)); - } else { - this.onChange([...selected, fieldName]); - } - }; - - onChange = (selected: string[]) => { - this.setState({ selected }); - this.props.onChange({ - ...this.props.options, - include: selected.join('|'), - }); - }; - onToggleReplaceFields = () => { const { options } = this.props; this.props.onChange({ @@ -103,6 +121,15 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< }); }; + onModeChanged = (value: SelectableValue) => { + const { options, onChange } = this.props; + const mode = value.value ?? CalculateFieldMode.BinaryOperation; + onChange({ + ...options, + mode, + }); + }; + onAliasChanged = (evt: ChangeEvent) => { const { options } = this.props; this.props.onChange({ @@ -111,20 +138,50 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< }); }; - onStatsChange = (stats: string[]) => { - this.props.onChange({ - ...this.props.options, - reducer: stats.length ? (stats[0] as ReducerID) : ReducerID.sum, + //--------------------------------------------------------- + // Reduce by Row + //--------------------------------------------------------- + + updateReduceOptions = (v: ReduceOptions) => { + const { options, onChange } = this.props; + onChange({ + ...options, + mode: CalculateFieldMode.ReduceRow, + reduce: v, }); }; - render() { - const { options } = this.props; + onFieldToggle = (fieldName: string) => { + const { selected } = this.state; + if (selected.indexOf(fieldName) > -1) { + this.onChange(selected.filter(s => s !== fieldName)); + } else { + this.onChange([...selected, fieldName]); + } + }; + + onChange = (selected: string[]) => { + this.setState({ selected }); + const { reduce } = this.props.options; + this.updateReduceOptions({ + ...reduce!, + include: selected.join('|'), + }); + }; + + onStatsChange = (stats: string[]) => { + const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum; + + const { reduce } = this.props.options; + this.updateReduceOptions({ ...reduce, reducer }); + }; + + renderReduceRow(options?: ReduceOptions) { const { names, selected } = this.state; - const reducer = fieldReducers.get(options.reducer); + options = defaults(options, { reducer: ReducerID.sum }); return ( -
+ <>
Field name
@@ -145,19 +202,149 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
-
+
Calculation
- +
+ + ); + } + + //--------------------------------------------------------- + // Binary Operator + //--------------------------------------------------------- + + updateBinaryOptions = (v: BinaryOptions) => { + const { options, onChange } = this.props; + onChange({ + ...options, + mode: CalculateFieldMode.BinaryOperation, + binary: v, + }); + }; + + onBinaryLeftChanged = (v: SelectableValue) => { + const { binary } = this.props.options; + this.updateBinaryOptions({ + ...binary!, + left: v.value!, + }); + }; + + onBinaryRightChanged = (v: SelectableValue) => { + const { binary } = this.props.options; + this.updateBinaryOptions({ + ...binary!, + right: v.value!, + }); + }; + + onBinaryOperationChanged = (v: SelectableValue) => { + const { binary } = this.props.options; + this.updateBinaryOptions({ + ...binary!, + operator: v.value! as BinaryOperationID, + }); + }; + + renderBinaryOperation(options?: BinaryOptions) { + options = defaults(options, { reducer: ReducerID.sum }); + + let foundLeft = !options?.left; + let foundRight = !options?.right; + const names = this.state.names.map(v => { + if (v === options?.left) { + foundLeft = true; + } + if (v === options?.right) { + foundRight = true; + } + return { label: v, value: v }; + }); + const leftNames = foundLeft ? names : [...names, { label: options?.left, value: options?.left }]; + const rightNames = foundRight ? names : [...names, { label: options?.right, value: options?.right }]; + + const ops = binaryOperators.list().map(v => { + return { label: v.id, value: v.id }; + }); + + return ( +
+
+
Operation
+
+
+ + v.value === mode)} + onChange={this.onModeChanged} + /> +
+
+ {mode === CalculateFieldMode.BinaryOperation && this.renderBinaryOperation(options.binary)} + {mode === CalculateFieldMode.ReduceRow && this.renderReduceRow(options.reduce)} +
+
Alias
- +
-
+
Date: Mon, 4 May 2020 14:44:25 +0200 Subject: [PATCH 16/99] e2e: upgrades Cypress to 4.50 (#24099) * Chore: upgrades Cypress to 4.5.0 * Refactor: splits up huge it into several * Refactor: prevent flakiness * Refactor: updates yarn.lock * Refactor: changes after PR comments * Refactor: uses e2e.flows instead of import --- e2e/suite1/specs/queryVariableCrud.spec.ts | 747 ++++++++++++--------- packages/grafana-e2e/package.json | 4 +- yarn.lock | 469 ++++++------- 3 files changed, 667 insertions(+), 553 deletions(-) diff --git a/e2e/suite1/specs/queryVariableCrud.spec.ts b/e2e/suite1/specs/queryVariableCrud.spec.ts index f9fa204ca80..fec2f70dfe2 100644 --- a/e2e/suite1/specs/queryVariableCrud.spec.ts +++ b/e2e/suite1/specs/queryVariableCrud.spec.ts @@ -1,74 +1,171 @@ import { e2e } from '@grafana/e2e'; -// This test should really be broken into several smaller tests -e2e.scenario({ - describeName: 'Variables', - itName: 'Query Variables CRUD', - addScenarioDataSource: true, - addScenarioDashBoard: true, - skipScenario: false, - scenario: () => { - e2e.getScenarioContext().then(({ lastAddedDashboardUid }: any) => { +// skipped scenario helper because of some perf issue upgrading cypress to 4.5.0 and splitted the whole test into smaller +// several it functions. Very important to keep the order of these it functions because they have dependency in the order +// https://github.com/cypress-io/cypress/issues/5987 +// https://github.com/cypress-io/cypress/issues/6023#issuecomment-574031655 +describe('Variables', () => { + let lastUid = ''; + let lastData = ''; + let variables: VariablesData[] = [ + { name: 'query1', query: '*', label: 'query1-label', options: ['All', 'A', 'B', 'C'], selectedOption: 'A' }, + { + name: 'query2', + query: '$query1.*', + label: 'query2-label', + options: ['All', 'AA', 'AB', 'AC'], + selectedOption: 'AA', + }, + { + name: 'query3', + query: '$query1.$query2.*', + label: 'query3-label', + options: ['All', 'AAA', 'AAB', 'AAC'], + selectedOption: 'AAA', + }, + ]; + + beforeEach(() => { + e2e.flows.login('admin', 'admin'); + if (!lastUid || !lastData) { + e2e.flows.addDataSource(); + e2e.flows.addDashboard(); + } else { + e2e.setScenarioContext({ lastAddedDataSource: lastData, lastAddedDashboardUid: lastUid }); + } + + e2e.getScenarioContext().then(({ lastAddedDashboardUid, lastAddedDataSource }: any) => { e2e.flows.openDashboard(lastAddedDashboardUid); + lastUid = lastAddedDashboardUid; + lastData = lastAddedDataSource; }); + }); + + it(`asserts defaults`, () => { e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click(); assertDefaultsForNewVariable(); + }); - e2e.pages.Dashboard.Settings.General.sectionItems('General').click(); - e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); - e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click(); + variables.forEach((variable, index) => { + it(`creates variable ${variable.name}`, () => { + e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); + e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); - let queryVariables: QueryVariableData[] = [ - { - name: 'query1', - query: '*', - label: 'query1-label', - options: ['All', 'A', 'B', 'C'], - selectedOption: 'A', - }, - { - name: 'query2', - query: '$query1.*', - label: 'query2-label', - options: ['All', 'AA', 'AB', 'AC'], - selectedOption: 'AA', - }, - { - name: 'query3', - query: '$query1.$query2.*', - label: 'query3-label', - options: ['All', 'AAA', 'AAB', 'AAC'], - selectedOption: 'AAA', - }, - ]; + if (index === 0) { + e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click(); + } else { + e2e.pages.Dashboard.Settings.Variables.List.newButton().click(); + } - assertAdding3dependantQueryVariablesScenario(queryVariables); + const { name, label, query, options, selectedOption } = variable; + e2e.getScenarioContext().then(({ lastAddedDataSource }: any) => { + createQueryVariable({ + dataSourceName: lastAddedDataSource, + name, + label, + query, + options, + selectedOption, + }); + }); - // assert select updates - assertSelects(queryVariables); + e2e.pages.Dashboard.Settings.General.saveDashBoard() + .should('be.visible') + .click(); + e2e.pages.SaveDashboardModal.save() + .should('be.visible') + .click(); + e2e.flows.assertSuccessNotification(); - // assert that duplicate works - queryVariables = assertDuplicateItem(queryVariables); + e2e.components.BackButton.backArrow() + .should('be.visible') + .click(); + }); + }); - // assert that delete works - queryVariables = assertDeleteItem(queryVariables); + it(`asserts submenus`, () => { + assertVariableLabelsAndComponents(variables); + }); - // assert that update works - queryVariables = assertUpdateItem(queryVariables); + it(`asserts variable table`, () => { + e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Settings.General.sectionItems('Variables') + .should('be.visible') + .click(); - // assert that move down works - queryVariables = assertMoveDownItem(queryVariables); + assertVariableTable(variables); + }); - // assert that move up works - assertMoveUpItem(queryVariables); - }, + it(`asserts variable selects`, () => { + assertSelects(variables); + }); + + it(`asserts duplicate variable`, () => { + // mutates variables + variables = assertDuplicateItem(variables); + e2e.flows.saveDashboard(); + }); + + it(`asserts delete variable`, () => { + // mutates variables + variables = assertDeleteItem(variables); + e2e.flows.saveDashboard(); + }); + + it(`asserts update variable`, () => { + // mutates variables + variables = assertUpdateItem(variables); + e2e.components.BackButton.backArrow() + .should('be.visible') + .should('be.visible') + .click(); + e2e.flows.saveDashboard(); + }); + + it(`asserts move variable down`, () => { + e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Settings.General.sectionItems('Variables') + .should('be.visible') + .click(); + + // mutates variables + variables = assertMoveDownItem(variables); + e2e.flows.saveDashboard(); + }); + + it(`asserts move variable up`, () => { + e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Settings.General.sectionItems('Variables') + .should('be.visible') + .click(); + + // mutates variables + assertMoveUpItem(variables); + }); }); +interface VariablesData { + name: string; + query: string; + label: string; + options: string[]; + selectedOption: string; +} + +interface CreateQueryVariableArguments extends VariablesData { + dataSourceName: string; +} + const assertDefaultsForNewVariable = () => { - logSection('Asserting defaults for new variable'); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().within(input => { expect(input.attr('placeholder')).equals('name'); expect(input.val()).equals(''); @@ -88,18 +185,11 @@ const assertDefaultsForNewVariable = () => { .should('have.text', ''); }); - e2e() - .window() - .then((win: any) => { - const chainer = 'have.text'; - const value = ''; - - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().within(select => { - e2e() - .get('option:selected') - .should(chainer, value); - }); - }); + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().within(select => { + e2e() + .get('option:selected') + .should('have.text', ''); + }); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().should('not.exist'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect().within(select => { @@ -133,32 +223,16 @@ const assertDefaultsForNewVariable = () => { }); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); - logSection('Asserting defaults for new variable, OK!'); }; -interface CreateQueryVariableArguments extends QueryVariableData { - dataSourceName: string; -} - const createQueryVariable = ({ name, label, dataSourceName, query }: CreateQueryVariableArguments) => { - logSection('Creating a Query Variable with', { name, label, dataSourceName, query }); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().should('be.visible'); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().type(name); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label); - e2e() - .window() - .then((win: any) => { - const text = `${dataSourceName}`; - - e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect() - .select(text) - .blur(); - }); + e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect() + .select(`${dataSourceName}`) + .blur(); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput() - .within(input => { - expect(input.attr('placeholder')).equals('metric name or tags query'); - expect(input.val()).equals(''); - }) .type(query) .blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist'); @@ -181,10 +255,34 @@ const createQueryVariable = ({ name, label, dataSourceName, query }: CreateQuery expect(input.val()).equals(''); }); e2e.pages.Dashboard.Settings.Variables.Edit.General.addButton().click(); - logSection('Creating a Query Variable with required, OK!'); }; -const assertVariableTableRow = ({ name, query }: QueryVariableData, index: number, length: number) => { +const assertVariableLabelAndComponent = ({ label, options, selectedOption }: VariablesData) => { + e2e.pages.Dashboard.SubMenu.submenuItemLabels(label).should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(selectedOption) + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown().should('be.visible'); + for (let optionIndex = 0; optionIndex < options.length; optionIndex++) { + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(options[optionIndex]).should('be.visible'); + } +}; + +const assertVariableLabelsAndComponents = (args: VariablesData[]) => { + e2e.pages.Dashboard.SubMenu.submenuItem().should('have.length', args.length); + for (let index = 0; index < args.length; index++) { + e2e.pages.Dashboard.SubMenu.submenuItem() + .eq(index) + .within(() => { + e2e() + .get('label') + .contains(args[index].name); + }); + assertVariableLabelAndComponent(args[index]); + } +}; + +const assertVariableTableRow = ({ name, query }: VariablesData, index: number, length: number) => { e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(name) .should('exist') .contains(name); @@ -201,8 +299,7 @@ const assertVariableTableRow = ({ name, query }: QueryVariableData, index: numbe e2e.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(name).should('exist'); }; -const assertVariableTable = (args: QueryVariableData[]) => { - logSection('Asserting variable table with', args); +const assertVariableTable = (args: VariablesData[]) => { e2e.pages.Dashboard.Settings.Variables.List.table() .should('be.visible') .within(() => { @@ -214,90 +311,197 @@ const assertVariableTable = (args: QueryVariableData[]) => { for (let index = 0; index < args.length; index++) { assertVariableTableRow(args[index], index, args.length); } - - logSection('Asserting variable table, Ok'); }; -const assertVariableLabelAndComponent = ({ label, options, selectedOption }: QueryVariableData) => { - e2e.pages.Dashboard.SubMenu.submenuItemLabels(label).should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(selectedOption) +const assertSelects = (variables: VariablesData[]) => { + // Values in submenus should be + // query1: [A] query2: [AA] query3: [AAA] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') .should('be.visible') .click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown().should('be.visible'); - for (let optionIndex = 0; optionIndex < options.length; optionIndex++) { - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(options[optionIndex]).should('be.visible'); - } + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar().click(); + // Values in submenus should be + // query1: [B] query2: [All] query3: [All] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .should('be.visible') + .should('have.length', 2); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .eq(0) + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + // Values in submenus should be + // query1: [B] query2: [BB] query3: [All] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .eq(0) + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0); + // Values in submenus should be + // query1: [B] query2: [BB] query3: [BBB] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC') + .should('be.visible') + .should('have.length', 1); + // Values in submenus should be + // query1: [B] query2: [BB + BC] query3: [BBB] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB + BCC') + .should('be.visible') + .should('have.length', 1); + // Values in submenus should be + // query1: [B] query2: [BB + BC] query3: [BBB + BCC] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BA') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .should('be.visible') + .should('have.length', 1); + // Values in submenus should be + // query1: [B] query2: [BA] query3: [All] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A') + .should('be.visible') + .should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B') + .should('be.visible') + .should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C') + .should('be.visible') + .should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A') + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .should('be.visible') + .should('have.length', 2); + // Values in submenus should be + // query1: [A] query2: [All] query3: [All] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .eq(0) + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .should('be.visible') + .should('have.length', 1); + // Values in submenus should be + // query1: [A] query2: [AA] query3: [All] + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + .eq(0) + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA') + .should('be.visible') + .click(); + e2e.pages.Dashboard.Toolbar.navBar() + .should('be.visible') + .click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA') + .should('be.visible') + .should('have.length', 1); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0); }; -const assertVariableLabelsAndComponents = (args: QueryVariableData[]) => { - logSection('Asserting variable components and labels'); - e2e.pages.Dashboard.SubMenu.submenuItem().should('have.length', args.length); - for (let index = 0; index < args.length; index++) { - e2e.pages.Dashboard.SubMenu.submenuItem() - .eq(index) - .within(() => { - e2e() - .get('label') - .contains(args[index].name); - }); - assertVariableLabelAndComponent(args[index]); - } - logSection('Asserting variable components and labels, Ok'); -}; - -const assertAdding3dependantQueryVariablesScenario = (queryVariables: QueryVariableData[]) => { - // This creates 3 variables where 2 depends on 1 and 3 depends on 2 and for each added variable - // we assert that the variable looks ok in the variable list and that it looks ok in the submenu in dashboard - for (let queryVariableIndex = 0; queryVariableIndex < queryVariables.length; queryVariableIndex++) { - const { name, label, query, options, selectedOption } = queryVariables[queryVariableIndex]; - const asserts = queryVariables.slice(0, queryVariableIndex + 1); - e2e.getScenarioContext().then(({ lastAddedDataSource }: any) => { - createQueryVariable({ - dataSourceName: lastAddedDataSource, - name, - label, - query, - options, - selectedOption, - }); - }); - - assertVariableTable(asserts); - - e2e.pages.Dashboard.Settings.General.saveDashBoard().click(); - e2e.pages.SaveDashboardModal.save().click(); - e2e.flows.assertSuccessNotification(); - - e2e.components.BackButton.backArrow().click(); - - assertVariableLabelsAndComponents(asserts); - - if (queryVariableIndex < queryVariables.length - 1) { - e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); - e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); - e2e.pages.Dashboard.Settings.Variables.List.newButton().click(); - } - } -}; - -interface QueryVariableData { - name: string; - query: string; - label: string; - options: string[]; - selectedOption: string; -} - -const logSection = (message: string, args?: any) => { - e2e().logToConsole(''); - e2e().logToConsole(message, args); - e2e().logToConsole('==============================================================================='); -}; - -const assertDuplicateItem = (queryVariables: QueryVariableData[]) => { - logSection('Asserting variable duplicate'); - - const itemToDuplicate = queryVariables[1]; +const assertDuplicateItem = (variables: VariablesData[]) => { + const itemToDuplicate = variables[1]; e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); e2e.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(itemToDuplicate.name) @@ -308,10 +512,10 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => { .within(() => { e2e() .get('tbody > tr') - .should('have.length', queryVariables.length + 1); + .should('have.length', variables.length + 1); }); const newItem = { ...itemToDuplicate, name: `copy_of_${itemToDuplicate.name}` }; - assertVariableTableRow(newItem, queryVariables.length - 1, queryVariables.length); + assertVariableTableRow(newItem, variables.length - 1, variables.length); e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(newItem.name).click(); newItem.label = `copy_of_${itemToDuplicate.label}`; @@ -323,7 +527,9 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => { e2e.pages.SaveDashboardModal.save().click(); e2e.flows.assertSuccessNotification(); - e2e.components.BackButton.backArrow().click(); + e2e.components.BackButton.backArrow() + .should('be.visible') + .click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels(newItem.label).should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(newItem.selectedOption) @@ -335,14 +541,11 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(newItem.options[optionIndex]).should('be.visible'); } - logSection('Asserting variable duplicate, OK!'); - return [...queryVariables, newItem]; + return [...variables, newItem]; }; -const assertDeleteItem = (queryVariables: QueryVariableData[]) => { - logSection('Asserting variable delete'); - - const itemToDelete = queryVariables[1]; +const assertDeleteItem = (variables: VariablesData[]) => { + const itemToDelete = variables[1]; e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); @@ -352,26 +555,26 @@ const assertDeleteItem = (queryVariables: QueryVariableData[]) => { .within(() => { e2e() .get('tbody > tr') - .should('have.length', queryVariables.length - 1); + .should('have.length', variables.length - 1); }); e2e.pages.Dashboard.Settings.General.saveDashBoard().click(); e2e.pages.SaveDashboardModal.save().click(); e2e.flows.assertSuccessNotification(); - e2e.components.BackButton.backArrow().click(); + e2e.components.BackButton.backArrow() + .should('be.visible') + .click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels(itemToDelete.label).should('not.exist'); - logSection('Asserting variable delete, OK!'); - - return queryVariables.filter(item => item.name !== itemToDelete.name); + return variables.filter(item => item.name !== itemToDelete.name); }; -const assertUpdateItem = (data: QueryVariableData[]) => { - const queryVariables = [...data]; +const assertUpdateItem = (data: VariablesData[]) => { + const variables = [...data]; // updates an item to a constant variable instead - const itemToUpdate = queryVariables[1]; + const itemToUpdate = variables[1]; let updatedItem = { ...itemToUpdate, name: `update_of_${itemToUpdate.name}`, @@ -381,8 +584,7 @@ const assertUpdateItem = (data: QueryVariableData[]) => { selectedOption: 'undefined', }; - logSection('Asserting variable update'); - queryVariables[1] = updatedItem; + variables[1] = updatedItem; e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); @@ -406,33 +608,29 @@ const assertUpdateItem = (data: QueryVariableData[]) => { e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select(''); e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(updatedItem.query); - e2e.components.BackButton.backArrow().click(); + e2e.components.BackButton.backArrow() + .should('be.visible') + .click(); - e2e() - .window() - .then((win: any) => { - queryVariables[1].selectedOption = 'A constant'; - assertVariableLabelAndComponent(queryVariables[1]); - }); + variables[1].selectedOption = 'A constant'; + assertVariableLabelAndComponent(variables[1]); e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); - assertVariableTableRow(queryVariables[1], 1, queryVariables.length); + assertVariableTableRow(variables[1], 1, variables.length); - queryVariables[1].selectedOption = 'A constant'; + variables[1].selectedOption = 'A constant'; - logSection('Asserting variable update, OK!'); - return queryVariables; + return variables; }; -const assertMoveDownItem = (data: QueryVariableData[]) => { - logSection('Asserting variable move down'); - const queryVariables = [...data]; - e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(queryVariables[0].name).click(); - const temp = { ...queryVariables[0] }; - queryVariables[0] = { ...queryVariables[1] }; - queryVariables[1] = temp; +const assertMoveDownItem = (data: VariablesData[]) => { + const variables = [...data]; + e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(variables[0].name).click(); + const temp = { ...variables[0] }; + variables[0] = { ...variables[1] }; + variables[1] = temp; e2e.pages.Dashboard.Settings.Variables.List.table().within(() => { e2e() .get('tbody > tr') @@ -441,11 +639,11 @@ const assertMoveDownItem = (data: QueryVariableData[]) => { e2e() .get('td') .eq(0) - .contains(queryVariables[0].name); + .contains(variables[0].name); e2e() .get('td') .eq(1) - .contains(queryVariables[0].query); + .contains(variables[0].query); }); e2e() .get('tbody > tr') @@ -454,130 +652,29 @@ const assertMoveDownItem = (data: QueryVariableData[]) => { e2e() .get('td') .eq(0) - .contains(queryVariables[1].name); + .contains(variables[1].name); e2e() .get('td') .eq(1) - .contains(queryVariables[1].query); + .contains(variables[1].query); }); }); - e2e.components.BackButton.backArrow().click(); + e2e.components.BackButton.backArrow() + .should('be.visible') + .click(); - assertVariableLabelsAndComponents(queryVariables); + assertVariableLabelsAndComponents(variables); - logSection('Asserting variable move down, OK!'); - - return queryVariables; + return variables; }; -const assertSelects = (queryVariables: QueryVariableData[]) => { - // Values in submenus should be - // query1: [A] query2: [AA] query3: [AAA] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - // Values in submenus should be - // query1: [B] query2: [All] query3: [All] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 2); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') - .eq(0) - .click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - // Values in submenus should be - // query1: [B] query2: [BB] query3: [All] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') - .eq(0) - .click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0); - // Values in submenus should be - // query1: [B] query2: [BB] query3: [BBB] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC').should('have.length', 1); - // Values in submenus should be - // query1: [B] query2: [BB + BC] query3: [BBB] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB + BCC').should('have.length', 1); - // Values in submenus should be - // query1: [B] query2: [BB + BC] query3: [BBB + BCC] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BA').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 1); - // Values in submenus should be - // query1: [B] query2: [BA] query3: [All] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 2); - // Values in submenus should be - // query1: [A] query2: [All] query3: [All] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') - .eq(0) - .click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 1); - // Values in submenus should be - // query1: [A] query2: [AA] query3: [All] - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') - .eq(0) - .click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').click(); - e2e.pages.Dashboard.Toolbar.navBar().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA').should('have.length', 1); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0); -}; - -const assertMoveUpItem = (data: QueryVariableData[]) => { - logSection('Asserting variable move up'); - const queryVariables = [...data]; - e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click(); - e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click(); - - e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(queryVariables[1].name).click(); - const temp = { ...queryVariables[0] }; - queryVariables[0] = { ...queryVariables[1] }; - queryVariables[1] = temp; +const assertMoveUpItem = (data: VariablesData[]) => { + const variables = [...data]; + e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(variables[1].name).click(); + const temp = { ...variables[0] }; + variables[0] = { ...variables[1] }; + variables[1] = temp; e2e.pages.Dashboard.Settings.Variables.List.table().within(() => { e2e() .get('tbody > tr') @@ -586,11 +683,11 @@ const assertMoveUpItem = (data: QueryVariableData[]) => { e2e() .get('td') .eq(0) - .contains(queryVariables[0].name); + .contains(variables[0].name); e2e() .get('td') .eq(1) - .contains(queryVariables[0].query); + .contains(variables[0].query); }); e2e() .get('tbody > tr') @@ -599,19 +696,19 @@ const assertMoveUpItem = (data: QueryVariableData[]) => { e2e() .get('td') .eq(0) - .contains(queryVariables[1].name); + .contains(variables[1].name); e2e() .get('td') .eq(1) - .contains(queryVariables[1].query); + .contains(variables[1].query); }); }); - e2e.components.BackButton.backArrow().click(); + e2e.components.BackButton.backArrow() + .should('be.visible') + .click(); - assertVariableLabelsAndComponents(queryVariables); + assertVariableLabelsAndComponents(variables); - logSection('Asserting variable move up, OK!'); - - return queryVariables; + return variables; }; diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 2bf51536243..8ab43b535b3 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -26,7 +26,7 @@ "docsExtract": "mkdir -p ../../reports/docs && api-extractor run 2>&1 | tee ../../reports/docs/$(basename $(pwd)).log", "lint": "eslint cypress/ src/ --ext=.js,.ts,.tsx", "open": "cypress open", - "start": "cypress run", + "start": "cypress run --headless --browser chrome", "typecheck": "tsc --noEmit" }, "devDependencies": { @@ -48,7 +48,7 @@ "@grafana/tsconfig": "^1.0.0-rc1", "blink-diff": "1.0.13", "commander": "5.0.0", - "cypress": "3.7.0", + "cypress": "4.5.0", "execa": "4.0.0", "ts-loader": "6.2.1", "typescript": "3.7.5", diff --git a/yarn.lock b/yarn.lock index 98e916b50fc..23c70036c05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2893,6 +2893,32 @@ date-fns "^1.27.2" figures "^1.7.0" +"@cypress/request@2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + "@cypress/webpack-preprocessor@4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.1.3.tgz#d5fad767a304c16ec05ca08034827c601f1c9c0c" @@ -5068,6 +5094,16 @@ dependencies: "@types/babel-types" "*" +"@types/blob-util@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" + integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== + +"@types/bluebird@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== + "@types/body-parser@*": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897" @@ -5081,6 +5117,24 @@ resolved "https://registry.yarnpkg.com/@types/braintree__sanitize-url/-/braintree__sanitize-url-4.0.0.tgz#0e8a834501f8c375d4b3fb8dcf9398a08ebe068d" integrity sha512-69eGJ8808/WfTJGsvMi1pxQ9UG5Z+llD1x9ash5QX+qvxElDD+eYNAn19cTEVTq6WwUqrqlaTWVCKaTRFTuGmA== +"@types/chai-jquery@1.1.40": + version "1.1.40" + resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" + integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== + dependencies: + "@types/chai" "*" + "@types/jquery" "*" + +"@types/chai@*": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + +"@types/chai@4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d" + integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g== + "@types/cheerio@*": version "0.22.13" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6" @@ -5614,11 +5668,25 @@ dependencies: "@types/jest-diff" "*" +"@types/jquery@*": + version "3.3.36" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.36.tgz#a868c1c244a9f7b988d8fc56a3234f22e73c57b8" + integrity sha512-jHL8J5y5fJ0+C9zCTkeOvX4zqRnPug3r6JhAqAYl2YyBCYHiXTbZSH0MRCpayZADed5TigPjH92dEKczUFT2TQ== + dependencies: + "@types/sizzle" "*" + "@types/jquery@1.10.35": version "1.10.35" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-1.10.35.tgz#4e5c2b1e5b3bf0b863efb8c5e70081f52e6c9518" integrity sha512-SVtqEcudm7yjkTwoRA1gC6CNMhGDdMx4Pg8BPdiqI7bXXdCn1BPmtxgeWYQOgDxrq53/5YTlhq5ULxBEAlWIBg== +"@types/jquery@3.3.31": + version "3.3.31" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" + integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== + dependencies: + "@types/sizzle" "*" + "@types/jquery@3.3.32": version "3.3.32" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.32.tgz#93e27fdc45dd38ee07f2f0acf34b59c1ccee036f" @@ -5683,11 +5751,16 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/minimatch@*", "@types/minimatch@^3.0.3": +"@types/minimatch@*", "@types/minimatch@3.0.3", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/mocha@5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + "@types/moment-timezone@0.5.13": version "0.5.13" resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.13.tgz#0317ccc91eb4c7f4901704166166395c39276528" @@ -6075,11 +6148,36 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/sinon-chai@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" + integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.0.tgz#5b70a360f55645dd64f205defd2a31b749a59799" + integrity sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@7.5.1": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" + integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== + "@types/sinon@^7.5.2": version "7.5.2" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.2.tgz#5e2f1d120f07b9cda07e5dedd4f3bf8888fccdb9" integrity sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg== +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + "@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -6866,7 +6964,7 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: +ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= @@ -7303,13 +7401,6 @@ async-retry@^1.1.4: dependencies: retry "0.12.0" -async@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== - dependencies: - lodash "^4.17.10" - async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -7322,6 +7413,11 @@ async@^2.0.0, async@^2.1.4, async@^2.6.1, async@^2.6.2: dependencies: lodash "^4.17.14" +async@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + async@~0.2.6: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" @@ -7937,16 +8033,16 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" - integrity sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw= - bluebird@3.7.1, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: version "3.7.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== +bluebird@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -8359,12 +8455,10 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -cachedir@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-1.3.0.tgz#5e01928bf2d95b5edd94b0942188246740e0dbc4" - integrity sha512-O1ji32oyON9laVPJL1IZ5bmwd2cB46VfpxkDequezH+15FDzzVddEyrGEeX4WusDSqKxdyFdDQDEG1yo1GoWkg== - dependencies: - os-homedir "^1.0.1" +cachedir@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== calculate-size@1.1.1: version "1.1.1" @@ -8487,11 +8581,6 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-db@1.0.30000772: - version "1.0.30000772" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b" - integrity sha1-UarokXaChureSj2DGep21qAbUSs= - caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30000999: version "1.0.30000999" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz#427253a69ad7bea4aa8d8345687b8eec51ca0e43" @@ -8826,11 +8915,6 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-spinners@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" - integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= - cli-spinners@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" @@ -9111,11 +9195,6 @@ commander@2, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== - commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -9140,16 +9219,16 @@ commander@2.9.x: dependencies: graceful-readlink ">= 1.0.0" +commander@4.1.0, commander@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" + integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== + commander@5.0.0, commander@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.0.0.tgz#dbf1909b49e5044f8fdaf0adc809f0c0722bdfd0" integrity sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ== -commander@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" - integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== - commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -9235,7 +9314,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0: +concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -10016,41 +10095,55 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.7.0.tgz#e2cd71b87b6ce0d4c72c6ea25da1005d75c1f231" - integrity sha512-o+vfRxqAba8TduelzfZQ4WHmj2yNEjaoO2EuZ8dZ9pJpuW+WGtBGheKIp6zkoQsp8ZgFe8OoHh1i2mY8BDnMAw== +cypress@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" + integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" + "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" + "@types/blob-util" "1.3.3" + "@types/bluebird" "3.5.29" + "@types/chai" "4.2.7" + "@types/chai-jquery" "1.1.40" + "@types/jquery" "3.3.31" + "@types/lodash" "4.14.149" + "@types/minimatch" "3.0.3" + "@types/mocha" "5.2.7" + "@types/sinon" "7.5.1" + "@types/sinon-chai" "3.2.3" "@types/sizzle" "2.3.2" arch "2.1.1" - bluebird "3.5.0" - cachedir "1.3.0" + bluebird "3.7.2" + cachedir "2.3.0" chalk "2.4.2" check-more-types "2.24.0" - commander "2.15.1" + cli-table3 "0.5.1" + commander "4.1.0" common-tags "1.8.0" - debug "3.2.6" - execa "0.10.0" + debug "4.1.1" + eventemitter2 "4.1.2" + execa "1.0.0" executable "4.1.1" - extract-zip "1.6.7" - fs-extra "5.0.0" - getos "3.1.1" - is-ci "1.2.1" + extract-zip "1.7.0" + fs-extra "8.1.0" + getos "3.1.4" + is-ci "2.0.0" is-installed-globally "0.1.0" lazy-ass "1.6.0" - listr "0.12.0" + listr "0.14.3" lodash "4.17.15" - log-symbols "2.2.0" - minimist "1.2.0" + log-symbols "3.0.0" + minimist "1.2.5" moment "2.24.0" - ramda "0.24.1" - request "2.88.0" + ospath "1.2.2" + pretty-bytes "5.3.0" + ramda "0.26.1" request-progress "3.0.0" - supports-color "5.5.0" + supports-color "7.1.0" tmp "0.1.0" - untildify "3.0.3" + untildify "4.0.0" url "0.11.0" yauzl "2.10.0" @@ -10373,7 +10466,7 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= -debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: +debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -11693,6 +11786,11 @@ event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" +eventemitter2@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15" + integrity sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU= + eventemitter2@~0.4.13: version "0.4.14" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" @@ -11733,13 +11831,13 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== -execa@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== +execa@1.0.0, execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: cross-spawn "^6.0.0" - get-stream "^3.0.0" + get-stream "^4.0.0" is-stream "^1.1.0" npm-run-path "^2.0.0" p-finally "^1.0.0" @@ -11774,19 +11872,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -11956,7 +12041,17 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.6.7, extract-zip@^1.6.6: +extract-zip@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + +extract-zip@^1.6.6: version "1.6.7" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= @@ -12615,15 +12710,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" - integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@8.1.0, fs-extra@^8.0.1, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -12897,12 +12983,12 @@ getobject@~0.1.0: resolved "https://registry.yarnpkg.com/getobject/-/getobject-0.1.0.tgz#047a449789fa160d018f5486ed91320b6ec7885c" integrity sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw= -getos@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.1.tgz#967a813cceafee0156b0483f7cffa5b3eff029c5" - integrity sha512-oUP1rnEhAr97rkitiszGP9EgDVYnmchgFzfqRzSkgtfv7ai6tEi7Ko8GgjNXts7VLWEqrTWyhsOKLe5C5b/Zkg== +getos@3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.4.tgz#29cdf240ed10a70c049add7b6f8cb08c81876faf" + integrity sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw== dependencies: - async "2.6.1" + async "^3.1.0" getpass@^0.1.1: version "0.1.7" @@ -13487,7 +13573,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0: +har-validator@~5.1.0, har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -14507,20 +14593,20 @@ is-callable@^1.1.5: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== -is-ci@1.2.1, is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - -is-ci@^2.0.0: +is-ci@2.0.0, is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== dependencies: ci-info "^2.0.0" +is-ci@^1.0.10: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== + dependencies: + ci-info "^1.5.0" + is-color-stop@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" @@ -16013,20 +16099,6 @@ listr-silent-renderer@^1.1.1: resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= -listr-update-renderer@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" - integrity sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - strip-ansi "^3.0.1" - listr-update-renderer@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" @@ -16041,16 +16113,6 @@ listr-update-renderer@^0.5.0: log-update "^2.3.0" strip-ansi "^3.0.1" -listr-verbose-renderer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" - integrity sha1-ggb0z21S3cWCfl/RSYng6WWTOjU= - dependencies: - chalk "^1.1.3" - cli-cursor "^1.0.2" - date-fns "^1.27.2" - figures "^1.7.0" - listr-verbose-renderer@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" @@ -16061,29 +16123,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" - integrity sha1-a84sD1YD+klYDqF81qAMwOX6RRo= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - figures "^1.7.0" - indent-string "^2.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.2.0" - listr-verbose-renderer "^0.4.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - ora "^0.2.3" - p-map "^1.1.1" - rxjs "^5.0.0-beta.11" - stream-to-observable "^0.1.0" - strip-ansi "^3.0.1" - -listr@^0.14.3: +listr@0.14.3, listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -16370,27 +16410,19 @@ log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= - dependencies: - chalk "^1.0.0" - -log-symbols@^3.0.0: +log-symbols@3.0.0, log-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== dependencies: chalk "^2.4.2" -log-update@^1.0.2: +log-symbols@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" - integrity sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE= + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= dependencies: - ansi-escapes "^1.0.0" - cli-cursor "^1.0.2" + chalk "^1.0.0" log-update@^2.3.0: version "2.3.0" @@ -17007,16 +17039,16 @@ minimist@1.1.x: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= -minimist@1.2.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@^1.2.5: +minimist@1.2.5, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -17118,6 +17150,13 @@ mkdirp@^0.5.3: dependencies: minimist "^1.2.5" +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mocha@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce" @@ -18103,16 +18142,6 @@ optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" -ora@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" - integrity sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q= - dependencies: - chalk "^1.1.1" - cli-cursor "^1.0.2" - cli-spinners "^0.1.2" - object-assign "^4.0.1" - ora@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.3.tgz#752a1b7b4be4825546a7a3d59256fa523b6b6d05" @@ -18139,7 +18168,7 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0, os-homedir@^1.0.1: +os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -18181,6 +18210,11 @@ osenv@0, osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +ospath@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -18257,11 +18291,6 @@ p-map-series@^1.0.0: dependencies: p-reduce "^1.0.0" -p-map@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" - integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== - p-map@^2.0.0, p-map@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -19637,6 +19666,11 @@ prettier@^1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== +pretty-bytes@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" + integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" @@ -20139,10 +20173,10 @@ railroad-diagrams@^1.0.0: resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= -ramda@0.24.1: - version "0.24.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857" - integrity sha1-w7d1UZfzW43DUCIoJixMkd22uFc= +ramda@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== ramda@^0.21.0: version "0.21.0" @@ -21605,7 +21639,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@2.88.0, "request@>=2.76.0 <3.0.0", request@^2.55.0, request@^2.83.0, request@^2.87.0, request@^2.88.0: +"request@>=2.76.0 <3.0.0", request@^2.55.0, request@^2.83.0, request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -22041,13 +22075,6 @@ rxjs@6.5.5, rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3: dependencies: tslib "^1.9.0" -rxjs@^5.0.0-beta.11: - version "5.5.12" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" - integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== - dependencies: - symbol-observable "1.0.1" - safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -23150,11 +23177,6 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= -stream-to-observable@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" - integrity sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4= - streamroller@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.4.1.tgz#d435bd5974373abd9bd9068359513085106cc05f" @@ -23477,13 +23499,6 @@ stylis@3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== -supports-color@5.5.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" @@ -23498,6 +23513,13 @@ supports-color@6.1.0, supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@7.1.0, supports-color@^7.0.0, supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -23510,12 +23532,12 @@ supports-color@^4.5.0: dependencies: has-flag "^2.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== +supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: - has-flag "^4.0.0" + has-flag "^3.0.0" svg-parser@^2.0.0: version "2.0.2" @@ -23549,11 +23571,6 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -symbol-observable@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" - integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= - symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -24096,7 +24113,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.3, tough-cookie@^2.3.4: +tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -24657,10 +24674,10 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -untildify@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" - integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== +untildify@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== unzip-response@^2.0.1: version "2.0.1" @@ -25781,7 +25798,7 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.10.0: +yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= From 92a16d2e105b874c2f6a0384fe5538dc8a9a601d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2020 15:05:31 +0200 Subject: [PATCH 17/99] Transforms: Adds beta notice and updates transform descriptions (#24158) * Transforms: Adds beta notice and updates transform descriptions * Rename organize fields * Webpack - enable images import * Introduce FeatureState type * Alow Container component grow/shrink config * Enable svg import in main app * Jest + webpack for svgs * InfoBox refactor (+ added feature info box), Badge component introduced * Update packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx Co-authored-by: Carl Bergquist * Minor fixes * Update packages/grafana-ui/src/components/TransformersUI/OrganizeFieldsTransformerEditor.tsx Co-authored-by: Carl Bergquist * Update packages/grafana-ui/src/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx Co-authored-by: Carl Bergquist * fix typo * Build storybook fixed * Fix padding Co-authored-by: Dominik Prokop Co-authored-by: Carl Bergquist --- jest.config.js | 3 + .../transformations/transformers/reduce.ts | 2 +- packages/grafana-data/src/types/app.ts | 9 ++ packages/grafana-data/src/utils/docs.ts | 9 ++ packages/grafana-data/src/utils/index.ts | 1 + packages/grafana-runtime/tsconfig.json | 7 +- packages/grafana-ui/.storybook/tsconfig.json | 7 +- .../src/components/Badge/Badge.story.tsx | 35 ++++++ .../grafana-ui/src/components/Badge/Badge.tsx | 90 +++++++++++++ .../src/components/InfoBox/FeatureInfoBox.tsx | 63 ++++++++++ .../src/components/InfoBox/InfoBox.story.tsx | 40 ++++-- .../src/components/InfoBox/InfoBox.tsx | 92 +++++++++----- .../src/components/InfoBox/panelArt_dark.svg | 58 +++++++++ .../src/components/InfoBox/panelArt_light.svg | 64 ++++++++++ .../src/components/Layout/Layout.tsx | 24 +++- .../FilterByNameTransformerEditor.tsx | 2 +- .../FilterByRefIdTransformerEditor.tsx | 5 +- .../LabelsToFieldsTransformerEditor.tsx | 3 +- .../OrganizeFieldsTransformerEditor.tsx | 5 +- .../SeriesToFieldsTransformerEditor.tsx | 5 +- packages/grafana-ui/src/components/index.ts | 2 + packages/grafana-ui/tsconfig.json | 2 +- public/app/core/utils/docsLinks.ts | 10 ++ .../PanelEditor/FieldConfigEditor.tsx | 19 ++- .../TransformationsEditor.tsx | 43 +++++-- .../panel_editor/VizTypePickerPlugin.tsx | 36 +++++- .../features/plugins/PluginSignatureBadge.tsx | 119 +----------------- public/app/types/svg.d.ts | 4 + public/test/mocks/svg.ts | 1 + scripts/webpack/webpack.common.js | 5 + scripts/webpack/webpack.dev.js | 4 - 31 files changed, 585 insertions(+), 184 deletions(-) create mode 100644 packages/grafana-data/src/utils/docs.ts create mode 100644 packages/grafana-ui/src/components/Badge/Badge.story.tsx create mode 100644 packages/grafana-ui/src/components/Badge/Badge.tsx create mode 100644 packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx create mode 100644 packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg create mode 100644 packages/grafana-ui/src/components/InfoBox/panelArt_light.svg create mode 100644 public/app/core/utils/docsLinks.ts create mode 100644 public/app/types/svg.d.ts create mode 100644 public/test/mocks/svg.ts diff --git a/jest.config.js b/jest.config.js index 4b2c97b20ed..cda3d8a0986 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,4 +14,7 @@ module.exports = { setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'], snapshotSerializers: ['enzyme-to-json/serializer'], globals: { 'ts-jest': { isolatedModules: true } }, + moduleNameMapper: { + '\\.svg': '/public/test/mocks/svg.ts', + }, }; diff --git a/packages/grafana-data/src/transformations/transformers/reduce.ts b/packages/grafana-data/src/transformations/transformers/reduce.ts index 043e3dc17ea..d4c33420813 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.ts @@ -18,7 +18,7 @@ export interface ReduceTransformerOptions { export const reduceTransformer: DataTransformerInfo = { id: DataTransformerID.reduce, name: 'Reduce', - description: 'Reduce all rows to a single row and concatenate all results', + description: 'Reduce all rows or data points to a single value using a function like max, min, mean or last', defaultOptions: { reducers: [ReducerID.max], }, diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index e0146803928..a400b53dbbe 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -74,3 +74,12 @@ export class AppPlugin extends GrafanaPlugin> { } } } + +/** + * Defines life cycle of a feature + * @internal + */ +export enum FeatureState { + alpha = 'alpha', + beta = 'beta', +} diff --git a/packages/grafana-data/src/utils/docs.ts b/packages/grafana-data/src/utils/docs.ts new file mode 100644 index 00000000000..14e2247b4db --- /dev/null +++ b/packages/grafana-data/src/utils/docs.ts @@ -0,0 +1,9 @@ +/** + * Enumeration of documentation topics + * @internal + */ +export enum DocsId { + Transformations, + FieldConfig, + FieldConfigOverrides, +} diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 82f565eb882..0f3e90bf2b7 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -16,3 +16,4 @@ export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; export { locationUtil } from './location'; export { urlUtil, UrlQueryMap, UrlQueryValue } from './url'; export { DataLinkBuiltInVars } from './dataLinks'; +export { DocsId } from './docs'; diff --git a/packages/grafana-runtime/tsconfig.json b/packages/grafana-runtime/tsconfig.json index 80ce0289af2..f8ab70ac70d 100644 --- a/packages/grafana-runtime/tsconfig.json +++ b/packages/grafana-runtime/tsconfig.json @@ -11,5 +11,10 @@ }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"] + "include": [ + "src/**/*.ts*", + "../../public/app/types/jquery/*.ts", + "../../public/app/types/sanitize-url.d.ts", + "../../public/app/types/svg.d.ts" + ] } diff --git a/packages/grafana-ui/.storybook/tsconfig.json b/packages/grafana-ui/.storybook/tsconfig.json index 807d02f32b3..7a8f86da20c 100644 --- a/packages/grafana-ui/.storybook/tsconfig.json +++ b/packages/grafana-ui/.storybook/tsconfig.json @@ -6,5 +6,10 @@ }, "exclude": ["../dist", "../node_modules"], "extends": "../tsconfig.json", - "include": ["../src/**/*.ts", "../src/**/*.tsx", "../../../public/app/types/sanitize-url.d.ts"] + "include": [ + "../src/**/*.ts", + "../src/**/*.tsx", + "../../../public/app/types/sanitize-url.d.ts", + "../../../public/app/types/svg.d.ts" + ] } diff --git a/packages/grafana-ui/src/components/Badge/Badge.story.tsx b/packages/grafana-ui/src/components/Badge/Badge.story.tsx new file mode 100644 index 00000000000..e76f72e5cb1 --- /dev/null +++ b/packages/grafana-ui/src/components/Badge/Badge.story.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { boolean, text, select } from '@storybook/addon-knobs'; +import { Badge, BadgeColor } from './Badge'; + +export default { + title: 'Other/Badge', + component: Badge, + decorators: [], + parameters: { + docs: {}, + }, +}; + +export const basic = () => { + const badgeColor = select( + 'Badge color', + { + Red: 'red', + Green: 'green', + Blue: 'blue', + Orange: 'orange', + }, + 'blue' + ); + const withIcon = boolean('With icon', true); + const tooltipText = text('Tooltip text', ''); + return ( + + ); +}; diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx new file mode 100644 index 00000000000..efbaab0dbfb --- /dev/null +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Icon } from '../Icon/Icon'; +import { useTheme } from '../../themes/ThemeContext'; +import { stylesFactory } from '../../themes/stylesFactory'; +import { IconName } from '../../types'; +import { Tooltip } from '../Tooltip/Tooltip'; +import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data'; +import tinycolor from 'tinycolor2'; +import { css } from 'emotion'; +import { HorizontalGroup } from '..'; + +export type BadgeColor = 'blue' | 'red' | 'green' | 'orange'; + +export interface BadgeProps { + text: string; + color: BadgeColor; + icon?: IconName; + tooltip?: string; +} + +export const Badge = React.memo(({ icon, color, text, tooltip }) => { + const theme = useTheme(); + const styles = getStyles(theme, color); + const badge = ( +
+ + {icon && } + {text} + +
+ ); + + return tooltip ? ( + + {badge} + + ) : ( + badge + ); +}); + +Badge.displayName = 'Badge'; + +const getStyles = stylesFactory((theme: GrafanaTheme, color: BadgeColor) => { + let sourceColor = getColorFromHexRgbOrName(color); + let borderColor = ''; + let bgColor = ''; + let textColor = ''; + + if (theme.isDark) { + bgColor = tinycolor(sourceColor) + .darken(38) + .toString(); + borderColor = tinycolor(sourceColor) + .darken(25) + .toString(); + textColor = tinycolor(sourceColor) + .lighten(45) + .toString(); + } else { + bgColor = tinycolor(sourceColor) + .lighten(30) + .toString(); + borderColor = tinycolor(sourceColor) + .lighten(15) + .toString(); + textColor = tinycolor(sourceColor) + .darken(40) + .toString(); + } + + return { + wrapper: css` + font-size: ${theme.typography.size.sm}; + display: inline-flex; + padding: 1px 4px; + border-radius: 3px; + margin-top: 6px; + background: ${bgColor}; + border: 1px solid ${borderColor}; + color: ${textColor}; + + > span { + position: relative; + top: 1px; + margin-left: 2px; + } + `, + }; +}); diff --git a/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx new file mode 100644 index 00000000000..43845d3e536 --- /dev/null +++ b/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { InfoBox, InfoBoxProps } from './InfoBox'; +import { FeatureState, GrafanaTheme } from '@grafana/data'; +import { stylesFactory, useTheme } from '../../themes'; +import { Badge, BadgeProps } from '../Badge/Badge'; +import { css } from 'emotion'; + +interface FeatureInfoBox extends Omit { + title: string; + featureState?: FeatureState; +} +export const FeatureInfoBox = React.memo( + React.forwardRef(({ title, featureState, ...otherProps }, ref) => { + const theme = useTheme(); + const styles = getFeatureInfoBoxStyles(theme); + + const titleEl = featureState ? ( + <> +
+ +
+

{title}

+ + ) : ( +

{title}

+ ); + return ; + }) +); + +const getFeatureInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => { + return { + badge: css` + margin-bottom: ${theme.spacing.sm}; + `, + }; +}); + +interface FeatureBadgeProps { + featureState: FeatureState; +} + +export const FeatureBadge: React.FC = ({ featureState }) => { + const display = getPanelStateBadgeDisplayModel(featureState); + return ; +}; + +function getPanelStateBadgeDisplayModel(featureState: FeatureState): BadgeProps { + switch (featureState) { + case FeatureState.alpha: + return { + text: 'Alpha', + icon: 'exclamation-triangle', + color: 'orange', + }; + } + + return { + text: 'Beta', + icon: 'rocket', + color: 'blue', + }; +} diff --git a/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx b/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx index 8579b5992ae..faf649f9cda 100644 --- a/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx +++ b/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { number } from '@storybook/addon-knobs'; import { InfoBox } from './InfoBox'; +import { FeatureInfoBox } from './FeatureInfoBox'; +import { FeatureState } from '@grafana/data'; export default { title: 'Layout/InfoBox', @@ -35,16 +37,11 @@ export const basic = () => { return (
- Checkout the{' '} - - MySQL Data Source Docs - {' '} - for more information., - - } + title="User Permission" + url={'http://docs.grafana.org/features/datasources/mysql/'} + onDismiss={() => { + alert('onDismiss clicked'); + }} >

The database user should only be granted SELECT permissions on the specified database & tables you want to @@ -57,3 +54,26 @@ export const basic = () => {

); }; + +export const featureInfoBox = () => { + const { containerWidth } = getKnobs(); + + return ( +
+ { + alert('onDismiss clicked'); + }} + > + Transformations allow you to join, calculate, re-order, hide and rename your query results before being + visualized.
+ Many transforms are not suitable if your using the Graph visualisation as it currently only supports time + series.
+ It can help to switch to Table visualisation to understand what a transformation is doing. +
+
+ ); +}; diff --git a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx index 1a8ebae2499..bda80126727 100644 --- a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx +++ b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx @@ -2,10 +2,19 @@ import React from 'react'; import { css, cx } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; import { stylesFactory, useTheme } from '../../themes'; +import { Icon } from '../Icon/Icon'; +import { IconButton } from '../IconButton/IconButton'; +import { HorizontalGroup } from '../Layout/Layout'; +import panelArtDark from './panelArt_dark.svg'; +import panelArtLight from './panelArt_light.svg'; -export interface Props extends React.HTMLAttributes { - header?: string | JSX.Element; - footer?: string | JSX.Element; +export interface InfoBoxProps extends Omit, 'title'> { + children: React.ReactNode; + title?: string | JSX.Element; + url?: string; + urlTitle?: string; + branded?: boolean; + onDismiss?: () => void; } /** @@ -14,33 +23,39 @@ export interface Props extends React.HTMLAttributes { * @Alpha */ export const InfoBox = React.memo( - React.forwardRef(({ header, footer, className, children, ...otherProps }, ref) => { - const theme = useTheme(); - const css = getInfoBoxStyles(theme); + React.forwardRef( + ({ title, className, children, branded, url, urlTitle, onDismiss, ...otherProps }, ref) => { + const theme = useTheme(); + const styles = getInfoBoxStyles(theme); + const wrapperClassName = branded ? cx(styles.wrapperBranded, className) : cx(styles.wrapper, className); - return ( -
- {header && ( -
-
{header}
+ return ( +
+
+ +
{typeof title === 'string' ?

{title}

: title}
+ {onDismiss && } +
- )} - {children} - {footer &&
{footer}
} -
- ); - }) +
{children}
+ {url && ( + + {urlTitle || 'Read more'} + + )} +
+ ); + } + ) ); const getInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => ({ wrapper: css` position: relative; - padding: ${theme.spacing.lg}; + padding: ${theme.spacing.md}; background-color: ${theme.colors.bg2}; border-top: 3px solid ${theme.palette.blue80}; margin-bottom: ${theme.spacing.md}; - margin-right: ${theme.spacing.xs}; - box-shadow: ${theme.shadows.listItem}; flex-grow: 1; ul { @@ -60,18 +75,39 @@ const getInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => ({ margin-bottom: 0; } - a { - @extend .external-link; - } - &--max-lg { max-width: ${theme.breakpoints.lg}; } `, - header: css` - margin-bottom: ${theme.spacing.d}; + wrapperBranded: css` + padding: ${theme.spacing.md}; + border-radius: ${theme.border.radius.md}; + position: relative; + box-shadow: 0 0 30px 10px rgba(0, 0, 0, ${theme.isLight ? 0.05 : 0.2}); + z-index: 0; + + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url(${theme.isLight ? panelArtLight : panelArtDark}); + border-radius: ${theme.border.radius.md}; + background-position: 50% 50%; + background-size: cover; + filter: saturate(80%); + z-index: -1; + } + + p:last-child { + margin-bottom: 0; + } `, - footer: css` - margin-top: ${theme.spacing.d}; + docsLink: css` + display: inline-block; + margin-top: ${theme.spacing.lg}; + font-size: ${theme.typography.size.sm}; `, })); diff --git a/packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg b/packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg new file mode 100644 index 00000000000..82a26c49b84 --- /dev/null +++ b/packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/grafana-ui/src/components/InfoBox/panelArt_light.svg b/packages/grafana-ui/src/components/InfoBox/panelArt_light.svg new file mode 100644 index 00000000000..69a1a4611c1 --- /dev/null +++ b/packages/grafana-ui/src/components/InfoBox/panelArt_light.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/grafana-ui/src/components/Layout/Layout.tsx b/packages/grafana-ui/src/components/Layout/Layout.tsx index 7f2b2ba97e4..3cc6c8e12b3 100644 --- a/packages/grafana-ui/src/components/Layout/Layout.tsx +++ b/packages/grafana-ui/src/components/Layout/Layout.tsx @@ -1,5 +1,5 @@ import React, { HTMLProps } from 'react'; -import { css } from 'emotion'; +import { css, cx } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; import { stylesFactory, useTheme } from '../../themes'; @@ -24,6 +24,8 @@ export interface LayoutProps extends Omit, 'align' | ' export interface ContainerProps { padding?: Spacing; margin?: Spacing; + grow?: number; + shrink?: number; } export const Layout: React.FC = ({ @@ -84,10 +86,26 @@ export const VerticalGroup: React.FC> ); -export const Container: React.FC = ({ children, padding, margin }) => { +export const Container: React.FC = ({ children, padding, margin, grow, shrink }) => { const theme = useTheme(); const styles = getContainerStyles(theme, padding, margin); - return
{children}
; + return ( +
+ {children} +
+ ); }; const getStyles = stylesFactory( diff --git a/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx b/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx index 8f56a193604..e4c174221b4 100644 --- a/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx +++ b/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx @@ -190,5 +190,5 @@ export const filterFieldsByNameTransformRegistryItem: TransformerRegistyItem = { + [DocsId.Transformations]: 'https://docs.grafana.com', + [DocsId.FieldConfig]: 'https://docs.grafana.com', + [DocsId.FieldConfigOverrides]: 'https://docs.grafana.com', +}; + +export const getDocsLink = (id: DocsId) => DOCS_LINKS[id]; diff --git a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx index a1aa7cdee5c..c7db5ab7cb4 100644 --- a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx @@ -2,18 +2,20 @@ import React, { useCallback } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { DataFrame, + FeatureState, FieldConfigPropertyItem, FieldConfigSource, PanelPlugin, SelectableValue, VariableSuggestionsScope, } from '@grafana/data'; -import { Container, Counter, Field, fieldMatchersUI, Label, ValuePicker } from '@grafana/ui'; +import { Container, Counter, FeatureInfoBox, Field, fieldMatchersUI, Label, useTheme, ValuePicker } from '@grafana/ui'; import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv'; import { OverrideEditor } from './OverrideEditor'; import groupBy from 'lodash/groupBy'; import { OptionsGroup } from './OptionsGroup'; import { selectors } from '@grafana/e2e-selectors'; +import { css } from 'emotion'; interface Props { plugin: PanelPlugin; @@ -27,6 +29,8 @@ interface Props { * Expects the container div to have size set and will fill it 100% */ export const OverrideFieldConfigEditor: React.FC = props => { + const theme = useTheme(); + const { config } = props; const onOverrideChange = (index: number, override: any) => { const { config } = props; let overrides = cloneDeep(config.overrides); @@ -104,6 +108,19 @@ export const OverrideFieldConfigEditor: React.FC = props => { return (
+ {config.overrides.length === 0 && ( + + Field options overrides give you a fine grained control over how your data is displayed. + + )} + {renderOverrides()} {renderAddOverride()}
diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index f4ed8135f7e..f994426f5b1 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -1,8 +1,18 @@ import React from 'react'; -import { Button, Container, CustomScrollbar, stylesFactory, useTheme, ValuePicker, VerticalGroup } from '@grafana/ui'; +import { + Button, + Container, + CustomScrollbar, + FeatureInfoBox, + stylesFactory, + useTheme, + ValuePicker, + VerticalGroup, +} from '@grafana/ui'; import { DataFrame, DataTransformerConfig, + FeatureState, GrafanaTheme, SelectableValue, standardTransformersRegistry, @@ -114,13 +124,23 @@ export class TransformationsEditor extends React.PureComponent { renderNoAddedTransformsState() { return ( - <> -

- Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being - visualized.
- Choose one of the transformations below to start with: -

- + + + +

+ Transformations allow you to join, calculate, re-order, hide and rename your query results before being + visualized.
+ Many transforms are not suitable if your using the Graph visualisation as it currently only supports time + series.
+ It can help to switch to Table visualisation to understand what a transformation is doing.
+

+

Select one of the transformations below to start.

+
+
{standardTransformersRegistry.list().map(t => { return ( @@ -136,7 +156,7 @@ export class TransformationsEditor extends React.PureComponent { ); })} - +
); } @@ -170,6 +190,11 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => { border: none; padding: ${theme.spacing.sm}; + // hack because these cards use classes from a very different card for some reason + .add-data-source-item-text { + font-size: ${theme.typography.size.md}; + } + &:hover { background: ${theme.colors.bg3}; box-shadow: none; diff --git a/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx b/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx index f3d26eec83e..646c692ded1 100644 --- a/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx +++ b/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { GrafanaTheme, PanelPluginMeta, PluginState } from '@grafana/data'; -import { styleMixins, stylesFactory, useTheme } from '@grafana/ui'; +import { Badge, BadgeProps, styleMixins, stylesFactory, useTheme } from '@grafana/ui'; import { css, cx } from 'emotion'; import { selectors } from '@grafana/e2e-selectors'; -import { PanelPluginBadge } from '../../plugins/PluginSignatureBadge'; interface Props { isCurrent: boolean; @@ -126,3 +125,36 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { }); export default VizTypePickerPlugin; + +interface PanelPluginBadgeProps { + plugin: PanelPluginMeta; +} +const PanelPluginBadge: React.FC = ({ plugin }) => { + const display = getPanelStateBadgeDisplayModel(plugin); + + if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) { + return null; + } + return ; +}; + +function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): BadgeProps { + switch (panel.state) { + case PluginState.deprecated: + return { + text: 'Deprecated', + icon: 'exclamation-triangle', + color: 'red', + tooltip: `${panel.name} panel is deprecated`, + }; + } + + return { + text: 'Alpha', + icon: 'rocket', + color: 'blue', + tooltip: `${panel.name} panel is experimental`, + }; +} + +PanelPluginBadge.displayName = 'PanelPluginBadge'; diff --git a/public/app/features/plugins/PluginSignatureBadge.tsx b/public/app/features/plugins/PluginSignatureBadge.tsx index deb34df3cb7..308462d7dbe 100644 --- a/public/app/features/plugins/PluginSignatureBadge.tsx +++ b/public/app/features/plugins/PluginSignatureBadge.tsx @@ -1,61 +1,17 @@ import React from 'react'; -import { Icon, IconName, stylesFactory, Tooltip, useTheme } from '@grafana/ui'; -import { - getColorFromHexRgbOrName, - GrafanaTheme, - PanelPluginMeta, - PluginSignatureStatus, - PluginState, -} from '@grafana/data'; -import { css } from 'emotion'; -import tinycolor from 'tinycolor2'; +import { Badge, BadgeProps } from '@grafana/ui'; +import { PluginSignatureStatus } from '@grafana/data'; interface Props { status: PluginSignatureStatus; } export const PluginSignatureBadge: React.FC = ({ status }) => { - const theme = useTheme(); const display = getSignatureDisplayModel(status); - const styles = getStyles(theme, display); - - return ( - -
- - {display.text} -
-
- ); + return ; }; -interface PanelPluginBadgeProps { - plugin: PanelPluginMeta; -} -export const PanelPluginBadge: React.FC = ({ plugin }) => { - const theme = useTheme(); - const display = getPanelStateBadgeDisplayModel(plugin); - const styles = getStyles(theme, display); - - if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) { - return null; - } - return ( -
- - {display.text} -
- ); -}; - -interface DisplayModel { - text: string; - icon: IconName; - color: string; - tooltip: string; -} - -function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayModel { +function getSignatureDisplayModel(signature: PluginSignatureStatus): BadgeProps { switch (signature) { case PluginSignatureStatus.internal: return { text: 'Core', icon: 'cube', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' }; @@ -80,71 +36,4 @@ function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayMode return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' }; } -function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): DisplayModel { - switch (panel.state) { - case PluginState.deprecated: - return { - text: 'Deprecated', - icon: 'exclamation-triangle', - color: 'red', - tooltip: `${panel.name} panel is deprecated`, - }; - } - - return { - text: 'Alpha', - icon: 'rocket', - color: 'blue', - tooltip: `${panel.name} panel is experimental`, - }; -} - -const getStyles = stylesFactory((theme: GrafanaTheme, model: DisplayModel) => { - let sourceColor = getColorFromHexRgbOrName(model.color); - let borderColor = ''; - let bgColor = ''; - let textColor = ''; - - if (theme.isDark) { - bgColor = tinycolor(sourceColor) - .darken(38) - .toString(); - borderColor = tinycolor(sourceColor) - .darken(25) - .toString(); - textColor = tinycolor(sourceColor) - .lighten(45) - .toString(); - } else { - bgColor = tinycolor(sourceColor) - .lighten(30) - .toString(); - borderColor = tinycolor(sourceColor) - .lighten(15) - .toString(); - textColor = tinycolor(sourceColor) - .darken(40) - .toString(); - } - - return { - wrapper: css` - font-size: ${theme.typography.size.sm}; - display: inline-flex; - padding: 1px 4px; - border-radius: 3px; - margin-top: 6px; - background: ${bgColor}; - border: 1px solid ${borderColor}; - color: ${textColor}; - - > span { - position: relative; - top: 1px; - margin-left: 2px; - } - `, - }; -}); - PluginSignatureBadge.displayName = 'PluginSignatureBadge'; diff --git a/public/app/types/svg.d.ts b/public/app/types/svg.d.ts new file mode 100644 index 00000000000..cdb2b1a9a23 --- /dev/null +++ b/public/app/types/svg.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: string; + export default content; +} diff --git a/public/test/mocks/svg.ts b/public/test/mocks/svg.ts new file mode 100644 index 00000000000..6fdb2b40b35 --- /dev/null +++ b/public/test/mocks/svg.ts @@ -0,0 +1 @@ +export const svg = 'svg'; diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index f7cc9d81e62..72d7afbb85b 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -108,6 +108,11 @@ module.exports = { }, ], }, + { + test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/, + loader: 'file-loader', + options: { name: 'static/img/[name].[hash:8].[ext]' }, + }, ], }, // https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-3 diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index e1dd4c4fe23..e32e5ffb91e 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -86,10 +86,6 @@ module.exports = (env = {}) => sourceMap: false, preserveUrl: false, }), - { - test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, - loader: 'file-loader', - }, ], }, From 3c433b218ecc5254f4a4addc3c4828d90b8dd1b2 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Mon, 4 May 2020 17:06:53 +0200 Subject: [PATCH 18/99] DashboardManager: Disable editing if there are no folder permissions (#24237) * Disable editing if there are no folder permissions * Remove log --- public/app/features/search/components/ManageDashboards.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/search/components/ManageDashboards.tsx b/public/app/features/search/components/ManageDashboards.tsx index cccce6c1511..f933d62a4e7 100644 --- a/public/app/features/search/components/ManageDashboards.tsx +++ b/public/app/features/search/components/ManageDashboards.tsx @@ -100,8 +100,8 @@ export const ManageDashboards: FC = memo(({ folderId, folderUid }) => {
= memo(({ folderId, folderUid }) => { Date: Mon, 4 May 2020 17:14:23 +0200 Subject: [PATCH 19/99] Docs: add Usage Insights documentation (#23982) * Docs: start on usage insights * Docs: first draft for Usage Insights content * Docs: clean up usage insights docs * Docs: revert prettier updates * Update docs/sources/enterprise/usage-insights.md Co-Authored-By: Emil Tullstedt * Docs: rewrite presence indicators paragraph * Docs: feedback update * Update docs/sources/enterprise/_index.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Docs: headings to sentence case * Update docs/sources/enterprise/usage-insights.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/enterprise/usage-insights.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/enterprise/usage-insights.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/enterprise/usage-insights.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Docs: add dashboard insights button image * Docs: add consistency * Docs: add lower case * Update docs/sources/enterprise/usage-insights.md Co-authored-by: Alex Khomenko * Update docs/sources/enterprise/usage-insights.md Co-authored-by: Alex Khomenko * Update docs/sources/enterprise/usage-insights.md Co-authored-by: Alex Khomenko * Docs: singular presence indicator and improved search feedback * Docs: Apply PR feedback Co-authored-by: Emil Tullstedt Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Alex Khomenko --- docs/sources/enterprise/_index.md | 6 ++- docs/sources/enterprise/usage-insights.md | 57 +++++++++++++++++++++++ docs/sources/menu.yaml | 6 ++- 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 docs/sources/enterprise/usage-insights.md diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index 94a0b2dd09b..f0a43374601 100755 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -1,7 +1,7 @@ +++ title = "Grafana Enterprise" description = "Grafana Enterprise overview" -keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"] +keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise", "insights", "reporting"] type = "docs" [menu.docs] name = "Grafana Enterprise" @@ -58,6 +58,10 @@ Supported auth providers: [White labeling]({{< relref "white-labeling.md" >}}) allows you to replace the Grafana brand and logo with your own corporate brand and logo. You can also change footer links to point to your custom resources. +## Usage insights + +[Usage insights]({{< relref "usage-insights.md" >}}) allow you to understand how your Grafana instance is used. You can see who is looking at a dashboard, how often a dashboard is seen, and which dashboards are prone to errors. You'll also be able to discover what are the least and the most used dashboards. + ## Enterprise plugins With a Grafana Enterprise license, you get access to premium plugins, including: diff --git a/docs/sources/enterprise/usage-insights.md b/docs/sources/enterprise/usage-insights.md new file mode 100644 index 00000000000..4d61b8cc5da --- /dev/null +++ b/docs/sources/enterprise/usage-insights.md @@ -0,0 +1,57 @@ ++++ +title = "Usage-insights" +description = "Usage-insights" +keywords = ["grafana", "usage-insights", "enterprise"] +aliases = ["/docs/grafana/latest/enterprise/usage-insights/"] +type = "docs" +[menu.docs] +name = "Usage-insights" +parent = "enterprise" +weight = 700 ++++ + +# Usage insights + +Usage insights allows you to have a better understanding of how your Grafana instance is used. The collected data are the number of: + +- Dashboard views (aggregated and per user) +- Data source errors +- Data source queries + +> Only available in Grafana Enterprise v7.0+. + +## Presence indicator + +The presence indicator is visible to all signed-in users on all dashboards. It shows the avatars of users who interacted with the dashboard recently (last 10 minutes by default). You can see the user's name by hovering your cursor over the user's avatar. The avatars come from [Gravatar](https://gravatar.com) based on the user's email. + +When more users are active on a dashboard than can fit in the presence indicator section, click on the `+X` icon that opens [dashboard insights]({{< relref "#dashboard-insights" >}}) to see more details about recent user activity. + +{{< docs-imagebox img="/img/docs/enterprise/presence_indicators.png" max-width="400px" class="docs-image--no-shadow" >}} + +You can choose your own definition of "recent" by setting it in the [configuration]({{< relref "../installation/configuration.md">}}) file. + +```ini +[analytics.views] +# Set age for recent active users +recent_users_age = 10m +``` + +## Dashboard insights + +You can see dashboard usage information by clicking on the `Dashboard insights` button in the top bar. + +{{< docs-imagebox img="/img/docs/enterprise/dashboard_insights_button.png" max-width="400px" class="docs-image--no-shadow" >}} + +It shows two kinds of information: + +- **Stats:** Shows the daily query count and error count for the last 30 days. +- **Users & activity:** Shows the daily view count for the last 30 days; last activities on the dashboard and recent users (with a limit of 20). + +{{< docs-imagebox img="/img/docs/enterprise/dashboard_insights_stats.png" max-width="400px" class="docs-image--no-shadow" >}}{{< docs-imagebox img="/img/docs/enterprise/dashboard_insights_users.png" max-width="400px" class="docs-image--no-shadow" >}} + + +## Improved dashboard search + +In the search view, you can sort dashboards using these insights data. It helps you find unused or broken dashboards or discover most viewed ones. + +{{< docs-imagebox img="/img/docs/enterprise/improved_search.png" max-width="650px" class="docs-image--no-shadow" >}} diff --git a/docs/sources/menu.yaml b/docs/sources/menu.yaml index 4d0e54c66c5..20f631e3778 100644 --- a/docs/sources/menu.yaml +++ b/docs/sources/menu.yaml @@ -265,16 +265,18 @@ link: /enterprise/enhanced_ldap/ - name: Reporting link: /enterprise/reporting/ + - name: Export dashboard as PDF + link: /enterprise/export-pdf/ - name: SAML authentication link: /enterprise/saml/ - name: Team sync link: /enterprise/team-sync/ - name: White labeling link: /enterprise/white-labeling/ + - name: Usage insights + link: /enterprise/usage-insights/ - name: License expiration link: /enterprise/license-expiration/ - - name: Export dashboard as PDF - link: /enterprise/export-pdf/ - name: Plugins link: /plugins/ children: From 89db44e6f309474dd2dee03e3d65a9acbb9182fb Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 4 May 2020 17:36:57 +0200 Subject: [PATCH 20/99] DataLinks: Do not add empty links (#24088) * Do not add empty links * Review --- .../DataLinkEditorModalContent.tsx | 13 +++---- .../DataLinksInlineEditor.tsx | 38 +++++++++++++------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx index 48c2339e333..b6818c2716c 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx @@ -9,16 +9,16 @@ interface DataLinkEditorModalContentProps { index: number; data: DataFrame[]; suggestions: VariableSuggestion[]; - onChange: (index: number, ink: DataLink) => void; - onClose: () => void; + onSave: (index: number, ink: DataLink) => void; + onCancel: (index: number) => void; } export const DataLinkEditorModalContent: FC = ({ link, index, suggestions, - onChange, - onClose, + onSave, + onCancel, }) => { const [dirtyLink, setDirtyLink] = useState(link); return ( @@ -35,13 +35,12 @@ export const DataLinkEditorModalContent: FC = ( - diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index 9069b51430f..bad8b7a5d61 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -18,26 +18,40 @@ interface DataLinksInlineEditorProps { export const DataLinksInlineEditor: React.FC = ({ links, onChange, suggestions, data }) => { const theme = useTheme(); const [editIndex, setEditIndex] = useState(null); + const [isNew, setIsNew] = useState(false); + const styles = getDataLinksInlineEditorStyles(theme); const linksSafe: DataLink[] = links ?? []; - const isEditing = editIndex !== null && linksSafe[editIndex] !== undefined; + const isEditing = editIndex !== null; const onDataLinkChange = (index: number, link: DataLink) => { + if (isNew) { + if (link.title.trim() === '' && link.url.trim() === '') { + setIsNew(false); + setEditIndex(null); + return; + } else { + setEditIndex(null); + setIsNew(false); + } + } const update = cloneDeep(linksSafe); update[index] = link; onChange(update); + setEditIndex(null); }; const onDataLinkAdd = () => { let update = cloneDeep(linksSafe); + setEditIndex(update.length); + setIsNew(true); + }; - update.push({ - title: '', - url: '', - }); - - setEditIndex(update.length - 1); - onChange(update); + const onDataLinkCancel = (index: number) => { + if (isNew) { + setIsNew(false); + } + setEditIndex(null); }; const onDataLinkRemove = (index: number) => { @@ -72,15 +86,15 @@ export const DataLinksInlineEditor: React.FC = ({ li title="Edit link" isOpen={true} onDismiss={() => { - setEditIndex(null); + onDataLinkCancel(editIndex); }} > setEditIndex(null)} + onSave={onDataLinkChange} + onCancel={onDataLinkCancel} suggestions={suggestions} /> From 2fc2a7c3f5852a3005bf42fcc9921e9a55af5606 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 4 May 2020 17:39:20 +0200 Subject: [PATCH 21/99] Plugins: Only load transform plug-ins if expressions feature on (#24110) * PluginManager: Only load transform plugins if expressions feature on Co-authored-by: Marcus Efraimsson --- pkg/plugins/plugins.go | 14 ++++- pkg/plugins/plugins_test.go | 54 ++++++++++++++++--- .../behind-feature-flag/gel/plugin.json | 13 +++++ pkg/setting/setting.go | 5 ++ 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 pkg/plugins/testdata/behind-feature-flag/gel/plugin.json diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 923429cb38b..37da41d19d3 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -241,7 +241,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro } if f.Name() == "plugin.json" { - err := scanner.loadPluginJson(currentPath) + err := scanner.loadPlugin(currentPath) if err != nil { scanner.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath)) scanner.errors = append(scanner.errors, err) @@ -250,7 +250,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro return nil } -func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { +func (scanner *PluginScanner) loadPlugin(pluginJsonFilePath string) error { currentDir := filepath.Dir(pluginJsonFilePath) reader, err := os.Open(pluginJsonFilePath) if err != nil { @@ -269,6 +269,16 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { return errors.New("did not find type or id properties in plugin.json") } + // The expressions feature toggle corresponds to transform plug-ins. + if pluginCommon.Type == "transform" { + isEnabled := scanner.cfg.IsExpressionsEnabled() + if !isEnabled { + scanner.log.Debug("Transform plugin is disabled since the expressions feature toggle is not enabled", + "pluginID", pluginCommon.Id) + return nil + } + } + pluginCommon.PluginDir = filepath.Dir(pluginJsonFilePath) // For the time being, we choose to only require back-end plugins to be signed diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 8d33c628273..96eeaefe6e9 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -77,7 +77,7 @@ func TestPluginManager_Init(t *testing.T) { Cfg: &setting.Cfg{ PluginsAllowUnsigned: []string{"test"}, }, - BackendPluginManager: fakeBackendPluginManager{}, + BackendPluginManager: &fakeBackendPluginManager{}, } err := pm.Init() require.NoError(t, err) @@ -100,6 +100,46 @@ func TestPluginManager_Init(t *testing.T) { assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors) }) + + t.Run("Transform plugins should be ignored when expressions feature is off", func(t *testing.T) { + origPluginsPath := setting.PluginsPath + t.Cleanup(func() { + setting.PluginsPath = origPluginsPath + }) + setting.PluginsPath = "testdata/behind-feature-flag" + + fm := fakeBackendPluginManager{} + pm := &PluginManager{ + Cfg: &setting.Cfg{}, + BackendPluginManager: &fm, + } + err := pm.Init() + require.NoError(t, err) + + assert.Empty(t, pm.scanningErrors) + assert.Equal(t, 0, fm.registerCount) + }) + + t.Run("Transform plugins should be loaded when expressions feature is on", func(t *testing.T) { + origPluginsPath := setting.PluginsPath + t.Cleanup(func() { + setting.PluginsPath = origPluginsPath + }) + setting.PluginsPath = "testdata/behind-feature-flag" + + pm := &PluginManager{ + Cfg: &setting.Cfg{ + FeatureToggles: map[string]bool{ + "expressions": true, + }, + }, + BackendPluginManager: &fakeBackendPluginManager{}, + } + err := pm.Init() + require.NoError(t, err) + + assert.Equal(t, []error{fmt.Errorf(`plugin "gel" is unsigned`)}, pm.scanningErrors) + }) } func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) { @@ -123,23 +163,25 @@ func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) { } type fakeBackendPluginManager struct { + registerCount int } -func (f fakeBackendPluginManager) Register(descriptor backendplugin.PluginDescriptor) error { +func (f *fakeBackendPluginManager) Register(descriptor backendplugin.PluginDescriptor) error { + f.registerCount++ return nil } -func (f fakeBackendPluginManager) StartPlugin(ctx context.Context, pluginID string) error { +func (f *fakeBackendPluginManager) StartPlugin(ctx context.Context, pluginID string) error { return nil } -func (f fakeBackendPluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backendplugin.CollectMetricsResult, error) { +func (f *fakeBackendPluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backendplugin.CollectMetricsResult, error) { return nil, nil } -func (f fakeBackendPluginManager) CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backendplugin.CheckHealthResult, error) { +func (f *fakeBackendPluginManager) CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backendplugin.CheckHealthResult, error) { return nil, nil } -func (f fakeBackendPluginManager) CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string) { +func (f *fakeBackendPluginManager) CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string) { } diff --git a/pkg/plugins/testdata/behind-feature-flag/gel/plugin.json b/pkg/plugins/testdata/behind-feature-flag/gel/plugin.json new file mode 100644 index 00000000000..e6c92ae166d --- /dev/null +++ b/pkg/plugins/testdata/behind-feature-flag/gel/plugin.json @@ -0,0 +1,13 @@ +{ + "type": "transform", + "name": "GEL", + "id": "gel", + "backend": true, + "info": { + "description": "Test", + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + } + } +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index cc6d44c277f..c23f23b00bf 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -289,6 +289,11 @@ type Cfg struct { FeatureToggles map[string]bool } +// IsExpressionsEnabled returns whether the expressions feature is enabled. +func (c Cfg) IsExpressionsEnabled() bool { + return c.FeatureToggles["expressions"] +} + type CommandLineArgs struct { Config string HomePath string From 9420873e6c49b1a017913b0b84156b558eaa3531 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 4 May 2020 18:06:21 +0200 Subject: [PATCH 22/99] Loki: Show loki datasource stats in panel inspector (#24190) * Loki: Show loki datasource stats in panel inspector - puts the loki query result stats into the query results meta stat API of Grafana, this allows the display of all backend loki stats in the panel inspector in the dashboards - added a hack to also display one of those values in Explore as a meta label using the dataframe meta `custom` mechanims to point to a single stat entry for each series which is then added together to show total bytes processed across all query row results (this should be changed for 7.1 to make full use of the panel inspector in Explore) * Fix test * nicer stats labels for loki stats with units --- public/app/core/logs_model.ts | 21 +++++++++ .../loki/result_transformer.test.ts | 17 ++++++- .../datasource/loki/result_transformer.ts | 44 ++++++++++++++++++- public/app/plugins/datasource/loki/types.ts | 9 ++++ 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 1121bd7a72f..fc10b771cd9 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -32,6 +32,7 @@ import { getThemeColor } from 'app/core/utils/colors'; import { sortInAscendingOrder, deduplicateLogRowsById } from 'app/core/utils/explore'; import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel'; +import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters'; export const LogLevelColor = { [LogLevel.critical]: colors[7], @@ -375,6 +376,26 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi }); } + // Hack to print loki stats in Explore. Should be using proper stats display via drawer in Explore (rework in 7.1) + let totalBytes = 0; + for (const series of logSeries) { + const totalBytesKey = series.meta?.custom?.lokiQueryStatKey; + if (totalBytesKey && series.meta.stats) { + const byteStat = series.meta.stats.find(stat => stat.title === totalBytesKey); + if (byteStat) { + totalBytes += byteStat.value; + } + } + } + if (totalBytes > 0) { + const { text, suffix } = decimalSIPrefix('B')(totalBytes); + meta.push({ + label: 'Total bytes processed', + value: `${text} ${suffix}`, + kind: LogsMetaKind.String, + }); + } + return { hasUniqueLabels, meta, diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index ec0f1fa86f1..5070ebda594 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -1,5 +1,5 @@ import { CircularDataFrame, FieldCache, FieldType, MutableDataFrame } from '@grafana/data'; -import { LokiStreamResult, LokiTailResponse } from './types'; +import { LokiStreamResult, LokiTailResponse, LokiStreamResponse, LokiResultType } from './types'; import * as ResultTransformer from './result_transformer'; import { enhanceDataFrame } from './result_transformer'; @@ -18,6 +18,19 @@ const streamResult: LokiStreamResult[] = [ }, ]; +const lokiResponse: LokiStreamResponse = { + status: 'success', + data: { + result: streamResult, + resultType: LokiResultType.Stream, + stats: { + summary: { + bytesTotal: 900, + }, + }, + }, +}; + describe('loki result transformer', () => { afterAll(() => { jest.restoreAllMocks(); @@ -45,7 +58,7 @@ describe('loki result transformer', () => { describe('lokiStreamsToDataframes', () => { it('should enhance data frames', () => { jest.spyOn(ResultTransformer, 'enhanceDataFrame'); - const dataFrames = ResultTransformer.lokiStreamsToDataframes(streamResult, { refId: 'B' }, 500, { + const dataFrames = ResultTransformer.lokiStreamsToDataframes(lokiResponse, { refId: 'B' }, 500, { derivedFields: [ { matcherRegex: 'trace=(w+)', diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index fe45629725d..94a3328cfb5 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -14,6 +14,7 @@ import { DataFrameView, DataLink, Field, + QueryResultMetaStat, } from '@grafana/data'; import templateSrv from 'app/features/templating/template_srv'; @@ -31,6 +32,8 @@ import { LokiQuery, LokiOptions, DerivedFieldConfig, + LokiStreamResponse, + LokiStats, } from './types'; /** @@ -257,13 +260,48 @@ function getOriginalMetricName(labelData: { [key: string]: string }) { return `${metricName}{${labelPart}}`; } +export function decamelize(s: string): string { + return s.replace(/[A-Z]/g, m => ` ${m.toLowerCase()}`); +} + +// Turn loki stats { metric: value } into meta stat { title: metric, value: value } +function lokiStatsToMetaStat(stats: LokiStats): QueryResultMetaStat[] { + const result: QueryResultMetaStat[] = []; + if (!stats) { + return result; + } + for (const section in stats) { + const values = stats[section]; + for (const label in values) { + const value = values[label]; + let unit; + if (/time/i.test(label) && value) { + unit = 's'; + } else if (/bytes.*persecond/i.test(label)) { + unit = 'Bps'; + } else if (/bytes/i.test(label)) { + unit = 'decbytes'; + } + const title = `${_.capitalize(section)}: ${decamelize(label)}`; + result.push({ title, value, unit }); + } + } + return result; +} + export function lokiStreamsToDataframes( - data: LokiStreamResult[], + response: LokiStreamResponse, target: { refId: string; expr?: string; regexp?: string }, limit: number, config: LokiOptions, reverse = false ): DataFrame[] { + const data = limit > 0 ? response.data.result : []; + const stats: QueryResultMetaStat[] = lokiStatsToMetaStat(response.data.stats); + // Use custom mechanism to identify which stat we want to promote to label + const custom = { + lokiQueryStatKey: 'Summary: totalBytesProcessed', + }; const series: DataFrame[] = data.map(stream => { const dataFrame = lokiStreamResultToDataFrame(stream, reverse); enhanceDataFrame(dataFrame, config); @@ -273,6 +311,8 @@ export function lokiStreamsToDataframes( meta: { searchWords: getHighlighterExpressionsFromQuery(formatQuery(target.expr, target.regexp)), limit, + stats, + custom, }, }; }); @@ -378,7 +418,7 @@ export function processRangeQueryResponse( switch (response.data.resultType) { case LokiResultType.Stream: return of({ - data: lokiStreamsToDataframes(limit > 0 ? response.data.result : [], target, limit, config, reverse), + data: lokiStreamsToDataframes(response as LokiStreamResponse, target, limit, config, reverse), key: `${target.refId}_log`, }); diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index 9cebc847b28..306f333bc38 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -39,6 +39,12 @@ export interface LokiOptions extends DataSourceJsonData { derivedFields?: DerivedFieldConfig[]; } +export interface LokiStats { + [component: string]: { + [label: string]: number; + }; +} + export interface LokiVectorResult { metric: { [label: string]: string }; value: [number, string]; @@ -49,6 +55,7 @@ export interface LokiVectorResponse { data: { resultType: LokiResultType.Vector; result: LokiVectorResult[]; + stats?: LokiStats; }; } @@ -62,6 +69,7 @@ export interface LokiMatrixResponse { data: { resultType: LokiResultType.Matrix; result: LokiMatrixResult[]; + stats?: LokiStats; }; } @@ -75,6 +83,7 @@ export interface LokiStreamResponse { data: { resultType: LokiResultType.Stream; result: LokiStreamResult[]; + stats?: LokiStats; }; } From 4f5ce48b2ab4cda74b97eb90689824fc9e671e7d Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 4 May 2020 18:16:54 +0200 Subject: [PATCH 23/99] Loki: Fix label matcher for log metrics queries (#24238) * Fix switched filter buttons * Fix filter for and filter out for metrics queries * Add test coverage --- .../src/components/Logs/LogDetailsRow.tsx | 4 ++-- .../app/plugins/datasource/loki/datasource.ts | 17 +++++------------ .../prometheus/add_label_to_query.test.ts | 19 ++++++------------- .../prometheus/add_label_to_query.ts | 6 ------ 4 files changed, 13 insertions(+), 33 deletions(-) diff --git a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx index bcc5836019b..5fa8ee4d461 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx @@ -101,10 +101,10 @@ class UnThemedLogDetailsRow extends PureComponent { {isLabel && ( <> - + - + )} diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 07116cc68c9..b48d2666491 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -6,12 +6,12 @@ import { map, filter, catchError, switchMap } from 'rxjs/operators'; // Services & Utils import { DataFrame, dateMath, FieldCache } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; -import { addLabelToSelector, keepSelectorFilters } from 'app/plugins/datasource/prometheus/add_label_to_query'; +import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query'; import { DatasourceRequestOptions } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore'; import { lokiResultsToTableModel, processRangeQueryResponse, lokiStreamResultToDataFrame } from './result_transformer'; -import { formatQuery, parseQuery, getHighlighterExpressionsFromQuery } from './query_utils'; +import { parseQuery, getHighlighterExpressionsFromQuery } from './query_utils'; // Types import { @@ -344,27 +344,20 @@ export class LokiDatasource extends DataSourceApi { } modifyQuery(query: LokiQuery, action: any): LokiQuery { - const parsed = parseQuery(query.expr || ''); - let { query: selector } = parsed; - let selectorLabels, selectorFilters; + let expression = query.expr ?? ''; switch (action.type) { case 'ADD_FILTER': { - selectorLabels = addLabelToSelector(selector, action.key, action.value); - selectorFilters = keepSelectorFilters(selector); - selector = `${selectorLabels} ${selectorFilters}`.trim(); + expression = addLabelToQuery(expression, action.key, action.value); break; } case 'ADD_FILTER_OUT': { - selectorLabels = addLabelToSelector(selector, action.key, action.value, '!='); - selectorFilters = keepSelectorFilters(selector); - selector = `${selectorLabels} ${selectorFilters}`.trim(); + expression = addLabelToQuery(expression, action.key, action.value, '!='); break; } default: break; } - const expression = formatQuery(selector, parsed.regexp); return { ...query, expr: expression }; } diff --git a/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts b/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts index d577baf7bc8..3386bb5f563 100644 --- a/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts +++ b/public/app/plugins/datasource/prometheus/add_label_to_query.test.ts @@ -1,4 +1,4 @@ -import { addLabelToQuery, addLabelToSelector, keepSelectorFilters } from './add_label_to_query'; +import { addLabelToQuery, addLabelToSelector } from './add_label_to_query'; describe('addLabelToQuery()', () => { it('should add label to simple query', () => { @@ -58,6 +58,11 @@ describe('addLabelToQuery()', () => { 'avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})' ); }); + + it('should not remove filters', () => { + expect(addLabelToQuery('{x="y"} |="yy"', 'bar', 'baz')).toBe('{bar="baz",x="y"} |="yy"'); + expect(addLabelToQuery('{x="y"} |="yy" !~"xx"', 'bar', 'baz')).toBe('{bar="baz",x="y"} |="yy" !~"xx"'); + }); }); describe('addLabelToSelector()', () => { @@ -72,15 +77,3 @@ describe('addLabelToSelector()', () => { expect(addLabelToSelector('{}', 'baz', '42', '!=')).toBe('{baz!="42"}'); }); }); - -describe('keepSelectorFilters()', () => { - test('should return empty string if no filter is in selector', () => { - expect(keepSelectorFilters('{foo="bar"}')).toBe(''); - }); - test('should return a filter if filter is in selector', () => { - expect(keepSelectorFilters('{foo="bar"} |="baz"')).toBe('|="baz"'); - }); - test('should return multiple filters if multiple filters are in selector', () => { - expect(keepSelectorFilters('{foo!="bar"} |="baz" |~"yy" !~"xx"')).toBe('|="baz" |~"yy" !~"xx"'); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/add_label_to_query.ts b/public/app/plugins/datasource/prometheus/add_label_to_query.ts index 0b005268849..8b5cc758a76 100644 --- a/public/app/plugins/datasource/prometheus/add_label_to_query.ts +++ b/public/app/plugins/datasource/prometheus/add_label_to_query.ts @@ -91,12 +91,6 @@ export function addLabelToSelector(selector: string, labelKey: string, labelValu return `{${formatted}}`; } -export function keepSelectorFilters(selector: string) { - // Remove all label-key between {} and return filters. If first character is space, remove it. - const filters = selector.replace(/\{(.*?)\}/g, '').replace(/^ /, ''); - return filters; -} - function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) { const nextSelectorStart = text.slice(position).indexOf(openChar); const nextSelectorEnd = text.slice(position).indexOf(closeChar); From 9e06f9c40216650e5051069eb2ed4a15703928c4 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 4 May 2020 18:17:31 +0200 Subject: [PATCH 24/99] Logs: Add log level Fatal (#24185) - recognizes log levels "fatal" and "information" - renders "fatal" with same color as other levels similar to "critical" --- packages/grafana-data/src/types/logs.ts | 2 ++ packages/grafana-data/src/utils/logs.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 39ad0c71874..a3a47dd322e 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -8,6 +8,7 @@ import { DataFrame } from './dataFrame'; */ export enum LogLevel { emerg = 'critical', + fatal = 'critical', alert = 'critical', crit = 'critical', critical = 'critical', @@ -17,6 +18,7 @@ export enum LogLevel { eror = 'error', error = 'error', info = 'info', + information = 'info', notice = 'info', dbug = 'debug', debug = 'debug', diff --git a/packages/grafana-data/src/utils/logs.test.ts b/packages/grafana-data/src/utils/logs.test.ts index 3eb86f61de4..c51e479ae7b 100644 --- a/packages/grafana-data/src/utils/logs.test.ts +++ b/packages/grafana-data/src/utils/logs.test.ts @@ -15,7 +15,7 @@ describe('getLoglevel()', () => { }); it('returns no log level on when level is part of a word', () => { - expect(getLogLevel('this is information')).toBe(LogLevel.unknown); + expect(getLogLevel('who warns us')).toBe(LogLevel.unknown); }); it('returns same log level for long and short version', () => { From 83683d87f8e45dd5e63d2f761548c46bb2899bf1 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 4 May 2020 11:05:04 -0700 Subject: [PATCH 25/99] Errors: support errors with frame data from backend responses (#24176) --- .../src/dataframe/ArrowDataFrame.test.ts | 42 +---- .../src/dataframe/ArrowDataFrame.ts | 17 -- .../__snapshots__/ArrowDataFrame.test.ts.snap | 103 ----------- packages/grafana-runtime/package.json | 1 + packages/grafana-runtime/src/index.ts | 1 + .../src/utils/DataSourceWithBackend.ts | 17 +- .../src/utils/queryResponse.test.ts | 165 ++++++++++++++++++ .../src/utils/queryResponse.ts | 91 ++++++++++ .../features/dashboard/state/runRequest.ts | 27 +-- .../datasource/cloudwatch/datasource.ts | 9 +- .../plugins/datasource/testdata/datasource.ts | 5 +- 11 files changed, 276 insertions(+), 202 deletions(-) create mode 100644 packages/grafana-runtime/src/utils/queryResponse.test.ts create mode 100644 packages/grafana-runtime/src/utils/queryResponse.ts diff --git a/packages/grafana-data/src/dataframe/ArrowDataFrame.test.ts b/packages/grafana-data/src/dataframe/ArrowDataFrame.test.ts index cf3da3d4fc4..415b5b9c1ca 100644 --- a/packages/grafana-data/src/dataframe/ArrowDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/ArrowDataFrame.test.ts @@ -1,51 +1,11 @@ import fs from 'fs'; import path from 'path'; -import { resultsToDataFrames, grafanaDataFrameToArrowTable, arrowTableToDataFrame } from './ArrowDataFrame'; +import { grafanaDataFrameToArrowTable, arrowTableToDataFrame } from './ArrowDataFrame'; import { toDataFrameDTO, toDataFrame } from './processDataFrame'; import { FieldType } from '../types'; import { Table } from 'apache-arrow'; -/* eslint-disable */ -const resp = { - results: { - '': { - refId: '', - dataframes: [ - 'QVJST1cxAACsAQAAEAAAAAAACgAOAAwACwAEAAoAAAAUAAAAAAAAAQMACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAOD+//8IAAAADAAAAAIAAABHQwAABQAAAHJlZklkAAAAAP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAAlAAAAAQAAACG////FAAAAGAAAABgAAAAAAADAWAAAAACAAAALAAAAAQAAABQ////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAAB0////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAAAAABm////AAACAAAAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAbAAAAHQAAAAAAAoBdAAAAAIAAAA0AAAABAAAANz///8IAAAAEAAAAAQAAAB0aW1lAAAAAAQAAAB0eXBlAAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAC8AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAA0AAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAA0AAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAIAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAFp00e2XHFQAIo158ZccVAPqoiH1lxxUA7K6yfmXHFQDetNx/ZccVANC6BoFlxxUAwsAwgmXHFQC0xlqDZccVAKbMhIRlxxUAmNKuhWXHFQCK2NiGZccVAHzeAohlxxUAbuQsiWXHFQAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAMAAQAAALgBAAAAAAAAwAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAABQAAAAAgAAACgAAAAEAAAA4P7//wgAAAAMAAAAAgAAAEdDAAAFAAAAcmVmSWQAAAAA////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAIAAACUAAAABAAAAIb///8UAAAAYAAAAGAAAAAAAAMBYAAAAAIAAAAsAAAABAAAAFD///8IAAAAEAAAAAYAAABudW1iZXIAAAQAAAB0eXBlAAAAAHT///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAAAAAGb///8AAAIAAAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABsAAAAdAAAAAAACgF0AAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAHRpbWUAAAAABAAAAHR5cGUAAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAANgBAABBUlJPVzE=', - 'QVJST1cxAAC8AQAAEAAAAAAACgAOAAwACwAEAAoAAAAUAAAAAAAAAQMACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAND+//8IAAAADAAAAAIAAABHQgAABQAAAHJlZklkAAAA8P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAApAAAAAQAAAB2////FAAAAGgAAABoAAAAAAADAWgAAAACAAAALAAAAAQAAABA////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAABk////CAAAABQAAAAJAAAAR0Itc2VyaWVzAAAABAAAAG5hbWUAAAAAAAAAAF7///8AAAIACQAAAEdCLXNlcmllcwASABgAFAATABIADAAAAAgABAASAAAAFAAAAGwAAAB0AAAAAAAKAXQAAAACAAAANAAAAAQAAADc////CAAAABAAAAAEAAAAdGltZQAAAAAEAAAAdHlwZQAAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAvAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAANAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAANAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaAAAAAAAAABoAAAAAAAAAAAAAAAAAAAAaAAAAAAAAABoAAAAAAAAAAAAAAACAAAADQAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAAAAAAAAAAAAAAAABadNHtlxxUACKNefGXHFQD6qIh9ZccVAOyusn5lxxUA3rTcf2XHFQDQugaBZccVAMLAMIJlxxUAtMZag2XHFQCmzISEZccVAJjSroVlxxUAitjYhmXHFQB83gKIZccVAG7kLIllxxUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAADIAQAAAAAAAMAAAAAAAAAA0AAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAND+//8IAAAADAAAAAIAAABHQgAABQAAAHJlZklkAAAA8P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAApAAAAAQAAAB2////FAAAAGgAAABoAAAAAAADAWgAAAACAAAALAAAAAQAAABA////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAABk////CAAAABQAAAAJAAAAR0Itc2VyaWVzAAAABAAAAG5hbWUAAAAAAAAAAF7///8AAAIACQAAAEdCLXNlcmllcwASABgAFAATABIADAAAAAgABAASAAAAFAAAAGwAAAB0AAAAAAAKAXQAAAACAAAANAAAAAQAAADc////CAAAABAAAAAEAAAAdGltZQAAAAAEAAAAdHlwZQAAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA6AEAAEFSUk9XMQ==', - ], - series: [] as any[], - tables: null as any, - frames: null as any, - }, - }, -}; -/* eslint-enable */ - -describe('GEL Utils', () => { - test('should parse output with dataframe', () => { - const frames = resultsToDataFrames(resp); - for (const frame of frames) { - console.log('Frame', frame.refId); - for (const field of frame.fields) { - console.log(' > ', field.name, field.labels); - console.log(' (values)= ', field.values.toArray()); - } - } - - const norm = frames.map(f => toDataFrameDTO(f)); - expect(norm).toMatchSnapshot(); - }); - - test('processEmptyResults', () => { - const frames = resultsToDataFrames({ - results: { '': { refId: '', meta: null, series: null, tables: null, dataframes: null } }, - }); - expect(frames.length).toEqual(0); - }); -}); - describe('Read/Write arrow Table to DataFrame', () => { test('should parse output with dataframe', () => { const frame = toDataFrame({ diff --git a/packages/grafana-data/src/dataframe/ArrowDataFrame.ts b/packages/grafana-data/src/dataframe/ArrowDataFrame.ts index cce08dc5ada..20d5045a528 100644 --- a/packages/grafana-data/src/dataframe/ArrowDataFrame.ts +++ b/packages/grafana-data/src/dataframe/ArrowDataFrame.ts @@ -161,20 +161,3 @@ export function grafanaDataFrameToArrowTable(data: DataFrame): Table { } return table; } - -export function resultsToDataFrames(rsp: any): DataFrame[] { - if (rsp === undefined || rsp.results === undefined) { - return []; - } - - const results = rsp.results as Array<{ dataframes: string[] }>; - const frames: DataFrame[] = Object.values(results).flatMap(res => { - if (!res.dataframes) { - return []; - } - - return res.dataframes.map((b: string) => arrowTableToDataFrame(base64StringToArrowTable(b))); - }); - - return frames; -} diff --git a/packages/grafana-data/src/dataframe/__snapshots__/ArrowDataFrame.test.ts.snap b/packages/grafana-data/src/dataframe/__snapshots__/ArrowDataFrame.test.ts.snap index 358c55e3907..29d44ce7138 100644 --- a/packages/grafana-data/src/dataframe/__snapshots__/ArrowDataFrame.test.ts.snap +++ b/packages/grafana-data/src/dataframe/__snapshots__/ArrowDataFrame.test.ts.snap @@ -1,108 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GEL Utils should parse output with dataframe 1`] = ` -Array [ - Object { - "fields": Array [ - Object { - "config": Object {}, - "labels": undefined, - "name": "Time", - "type": "time", - "values": Array [ - 1569334575000, - 1569334580000, - 1569334585000, - 1569334590000, - 1569334595000, - 1569334600000, - 1569334605000, - 1569334610000, - 1569334615000, - 1569334620000, - 1569334625000, - 1569334630000, - 1569334635000, - ], - }, - Object { - "config": Object {}, - "labels": undefined, - "name": "", - "type": "number", - "values": Array [ - 3, - 3, - 3, - 5, - 5, - 5, - 3, - 3, - 3, - 5, - 5, - 5, - 3, - ], - }, - ], - "meta": undefined, - "name": undefined, - "refId": "GC", - }, - Object { - "fields": Array [ - Object { - "config": Object {}, - "labels": undefined, - "name": "Time", - "type": "time", - "values": Array [ - 1569334575000, - 1569334580000, - 1569334585000, - 1569334590000, - 1569334595000, - 1569334600000, - 1569334605000, - 1569334610000, - 1569334615000, - 1569334620000, - 1569334625000, - 1569334630000, - 1569334635000, - ], - }, - Object { - "config": Object {}, - "labels": undefined, - "name": "GB-series", - "type": "number", - "values": Array [ - 0, - 0, - 0, - 2, - 2, - 2, - 0, - 0, - 0, - 2, - 2, - 2, - 0, - ], - }, - ], - "meta": undefined, - "name": undefined, - "refId": "GB", - }, -] -`; - exports[`Read/Write arrow Table to DataFrame should read all types 1`] = ` Object { "fields": Array [ diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 28a6dea230f..f0419570de7 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -34,6 +34,7 @@ "@rollup/plugin-node-resolve": "7.1.1", "@types/rollup-plugin-visualizer": "2.6.0", "@types/systemjs": "^0.20.6", + "@types/jest": "23.3.14", "lodash": "4.17.15", "pretty-format": "25.1.0", "rollup": "2.0.6", diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 2b0463417aa..386dc95a9ae 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -9,3 +9,4 @@ export * from './types'; export { loadPluginCss, SystemJS, PluginCssOptions } from './utils/plugin'; export { reportMetaAnalytics } from './utils/analytics'; export { DataSourceWithBackend, HealthCheckResult, HealthStatus } from './utils/DataSourceWithBackend'; +export { toDataQueryError, toDataQueryResponse } from './utils/queryResponse'; diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 535cb20b265..25e45caddc3 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -9,6 +9,7 @@ import { import { Observable, from } from 'rxjs'; import { config } from '..'; import { getBackendSrv } from '../services'; +import { toDataQueryResponse } from './queryResponse'; const ExpressionDatasourceID = '__expr__'; @@ -94,7 +95,11 @@ export class DataSourceWithBackend< requestId, }) .then((rsp: any) => { - return this.toDataQueryResponse(rsp?.data); + return toDataQueryResponse(rsp); + }) + .catch(err => { + err.isHandled = true; // Avoid extra popup warning + return toDataQueryResponse(err); }); return from(req); @@ -109,16 +114,6 @@ export class DataSourceWithBackend< return query; } - /** - * This makes the arrow library loading async. - */ - async toDataQueryResponse(rsp: any): Promise { - const { resultsToDataFrames } = await import( - /* webpackChunkName: "apache-arrow-util" */ '@grafana/data/src/dataframe/ArrowDataFrame' - ); - return { data: resultsToDataFrames(rsp) }; - } - /** * Make a GET request to the datasource resource path */ diff --git a/packages/grafana-runtime/src/utils/queryResponse.test.ts b/packages/grafana-runtime/src/utils/queryResponse.test.ts new file mode 100644 index 00000000000..25169669a0a --- /dev/null +++ b/packages/grafana-runtime/src/utils/queryResponse.test.ts @@ -0,0 +1,165 @@ +import { toDataFrameDTO } from '@grafana/data'; + +import { toDataQueryResponse } from './queryResponse'; + +/* eslint-disable */ +const resp = { + data: { + results: { + GC: { + dataframes: [ + 'QVJST1cxAACsAQAAEAAAAAAACgAOAAwACwAEAAoAAAAUAAAAAAAAAQMACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAOD+//8IAAAADAAAAAIAAABHQwAABQAAAHJlZklkAAAAAP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAACAAAAlAAAAAQAAACG////FAAAAGAAAABgAAAAAAADAWAAAAACAAAALAAAAAQAAABQ////CAAAABAAAAAGAAAAbnVtYmVyAAAEAAAAdHlwZQAAAAB0////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAAAAABm////AAACAAAAAAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAbAAAAHQAAAAAAAoBdAAAAAIAAAA0AAAABAAAANz///8IAAAAEAAAAAQAAAB0aW1lAAAAAAQAAAB0eXBlAAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAC8AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAA0AAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAA0AAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAAAAABoAAAAAAAAAGgAAAAAAAAAAAAAAAIAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAFp00e2XHFQAIo158ZccVAPqoiH1lxxUA7K6yfmXHFQDetNx/ZccVANC6BoFlxxUAwsAwgmXHFQC0xlqDZccVAKbMhIRlxxUAmNKuhWXHFQCK2NiGZccVAHzeAohlxxUAbuQsiWXHFQAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAAAAAAAAACEAAAAAAAAAIQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAAAhAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAMAAQAAALgBAAAAAAAAwAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAABQAAAAAgAAACgAAAAEAAAA4P7//wgAAAAMAAAAAgAAAEdDAAAFAAAAcmVmSWQAAAAA////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAIAAACUAAAABAAAAIb///8UAAAAYAAAAGAAAAAAAAMBYAAAAAIAAAAsAAAABAAAAFD///8IAAAAEAAAAAYAAABudW1iZXIAAAQAAAB0eXBlAAAAAHT///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAAAAAGb///8AAAIAAAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABsAAAAdAAAAAAACgF0AAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAHRpbWUAAAAABAAAAHR5cGUAAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAANgBAABBUlJPVzE=', + ], + frames: null as any, + }, + }, + }, +}; + +const resWithError = { + data: { + results: { + A: { + error: 'Hello Error', + series: null, + tables: null, + dataframes: [ + 'QVJST1cxAAD/////WAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAJwAAAADAAAATAAAACgAAAAEAAAAPP///wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABc////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAHz///8IAAAANAAAACoAAAB7Im5vdGljZXMiOlt7InNldmVyaXR5IjoyLCJ0ZXh0IjoiVGV4dCJ9XX0AAAQAAABtZXRhAAAAAAEAAAAYAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAAA0wAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABwAAAG51bWJlcnMABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgAHAAAAbnVtYmVycwAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAAA4AAAAAgAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAA8D8AAAAAAAAIQBAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAABoAQAAAAAAAJAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAnAAAAAMAAABMAAAAKAAAAAQAAAA8////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAFz///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAfP///wgAAAA0AAAAKgAAAHsibm90aWNlcyI6W3sic2V2ZXJpdHkiOjIsInRleHQiOiJUZXh0In1dfQAABAAAAG1ldGEAAAAAAQAAABgAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAADTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAHAAAAbnVtYmVycwAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAACAAcAAABudW1iZXJzAIABAABBUlJPVzE=', + ], + }, + }, + }, +}; + +const emptyResults = { + data: { '': { refId: '', meta: null, series: null, tables: null, dataframes: null } }, +}; + +/* eslint-enable */ + +describe('GEL Utils', () => { + test('should parse output with dataframe', () => { + const res = toDataQueryResponse(resp); + const frames = res.data; + for (const frame of frames) { + expect(frame.refId).toEqual('GC'); + } + + const norm = frames.map(f => toDataFrameDTO(f)); + expect(norm).toMatchInlineSnapshot(` + Array [ + Object { + "fields": Array [ + Object { + "config": Object {}, + "labels": undefined, + "name": "Time", + "type": "time", + "values": Array [ + 1569334575000, + 1569334580000, + 1569334585000, + 1569334590000, + 1569334595000, + 1569334600000, + 1569334605000, + 1569334610000, + 1569334615000, + 1569334620000, + 1569334625000, + 1569334630000, + 1569334635000, + ], + }, + Object { + "config": Object {}, + "labels": undefined, + "name": "", + "type": "number", + "values": Array [ + 3, + 3, + 3, + 5, + 5, + 5, + 3, + 3, + 3, + 5, + 5, + 5, + 3, + ], + }, + ], + "meta": undefined, + "name": undefined, + "refId": "GC", + }, + ] + `); + }); + + test('processEmptyResults', () => { + const frames = toDataQueryResponse(emptyResults).data; + expect(frames.length).toEqual(0); + }); + + test('resultWithError', () => { + // Generated from: + // qdr.Responses[q.GetRefID()] = backend.DataResponse{ + // Error: fmt.Errorf("an Error: %w", fmt.Errorf("another error")), + // Frames: data.Frames{ + // { + // Fields: data.Fields{data.NewField("numbers", nil, []float64{1, 3})}, + // Meta: &data.FrameMeta{ + // Notices: []data.Notice{ + // { + // Severity: data.NoticeSeverityError, + // Text: "Text", + // }, + // }, + // }, + // }, + // }, + // } + const res = toDataQueryResponse(resWithError); + expect(res.error).toMatchInlineSnapshot(` + Object { + "message": "Hello Error", + "refId": "A", + } + `); + + const norm = res.data.map(f => toDataFrameDTO(f)); + expect(norm).toMatchInlineSnapshot(` + Array [ + Object { + "fields": Array [ + Object { + "config": Object {}, + "labels": undefined, + "name": "numbers", + "type": "number", + "values": Array [ + 1, + 3, + ], + }, + ], + "meta": Object { + "notices": Array [ + Object { + "severity": 2, + "text": "Text", + }, + ], + }, + "name": undefined, + "refId": "A", + }, + ] + `); + }); +}); diff --git a/packages/grafana-runtime/src/utils/queryResponse.ts b/packages/grafana-runtime/src/utils/queryResponse.ts new file mode 100644 index 00000000000..cea33b5addd --- /dev/null +++ b/packages/grafana-runtime/src/utils/queryResponse.ts @@ -0,0 +1,91 @@ +import { + DataQueryResponse, + arrowTableToDataFrame, + base64StringToArrowTable, + KeyValue, + LoadingState, + DataQueryError, +} from '@grafana/data'; + +interface DataResponse { + error?: string; + refId?: string; + dataframes?: string[]; + // series: null, + // tables: null, +} + +/** + * Parse the results from `/api/ds/query + */ +export function toDataQueryResponse(res: any): DataQueryResponse { + const rsp: DataQueryResponse = { data: [], state: LoadingState.Done }; + if (res.data?.results) { + const results: KeyValue = res.data.results; + for (const refId of Object.keys(results)) { + const dr = results[refId] as DataResponse; + if (dr) { + if (dr.error) { + if (!rsp.error) { + rsp.error = { + refId, + message: dr.error, + }; + rsp.state = LoadingState.Error; + } + } + + if (dr.dataframes) { + for (const b64 of dr.dataframes) { + const t = base64StringToArrowTable(b64); + const f = arrowTableToDataFrame(t); + if (!f.refId) { + f.refId = refId; + } + rsp.data.push(f); + } + } + } + } + } + + // When it is not an OK response, make sure the error gets added + if (res.status && res.status !== 200) { + if (rsp.state !== LoadingState.Error) { + rsp.state = LoadingState.Error; + } + if (!rsp.error) { + rsp.error = toDataQueryError(res); + } + } + + return rsp; +} + +/** + * Convert an object into a DataQueryError -- if this is an HTTP response, + * it will put the correct values in the error filds + */ +export function toDataQueryError(err: any): DataQueryError { + const error = (err || {}) as DataQueryError; + + if (!error.message) { + if (typeof err === 'string' || err instanceof String) { + return { message: err } as DataQueryError; + } + + let message = 'Query error'; + if (error.message) { + message = error.message; + } else if (error.data && error.data.message) { + message = error.data.message; + } else if (error.data && error.data.error) { + message = error.data.error; + } else if (error.status) { + message = `Query error: ${error.status} ${error.statusText}`; + } + error.message = message; + } + + return error; +} diff --git a/public/app/features/dashboard/state/runRequest.ts b/public/app/features/dashboard/state/runRequest.ts index 57d02406860..9a6810f705f 100644 --- a/public/app/features/dashboard/state/runRequest.ts +++ b/public/app/features/dashboard/state/runRequest.ts @@ -18,6 +18,7 @@ import { DataFrame, guessFieldTypes, } from '@grafana/data'; +import { toDataQueryError } from '@grafana/runtime'; import { emitDataRequestEvent } from './analyticsProcessor'; import { ExpressionDatasourceID, expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; @@ -117,7 +118,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest) of({ ...state.panelData, state: LoadingState.Error, - error: processQueryError(err), + error: toDataQueryError(err), }) ), tap(emitDataRequestEvent(datasource)), @@ -153,30 +154,6 @@ export function callQueryMethod(datasource: DataSourceApi, request: DataQueryReq return from(returnVal); } -export function processQueryError(err: any): DataQueryError { - const error = (err || {}) as DataQueryError; - - if (!error.message) { - if (typeof err === 'string' || err instanceof String) { - return { message: err } as DataQueryError; - } - - let message = 'Query error'; - if (error.message) { - message = error.message; - } else if (error.data && error.data.message) { - message = error.data.message; - } else if (error.data && error.data.error) { - message = error.data.error; - } else if (error.status) { - message = `Query error: ${error.status} ${error.statusText}`; - } - error.message = message; - } - - return error; -} - /** * All panels will be passed tables that have our best guess at colum type set * diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index a6b0d2aadd3..c0e010f9627 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -14,7 +14,6 @@ import { ScopedVars, TimeRange, DataFrame, - resultsToDataFrames, DataQueryResponse, LoadingState, toDataFrame, @@ -22,7 +21,7 @@ import { FieldType, LogRowModel, } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; @@ -496,6 +495,12 @@ export class CloudWatchDatasource extends DataSourceApi { + // NOTE: this function currently only processes binary results from: + // /api/ds/query -- it will retrun empty results most of the time + return toDataQueryResponse(val).data || []; + }; + return from(this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams)).pipe( map(response => resultsToDataFrames(response)), catchError(err => { diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 7f69be4660c..a86fd271190 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -14,13 +14,12 @@ import { DataFrame, } from '@grafana/data'; import { Scenario, TestDataQuery } from './types'; -import { getBackendSrv } from '@grafana/runtime'; +import { getBackendSrv, toDataQueryError } from '@grafana/runtime'; import { queryMetricTree } from './metricTree'; import { from, merge, Observable, of } from 'rxjs'; import { runStream } from './runStreams'; import templateSrv from 'app/features/templating/template_srv'; import { getSearchFilterScopedVar } from 'app/features/templating/utils'; -import { processQueryError } from 'app/features/dashboard/state/runRequest'; type TestData = TimeSeries | TableData; @@ -164,7 +163,7 @@ function runArrowFile(target: TestDataQuery, req: DataQueryRequest Date: Mon, 4 May 2020 11:57:11 -0700 Subject: [PATCH 26/99] Signing: allow unsigned plugin in dev mode (#24242) --- pkg/plugins/plugins.go | 44 +++++++++++++++++++++---------------- pkg/plugins/plugins_test.go | 3 +++ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 37da41d19d3..6ca914d1eaf 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -282,26 +282,32 @@ func (scanner *PluginScanner) loadPlugin(pluginJsonFilePath string) error { pluginCommon.PluginDir = filepath.Dir(pluginJsonFilePath) // For the time being, we choose to only require back-end plugins to be signed + // NOTE: the state is calculated again for when setting metadata on the object if pluginCommon.Backend && scanner.requireSigned { - scanner.log.Debug("Plugin signature required, validating", "pluginID", pluginCommon.Id, - "pluginDir", pluginCommon.PluginDir) - allowUnsigned := false - for _, plug := range scanner.cfg.PluginsAllowUnsigned { - if plug == pluginCommon.Id { - allowUnsigned = true - break - } - } - if sig := GetPluginSignatureState(&pluginCommon); sig != PluginSignatureValid && !allowUnsigned { - switch sig { - case PluginSignatureUnsigned: - return fmt.Errorf("plugin %q is unsigned", pluginCommon.Id) - case PluginSignatureInvalid: - return fmt.Errorf("plugin %q has an invalid signature", pluginCommon.Id) - case PluginSignatureModified: - return fmt.Errorf("plugin %q's signature has been modified", pluginCommon.Id) - default: - return fmt.Errorf("unrecognized plugin signature state %v", sig) + sig := GetPluginSignatureState(&pluginCommon) + if sig != PluginSignatureValid { + scanner.log.Debug("Invalid Plugin Signature", "pluginID", pluginCommon.Id, "pluginDir", pluginCommon.PluginDir, "state", sig) + if sig == PluginSignatureUnsigned { + allowUnsigned := false + for _, plug := range scanner.cfg.PluginsAllowUnsigned { + if plug == pluginCommon.Id { + allowUnsigned = true + break + } + } + if setting.Env != setting.DEV && !allowUnsigned { + return fmt.Errorf("plugin %q is unsigned", pluginCommon.Id) + } + scanner.log.Warn("Running an unsigned backend plugin", "pluginID", pluginCommon.Id, "pluginDir", pluginCommon.PluginDir) + } else { + switch sig { + case PluginSignatureInvalid: + return fmt.Errorf("plugin %q has an invalid signature", pluginCommon.Id) + case PluginSignatureModified: + return fmt.Errorf("plugin %q's signature has been modified", pluginCommon.Id) + default: + return fmt.Errorf("unrecognized plugin signature state %v", sig) + } } } } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 96eeaefe6e9..a8ee8ced5fc 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -18,15 +18,18 @@ import ( func TestPluginManager_Init(t *testing.T) { origRootPath := setting.StaticRootPath origRaw := setting.Raw + origEnv := setting.Env t.Cleanup(func() { setting.StaticRootPath = origRootPath setting.Raw = origRaw + setting.Env = origEnv }) var err error setting.StaticRootPath, err = filepath.Abs("../../public/") require.NoError(t, err) setting.Raw = ini.Empty() + setting.Env = setting.PROD t.Run("Base case", func(t *testing.T) { pm := &PluginManager{ From 726009870b32df743dc2bbb79779ad77c27fee81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 May 2020 21:14:37 +0200 Subject: [PATCH 27/99] LoginPage: New design (#23892) * LoginPage: initial poc * wIP * Prgress * Start Forms migration * Fix layout and change password animation * Migrate style to emotion * Fix small things * Remove classes * Fix logo and title * Disable disabled button * Add custom fields and fix layout * Update flyin animation * Change animation timing * Update comment * Same styles for submit button * Update snapshot * Minor tweaks and made slogan random Co-authored-by: Tobias Skarhed --- .../grafana-ui/src/components/Forms/Form.tsx | 1 + .../grafana-ui/src/themes/ThemeContext.tsx | 7 + packages/grafana-ui/src/themes/index.ts | 4 +- .../app/core/components/Branding/Branding.tsx | 43 +- .../core/components/Login/ChangePassword.tsx | 156 +- .../app/core/components/Login/LoginForm.tsx | 161 +- .../app/core/components/Login/LoginPage.tsx | 153 +- .../components/Login/LoginServiceButtons.tsx | 47 +- .../app/core/components/Login/UserSignup.tsx | 11 +- .../__snapshots__/SideMenu.test.tsx.snap | 2 +- public/img/login_background_dark.svg | 6465 ++++++++++++++++ public/img/login_background_light.svg | 6774 +++++++++++++++++ public/sass/pages/_login.scss | 395 +- 13 files changed, 13540 insertions(+), 679 deletions(-) create mode 100644 public/img/login_background_dark.svg create mode 100644 public/img/login_background_light.svg diff --git a/packages/grafana-ui/src/components/Forms/Form.tsx b/packages/grafana-ui/src/components/Forms/Form.tsx index 94fb26f848b..0e31eec5e08 100644 --- a/packages/grafana-ui/src/components/Forms/Form.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.tsx @@ -38,6 +38,7 @@ export function Form({
diff --git a/packages/grafana-ui/src/themes/ThemeContext.tsx b/packages/grafana-ui/src/themes/ThemeContext.tsx index ab89fb3b285..5e308ec73e4 100644 --- a/packages/grafana-ui/src/themes/ThemeContext.tsx +++ b/packages/grafana-ui/src/themes/ThemeContext.tsx @@ -4,6 +4,7 @@ import hoistNonReactStatics from 'hoist-non-react-statics'; import { getTheme } from './getTheme'; import { Themeable } from '../types/theme'; import { GrafanaTheme, GrafanaThemeType } from '@grafana/data'; +import { stylesFactory } from './stylesFactory'; type Omit = Pick>; type Subtract = Omit; @@ -37,6 +38,12 @@ export const withTheme =

(Component: Rea export function useTheme() { return useContext(ThemeContextMock || ThemeContext); } +/** Hook for using memoized styles with access to the theme. */ +export const useStyles = (getStyles: (theme?: GrafanaTheme) => any) => { + const currentTheme = useTheme(); + const callback = stylesFactory(stylesTheme => getStyles(stylesTheme)); + return callback(currentTheme); +}; /** * Enables theme context mocking diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts index d1e0ea0295f..77f86a763fe 100644 --- a/packages/grafana-ui/src/themes/index.ts +++ b/packages/grafana-ui/src/themes/index.ts @@ -1,8 +1,8 @@ -import { ThemeContext, withTheme, useTheme, mockThemeContext } from './ThemeContext'; +import { ThemeContext, withTheme, useTheme, useStyles, mockThemeContext } from './ThemeContext'; import { getTheme, mockTheme } from './getTheme'; import { selectThemeVariant } from './selectThemeVariant'; export { stylesFactory } from './stylesFactory'; -export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext }; +export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext, useStyles }; import * as styleMixins from './mixins'; export { styleMixins }; diff --git a/public/app/core/components/Branding/Branding.tsx b/public/app/core/components/Branding/Branding.tsx index aa41fbc5025..deb119b2398 100644 --- a/public/app/core/components/Branding/Branding.tsx +++ b/public/app/core/components/Branding/Branding.tsx @@ -1,42 +1,53 @@ import React, { FC } from 'react'; import { css, cx } from 'emotion'; +import { useTheme } from '@grafana/ui'; export interface BrandComponentProps { className?: string; children?: JSX.Element | JSX.Element[]; } -export const LoginLogo: FC = ({ className }) => { - const maxSize = css` - max-width: 150px; - `; - - return ( - <> - Grafana -

- - ); +const LoginLogo: FC = ({ className }) => { + return Grafana; }; -export const LoginBackground: FC = ({ className, children }) => { +const LoginBackground: FC = ({ className, children }) => { + const theme = useTheme(); const background = css` - background: url(public/img/heatmap_bg_test.svg); + background: url(public/img/login_background_${theme.isDark ? 'dark' : 'light'}.svg); background-size: cover; `; return
{children}
; }; -export const MenuLogo: FC = ({ className }) => { +const MenuLogo: FC = ({ className }) => { return Grafana; }; -export const AppTitle = 'Grafana'; +const LoginBoxBackground = () => { + const theme = useTheme(); + return css` + background: ${theme.isLight ? 'rgba(6, 30, 200, 0.1 )' : 'rgba(18, 28, 41, 0.65)'}; + background-size: cover; + `; +}; export class Branding { static LoginLogo = LoginLogo; static LoginBackground = LoginBackground; static MenuLogo = MenuLogo; - static AppTitle = AppTitle; + static LoginBoxBackground = LoginBoxBackground; + static AppTitle = 'Grafana'; + static LoginTitle = 'Welcome to Grafana'; + static GetLoginSubTitle = () => { + const slogans = [ + "Don't get in the way of the data", + 'Your single pane of glass', + 'Built better together', + 'Democratising data', + ]; + const count = slogans.length; + return slogans[Math.floor(Math.random() * count)]; + }; } diff --git a/public/app/core/components/Login/ChangePassword.tsx b/public/app/core/components/Login/ChangePassword.tsx index d1d5acd5589..47cac53f63a 100644 --- a/public/app/core/components/Login/ChangePassword.tsx +++ b/public/app/core/components/Login/ChangePassword.tsx @@ -1,138 +1,60 @@ -import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react'; -import { Tooltip } from '@grafana/ui'; -import { AppEvents } from '@grafana/data'; - -import appEvents from 'app/core/app_events'; +import React, { FC, SyntheticEvent } from 'react'; +import { Tooltip, Form, Field, Input, VerticalGroup, Button, LinkButton } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; - +import { submitButton } from './LoginForm'; interface Props { onSubmit: (pw: string) => void; - onSkip: Function; - focus?: boolean; + onSkip: (event?: SyntheticEvent) => void; } -interface State { +interface PasswordDTO { newPassword: string; confirmNew: string; - valid: boolean; } -export class ChangePassword extends PureComponent { - private userInput: HTMLInputElement; - constructor(props: Props) { - super(props); - this.state = { - newPassword: '', - confirmNew: '', - valid: false, - }; - } - - componentDidUpdate(prevProps: Props) { - if (!prevProps.focus && this.props.focus) { - this.focus(); - } - } - - focus() { - this.userInput.focus(); - } - - onSubmit = (e: SyntheticEvent) => { - e.preventDefault(); - - const { newPassword, valid } = this.state; - if (valid) { - this.props.onSubmit(newPassword); - } else { - appEvents.emit(AppEvents.alertWarning, ['New passwords do not match']); - } +export const ChangePassword: FC = ({ onSubmit, onSkip }) => { + const submit = (passwords: PasswordDTO) => { + onSubmit(passwords.newPassword); }; - - onNewPasswordChange = (e: ChangeEvent) => { - this.setState({ - newPassword: e.target.value, - valid: this.validate('newPassword', e.target.value), - }); - }; - - onConfirmPasswordChange = (e: ChangeEvent) => { - this.setState({ - confirmNew: e.target.value, - valid: this.validate('confirmNew', e.target.value), - }); - }; - - onSkip = (e: SyntheticEvent) => { - this.props.onSkip(); - }; - - validate(changed: string, pw: string) { - if (changed === 'newPassword') { - return this.state.confirmNew === pw; - } else if (changed === 'confirmNew') { - return this.state.newPassword === pw; - } - return false; - } - - render() { - return ( -
-
-
Change Password
- Before you can get started with awesome dashboards we need you to make your account more secure by changing - your password. -
- You can change your password again later. -
- -
- + {({ errors, register, getValues }) => ( + <> + + { - this.userInput = input; - }} + ref={register({ + required: 'New password required', + })} /> -
-
- + + v === getValues().newPassword || 'Passwords must match!', + })} /> -
-
+ + + - + Skip - + - - -
- -
- ); - } -} + + + )} + + ); +}; diff --git a/public/app/core/components/Login/LoginForm.tsx b/public/app/core/components/Login/LoginForm.tsx index 1ef5cde8ba4..c21c0a1c93f 100644 --- a/public/app/core/components/Login/LoginForm.tsx +++ b/public/app/core/components/Login/LoginForm.tsx @@ -1,122 +1,69 @@ -import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react'; +import React, { FC } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { FormModel } from './LoginCtrl'; +import { Button, Form, Input, Field } from '@grafana/ui'; +import { css } from 'emotion'; interface Props { displayForgotPassword: boolean; - onChange?: (valid: boolean) => void; onSubmit: (data: FormModel) => void; isLoggingIn: boolean; passwordHint: string; loginHint: string; } -interface State { - user: string; - password: string; - email: string; - valid: boolean; -} +const forgottenPasswordStyles = css` + display: inline-block; + margin-top: 16px; + float: right; +`; -export class LoginForm extends PureComponent { - private userInput: HTMLInputElement; - constructor(props: Props) { - super(props); - this.state = { - user: '', - password: '', - email: '', - valid: false, - }; - } +const wrapperStyles = css` + width: 100%; + padding-bottom: 16px; +`; - componentDidMount() { - this.userInput.focus(); - } - onSubmit = (e: SyntheticEvent) => { - e.preventDefault(); +export const submitButton = css` + justify-content: center; + width: 100%; +`; - const { user, password, email } = this.state; - if (this.state.valid) { - this.props.onSubmit({ user, password, email }); - } - }; - - onChangePassword = (e: ChangeEvent) => { - this.setState({ - password: e.target.value, - valid: this.validate(this.state.user, e.target.value), - }); - }; - - onChangeUsername = (e: ChangeEvent) => { - this.setState({ - user: e.target.value, - valid: this.validate(e.target.value, this.state.password), - }); - }; - - validate(user: string, password: string) { - return user.length > 0 && password.length > 0; - } - - render() { - return ( -
-
- { - this.userInput = input; - }} - type="text" - name="user" - className="gf-form-input login-form-input" - required - placeholder={this.props.loginHint} - aria-label={selectors.pages.Login.username} - onChange={this.onChangeUsername} - /> -
-
- -
-
- {!this.props.isLoggingIn ? ( - - ) : ( - - )} - - {this.props.displayForgotPassword ? ( - - ) : null} -
-
- ); - } -} +export const LoginForm: FC = ({ displayForgotPassword, onSubmit, isLoggingIn, passwordHint, loginHint }) => { + return ( +
+
+ {({ register, errors }) => ( + <> + + + + + + + + {displayForgotPassword && ( + + Forgot your password? + + )} + + )} +
+
+ ); +}; diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index f051825ef45..cafc12f96fd 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -1,6 +1,6 @@ // Libraries import React, { FC } from 'react'; -import { CSSTransition } from 'react-transition-group'; +import { cx, keyframes, css } from 'emotion'; // Components import { UserSignup } from './UserSignup'; @@ -9,20 +9,25 @@ import LoginCtrl from './LoginCtrl'; import { LoginForm } from './LoginForm'; import { ChangePassword } from './ChangePassword'; import { Branding } from 'app/core/components/Branding/Branding'; -import { Footer } from 'app/core/components/Footer/Footer'; +import { useStyles } from '@grafana/ui'; +import { GrafanaTheme } from '@grafana/data'; export const LoginPage: FC = () => { + const loginStyles = useStyles(getLoginStyles); return ( - -
-
- + +
+
+ +
+

{Branding.LoginTitle}

+

{Branding.GetLoginSubTitle()}

+
{({ loginHint, passwordHint, - isOauthEnabled, ldapEnabled, authProxyEnabled, disableLoginForm, @@ -33,37 +38,123 @@ export const LoginPage: FC = () => { skipPasswordChange, isChangingPassword, }) => ( -
-
- {!disableLoginForm ? ( - - ) : null} +
+ {!isChangingPassword && ( +
+ {!disableLoginForm && ( + + )} - - {!disableUserSignUp ? : null} -
- - - + + {!disableUserSignUp && } +
+ )} + + {isChangingPassword && ( +
+ +
+ )}
)}
-