diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index ffb6b04c20a..6ed45388327 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -29,22 +29,23 @@ type PluginSetting struct { } type PluginListItem struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Info plugins.Info `json:"info"` - Dependencies plugins.Dependencies `json:"dependencies"` - LatestVersion string `json:"latestVersion"` - HasUpdate bool `json:"hasUpdate"` - DefaultNavUrl string `json:"defaultNavUrl"` - Category string `json:"category"` - State plugins.ReleaseState `json:"state"` - Signature plugins.SignatureStatus `json:"signature"` - SignatureType plugins.SignatureType `json:"signatureType"` - SignatureOrg string `json:"signatureOrg"` - AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Info plugins.Info `json:"info"` + Dependencies plugins.Dependencies `json:"dependencies"` + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` + DefaultNavUrl string `json:"defaultNavUrl"` + Category string `json:"category"` + State plugins.ReleaseState `json:"state"` + Signature plugins.SignatureStatus `json:"signature"` + SignatureType plugins.SignatureType `json:"signatureType"` + SignatureOrg string `json:"signatureOrg"` + AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` + AngularDetected bool `json:"angularDetected"` } type PluginList []PluginListItem diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 505004492b7..2791d5e272c 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -64,16 +64,17 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro } panels[panel.ID] = plugins.PanelDTO{ - ID: panel.ID, - Name: panel.Name, - Info: panel.Info, - Module: panel.Module, - BaseURL: panel.BaseURL, - SkipDataQuery: panel.SkipDataQuery, - HideFromList: panel.HideFromList, - ReleaseState: string(panel.State), - Signature: string(panel.Signature), - Sort: getPanelSort(panel.ID), + ID: panel.ID, + Name: panel.Name, + Info: panel.Info, + Module: panel.Module, + BaseURL: panel.BaseURL, + SkipDataQuery: panel.SkipDataQuery, + HideFromList: panel.HideFromList, + ReleaseState: string(panel.State), + Signature: string(panel.Signature), + Sort: getPanelSort(panel.ID), + AngularDetected: panel.AngularDetected, } } @@ -317,6 +318,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug Module: plugin.Module, BaseURL: plugin.BaseURL, } + dsDTO.AngularDetected = plugin.AngularDetected if ds.JsonData == nil { dsDTO.JSONData = make(map[string]interface{}) @@ -389,6 +391,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug Module: ds.Module, BaseURL: ds.BaseURL, }, + AngularDetected: ds.AngularDetected, } if ds.Name == grafanads.DatasourceName { dto.ID = grafanads.DatasourceID @@ -403,10 +406,11 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug func newAppDTO(plugin plugins.PluginDTO, settings pluginsettings.InfoDTO) *plugins.AppDTO { app := &plugins.AppDTO{ - ID: plugin.ID, - Version: plugin.Info.Version, - Path: plugin.Module, - Preload: false, + ID: plugin.ID, + Version: plugin.Info.Version, + Path: plugin.Module, + Preload: false, + AngularDetected: plugin.AngularDetected, } if settings.Enabled { diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 8f9b9eb6b4e..95f31511add 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -280,6 +280,41 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { }, }, }, + { + desc: "angular app plugin", + pluginStore: func() plugins.Store { + return &plugins.FakePluginStore{ + PluginList: []plugins.PluginDTO{ + { + Module: fmt.Sprintf("/%s/module.js", "test-app"), + JSONData: plugins.JSONData{ + ID: "test-app", + Info: plugins.Info{Version: "0.5.0"}, + Type: plugins.App, + Preload: true, + }, + AngularDetected: true, + }, + }, + } + }, + pluginSettings: func() pluginsettings.Service { + return &pluginsettings.FakePluginSettings{ + Plugins: newAppSettings("test-app", true), + } + }, + expected: settings{ + Apps: map[string]*plugins.AppDTO{ + "test-app": { + ID: "test-app", + Preload: true, + Path: "/test-app/module.js", + Version: "0.5.0", + AngularDetected: true, + }, + }, + }, + }, } for _, test := range tests { diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 7c424ad6bf3..792f177909d 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -128,18 +128,19 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons result := make(dtos.PluginList, 0) for _, pluginDef := range filteredPluginDefinitions { listItem := dtos.PluginListItem{ - Id: pluginDef.ID, - Name: pluginDef.Name, - Type: string(pluginDef.Type), - Category: pluginDef.Category, - Info: pluginDef.Info, - Dependencies: pluginDef.Dependencies, - DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, pluginDef.DefaultNavURL), - State: pluginDef.State, - Signature: pluginDef.Signature, - SignatureType: pluginDef.SignatureType, - SignatureOrg: pluginDef.SignatureOrg, - AccessControl: pluginsMetadata[pluginDef.ID], + Id: pluginDef.ID, + Name: pluginDef.Name, + Type: string(pluginDef.Type), + Category: pluginDef.Category, + Info: pluginDef.Info, + Dependencies: pluginDef.Dependencies, + DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, pluginDef.DefaultNavURL), + State: pluginDef.State, + Signature: pluginDef.Signature, + SignatureType: pluginDef.SignatureType, + SignatureOrg: pluginDef.SignatureOrg, + AccessControl: pluginsMetadata[pluginDef.ID], + AngularDetected: pluginDef.AngularDetected, } update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID) diff --git a/pkg/plugins/manager/loader/angulardetector/angulardetector.go b/pkg/plugins/manager/loader/angulardetector/angulardetector.go new file mode 100644 index 00000000000..4f13aa24007 --- /dev/null +++ b/pkg/plugins/manager/loader/angulardetector/angulardetector.go @@ -0,0 +1,82 @@ +package angulardetector + +import ( + "bytes" + "fmt" + "io" + "regexp" + + "github.com/grafana/grafana/pkg/plugins" +) + +var ( + _ detector = &containsBytesDetector{} + _ detector = ®exDetector{} +) + +// detector implements a check to see if a plugin uses Angular. +type detector interface { + // Detect takes the content of a moduleJs file and returns true if the plugin is using Angular. + Detect(moduleJs []byte) bool +} + +// containsBytesDetector is a detector that returns true if module.js contains the "pattern" string. +type containsBytesDetector struct { + pattern []byte +} + +// Detect returns true if moduleJs contains the byte slice d.pattern. +func (d *containsBytesDetector) Detect(moduleJs []byte) bool { + return bytes.Contains(moduleJs, d.pattern) +} + +// regexDetector is a detector that returns true if the module.js content matches a regular expression. +type regexDetector struct { + regex *regexp.Regexp +} + +// Detect returns true if moduleJs matches the regular expression d.regex. +func (d *regexDetector) Detect(moduleJs []byte) bool { + return d.regex.Match(moduleJs) +} + +// angularDetectors contains all the detectors to detect Angular plugins. +// They are executed in the specified order. +var angularDetectors = []detector{ + &containsBytesDetector{pattern: []byte("PanelCtrl")}, + &containsBytesDetector{pattern: []byte("QueryCtrl")}, + &containsBytesDetector{pattern: []byte("app/plugins/sdk")}, + &containsBytesDetector{pattern: []byte("angular.isNumber(")}, + &containsBytesDetector{pattern: []byte("editor.html")}, + &containsBytesDetector{pattern: []byte("ctrl.annotation")}, + &containsBytesDetector{pattern: []byte("getLegacyAngularInjector")}, + + ®exDetector{regex: regexp.MustCompile(`['"](app/core/utils/promiseToDigest)|(app/plugins/.*?)|(app/core/core_module)['"]`)}, + ®exDetector{regex: regexp.MustCompile(`from\s+['"]grafana\/app\/`)}, + ®exDetector{regex: regexp.MustCompile(`System\.register\(`)}, +} + +// Inspect open module.js and checks if the plugin is using Angular by matching against its source code. +// It returns true if module.js matches against any of the detectors in angularDetectors. +func Inspect(p *plugins.Plugin) (isAngular bool, err error) { + f, err := p.FS.Open("module.js") + if err != nil { + return false, fmt.Errorf("open module.js: %w", err) + } + defer func() { + if closeErr := f.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("close module.js: %w", closeErr) + } + }() + b, err := io.ReadAll(f) + if err != nil { + return false, fmt.Errorf("module.js readall: %w", err) + } + for _, d := range angularDetectors { + if d.Detect(b) { + isAngular = true + break + } + } + return +} diff --git a/pkg/plugins/manager/loader/angulardetector/angulardetector_test.go b/pkg/plugins/manager/loader/angulardetector/angulardetector_test.go new file mode 100644 index 00000000000..ca595df1520 --- /dev/null +++ b/pkg/plugins/manager/loader/angulardetector/angulardetector_test.go @@ -0,0 +1,60 @@ +package angulardetector + +import ( + "strconv" + "testing" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/stretchr/testify/require" +) + +func TestAngularDetector_Inspect(t *testing.T) { + type tc struct { + name string + plugin *plugins.Plugin + exp bool + } + var tcs []tc + + // Angular imports + for i, content := range [][]byte{ + []byte(`import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';`), + []byte(`define(["app/plugins/sdk"],(function(n){return function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof`), + []byte(`define(["app/plugins/sdk"],(function(n){return function(n){var t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return n[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=t,e.d=function(n,t,r){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:r})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toSt`), + []byte(`define(["react","lodash","@grafana/data","@grafana/ui","@emotion/css","@grafana/runtime","moment","app/core/utils/datemath","jquery","app/plugins/sdk","app/core/core_module","app/core/core","app/core/table_model","app/core/utils/kbn","app/core/config","angular"],(function(e,t,r,n,i,a,o,s,u,l,c,p,f,h,d,m){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};retur`), + } { + tcs = append(tcs, tc{ + name: "angular " + strconv.Itoa(i), + plugin: &plugins.Plugin{ + FS: plugins.NewInMemoryFS(map[string][]byte{ + "module.js": content, + }), + }, + exp: true, + }) + } + + // Not angular + tcs = append(tcs, tc{ + name: "not angular", + plugin: &plugins.Plugin{ + FS: plugins.NewInMemoryFS(map[string][]byte{ + "module.js": []byte(`import { PanelPlugin } from '@grafana/data'`), + }), + }, + exp: false, + }) + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + isAngular, err := Inspect(tc.plugin) + require.NoError(t, err) + require.Equal(t, tc.exp, isAngular) + }) + } + + t.Run("no module.js", func(t *testing.T) { + p := &plugins.Plugin{FS: plugins.NewInMemoryFS(map[string][]byte{})} + _, err := Inspect(p) + require.ErrorIs(t, err, plugins.ErrFileNotExist) + }) +} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 0ebce9e494a..f1fbefe9e78 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angulardetector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" @@ -153,6 +154,15 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun } } + // Detect angular for external plugins + if plugin.IsExternalPlugin() { + var err error + plugin.AngularDetected, err = angulardetector.Inspect(plugin) + if err != nil { + l.log.Warn("could not inspect plugin for angular", "pluginID", plugin.ID, "err", err) + } + } + if plugin.IsApp() { setDefaultNavURL(plugin) } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 3c10f3736c2..ad7674fa273 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -210,18 +210,19 @@ type PluginMetaDTO struct { } type DataSourceDTO struct { - ID int64 `json:"id,omitempty"` - UID string `json:"uid,omitempty"` - Type string `json:"type"` - Name string `json:"name"` - PluginMeta *PluginMetaDTO `json:"meta"` - URL string `json:"url,omitempty"` - IsDefault bool `json:"isDefault"` - Access string `json:"access,omitempty"` - Preload bool `json:"preload"` - Module string `json:"module,omitempty"` - JSONData map[string]interface{} `json:"jsonData"` - ReadOnly bool `json:"readOnly"` + ID int64 `json:"id,omitempty"` + UID string `json:"uid,omitempty"` + Type string `json:"type"` + Name string `json:"name"` + PluginMeta *PluginMetaDTO `json:"meta"` + URL string `json:"url,omitempty"` + IsDefault bool `json:"isDefault"` + Access string `json:"access,omitempty"` + Preload bool `json:"preload"` + Module string `json:"module,omitempty"` + JSONData map[string]interface{} `json:"jsonData"` + ReadOnly bool `json:"readOnly"` + AngularDetected bool `json:"angularDetected"` BasicAuth string `json:"basicAuth,omitempty"` WithCredentials bool `json:"withCredentials,omitempty"` @@ -241,23 +242,25 @@ type DataSourceDTO struct { } type PanelDTO struct { - ID string `json:"id"` - Name string `json:"name"` - Info Info `json:"info"` - HideFromList bool `json:"hideFromList"` - Sort int `json:"sort"` - SkipDataQuery bool `json:"skipDataQuery"` - ReleaseState string `json:"state"` - BaseURL string `json:"baseUrl"` - Signature string `json:"signature"` - Module string `json:"module"` + ID string `json:"id"` + Name string `json:"name"` + Info Info `json:"info"` + HideFromList bool `json:"hideFromList"` + Sort int `json:"sort"` + SkipDataQuery bool `json:"skipDataQuery"` + ReleaseState string `json:"state"` + BaseURL string `json:"baseUrl"` + Signature string `json:"signature"` + Module string `json:"module"` + AngularDetected bool `json:"angularDetected"` } type AppDTO struct { - ID string `json:"id"` - Path string `json:"path"` - Version string `json:"version"` - Preload bool `json:"preload"` + ID string `json:"id"` + Path string `json:"path"` + Version string `json:"version"` + Preload bool `json:"preload"` + AngularDetected bool `json:"angularDetected"` } const ( diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index a102ff7e71a..8165149c787 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -51,6 +51,8 @@ type Plugin struct { Module string BaseURL string + AngularDetected bool + Renderer pluginextensionv2.RendererPlugin SecretsManager secretsmanagerplugin.SecretsManagerPlugin client backendplugin.Plugin @@ -80,6 +82,8 @@ type PluginDTO struct { // SystemJS fields Module string BaseURL string + + AngularDetected bool } func (p PluginDTO) SupportsStreaming() bool { @@ -424,6 +428,7 @@ func (p *Plugin) ToDTO() PluginDTO { SignatureError: p.SignatureError, Module: p.Module, BaseURL: p.BaseURL, + AngularDetected: p.AngularDetected, } } diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 292386e1b24..3ad896af460 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -33,7 +33,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Alertmanager", @@ -74,7 +75,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Annotations list", @@ -110,7 +112,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Azure Monitor", @@ -168,7 +171,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Bar chart", @@ -204,7 +208,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Bar gauge", @@ -240,7 +245,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Candlestick", @@ -276,7 +282,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Canvas", @@ -312,7 +319,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "CloudWatch", @@ -348,7 +356,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Dashboard list", @@ -384,7 +393,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Datagrid", @@ -420,7 +430,8 @@ "state": "beta", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Elasticsearch", @@ -461,7 +472,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Flame Graph", @@ -497,7 +509,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Gauge", @@ -533,7 +546,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Geomap", @@ -569,7 +583,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Getting Started", @@ -605,7 +620,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Google Cloud Monitoring", @@ -641,7 +657,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Grafana Pyroscope", @@ -682,7 +699,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Graph (old)", @@ -718,7 +736,8 @@ "state": "deprecated", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Graphite", @@ -763,7 +782,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Heatmap", @@ -799,7 +819,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Histogram", @@ -835,7 +856,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "InfluxDB", @@ -871,7 +893,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Jaeger", @@ -916,7 +939,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Logs", @@ -952,7 +976,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Loki", @@ -997,7 +1022,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Microsoft SQL Server", @@ -1033,7 +1059,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "MySQL", @@ -1069,7 +1096,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "News", @@ -1105,7 +1133,8 @@ "state": "beta", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Node Graph", @@ -1141,7 +1170,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "OpenTSDB", @@ -1177,7 +1207,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Parca", @@ -1218,7 +1249,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Pie chart", @@ -1254,7 +1286,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "PostgreSQL", @@ -1290,7 +1323,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Prometheus", @@ -1331,7 +1365,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Stat", @@ -1367,7 +1402,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "State timeline", @@ -1403,7 +1439,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Status history", @@ -1439,7 +1476,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Table", @@ -1475,7 +1513,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Table (old)", @@ -1511,7 +1550,8 @@ "state": "deprecated", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Tempo", @@ -1552,7 +1592,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "TestData", @@ -1588,7 +1629,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Text", @@ -1624,7 +1666,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Time series", @@ -1660,7 +1703,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Traces", @@ -1696,7 +1740,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Trend", @@ -1732,7 +1777,8 @@ "state": "beta", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Welcome", @@ -1768,7 +1814,8 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "XY Chart", @@ -1804,7 +1851,8 @@ "state": "beta", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false }, { "name": "Zipkin", @@ -1845,6 +1893,7 @@ "state": "", "signature": "internal", "signatureType": "", - "signatureOrg": "" + "signatureOrg": "", + "angularDetected": false } ] \ No newline at end of file