Plugins: Angular deprecation: Detect Angular plugins and expose in API (#66824)

* Plugins: Angular deprecation: Detect Angular plugins and expose in API

* Plugins: Angular detector: Close module.js

* Plugins: Angular detector: consistent error messages

* Plugins: Angular detector: Add test for missing module.js

* Plugins: Angular detector: Fix integration tests

* Plugins: Angular detector: Changed Angular detection patterns

* Moved inMemoryFS to test_utils.go

* Add different angular detectors

* Plugins: Update plugins/data/expectedListResp.json

* Plugins: Rename angular property to angularDetected

* Plugins: Rename angular to angularDetected in Plugin and PluginDTO

* Plugins: Add angularDetected to datasources, apps and plugins frontendsettings

* Plugins: Add test for AngularDetected frontend settings
This commit is contained in:
Giuseppe Guerra 2023-05-12 12:51:11 +02:00 committed by GitHub
parent 4310f574db
commit 16359c82a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 367 additions and 117 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -0,0 +1,82 @@
package angulardetector
import (
"bytes"
"fmt"
"io"
"regexp"
"github.com/grafana/grafana/pkg/plugins"
)
var (
_ detector = &containsBytesDetector{}
_ detector = &regexDetector{}
)
// 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")},
&regexDetector{regex: regexp.MustCompile(`['"](app/core/utils/promiseToDigest)|(app/plugins/.*?)|(app/core/core_module)['"]`)},
&regexDetector{regex: regexp.MustCompile(`from\s+['"]grafana\/app\/`)},
&regexDetector{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
}

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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 (

View File

@ -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,
}
}

View File

@ -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
}
]