Plugins: Add fuzzy search to plugins catalogue (#81001)

* WIP add fuzzysearch to plugins catalog

* Add keywords to the plugins listing output

* add fuzzy search to plugin catalog, add keywords to plugins at frontend side

* refactor fuzzysearch function after review

* review changes

* change the version of uFuzzy library

* change reduce result object in getPluginDetailsForFuzzySearch

* fix yarn lock error

* fix helpers tests

* fix frontend searching test

* fix frontend linting issues

* fix tests

---------

Co-authored-by: Esteban Beltran <esteban@academo.me>
Co-authored-by: Giuseppe Guerra <giuseppe@guerra.in>
This commit is contained in:
Yulia Shanyrova 2024-02-14 14:30:24 +01:00 committed by GitHub
parent cf65d91ee9
commit 9dcb7800de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 314 additions and 164 deletions

View File

@ -143,6 +143,7 @@ func TestFinder_Find(t *testing.T) {
{Name: "img1", Path: "img/screenshot1.png"},
{Name: "img2", Path: "img/screenshot2.png"},
},
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "3.x.x",
@ -176,40 +177,19 @@ func TestFinder_Find(t *testing.T) {
{
name: "Multiple plugin dirs",
pluginDirs: []string{"../../testdata/duplicate-plugins", "../../testdata/invalid-v1-signature"},
expectedBundles: []*plugins.FoundBundle{{
Primary: plugins.FoundPlugin{
JSONData: plugins.JSONData{
ID: "test-app",
Type: plugins.TypeDataSource,
Name: "Parent",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Grafana Labs",
URL: "http://grafana.com",
},
Description: "Parent plugin",
Version: "1.0.0",
Updated: "2020-10-20",
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
},
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
},
Children: []*plugins.FoundPlugin{
{
expectedBundles: []*plugins.FoundBundle{
{
Primary: plugins.FoundPlugin{
JSONData: plugins.JSONData{
ID: "test-app",
Type: plugins.TypeDataSource,
Name: "Child",
Name: "Parent",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Grafana Labs",
URL: "http://grafana.com",
},
Description: "Child plugin",
Description: "Parent plugin",
Version: "1.0.0",
Updated: "2020-10-20",
},
@ -218,10 +198,32 @@ func TestFinder_Find(t *testing.T) {
Plugins: []plugins.Dependency{},
},
},
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
},
Children: []*plugins.FoundPlugin{
{
JSONData: plugins.JSONData{
ID: "test-app",
Type: plugins.TypeDataSource,
Name: "Child",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Grafana Labs",
URL: "http://grafana.com",
},
Description: "Child plugin",
Version: "1.0.0",
Updated: "2020-10-20",
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
},
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
},
},
},
},
{
Primary: plugins.FoundPlugin{
JSONData: plugins.JSONData{

View File

@ -154,7 +154,8 @@ func TestLoader_Load(t *testing.T) {
Class: plugins.ClassBundled,
},
},
}, {
},
{
name: "Load plugin with symbolic links",
class: plugins.ClassExternal,
cfg: &config.Cfg{Features: featuremgmt.WithFeatures()},
@ -183,8 +184,9 @@ func TestLoader_Load(t *testing.T) {
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
},
Version: "1.0.0",
Updated: "2015-02-10",
Version: "1.0.0",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "3.x.x",
@ -212,7 +214,8 @@ func TestLoader_Load(t *testing.T) {
Name: "Nginx Panel",
Type: string(plugins.TypePanel),
Role: org.RoleViewer,
Slug: "nginx-panel"},
Slug: "nginx-panel",
},
{
Name: "Nginx Datasource",
Type: string(plugins.TypeDataSource),
@ -230,7 +233,8 @@ func TestLoader_Load(t *testing.T) {
SignatureOrg: "Grafana Labs",
},
},
}, {
},
{
name: "Load an unsigned plugin (development)",
class: plugins.ClassExternal,
cfg: &config.Cfg{
@ -383,7 +387,8 @@ func TestLoader_Load(t *testing.T) {
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=8.0.0",

View File

@ -130,6 +130,7 @@ type Info struct {
Screenshots []Screenshots `json:"screenshots"`
Version string `json:"version"`
Updated string `json:"updated"`
Keywords []string `json:"keywords"`
}
type InfoLink struct {

View File

@ -49,7 +49,8 @@ func Test_ReadPluginJSON(t *testing.T) {
{Path: "img/screenshot1.png", Name: "img1"},
{Path: "img/screenshot2.png", Name: "img2"},
},
Updated: "2015-02-10",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: Dependencies{
GrafanaVersion: "3.x.x",
@ -107,7 +108,7 @@ func Test_ReadPluginJSON(t *testing.T) {
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "grafana-pyroscope-datasource",
"type": "datasource",
"type": "datasource",
"aliasIDs": ["phlare"]
}`
return io.NopCloser(strings.NewReader(pJSON))

View File

@ -154,7 +154,8 @@ func TestLoader_Load(t *testing.T) {
Class: plugins.ClassBundled,
},
},
}, {
},
{
name: "Load plugin with symbolic links",
class: plugins.ClassExternal,
cfg: &config.Cfg{Features: featuremgmt.WithFeatures()},
@ -183,8 +184,9 @@ func TestLoader_Load(t *testing.T) {
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
},
Version: "1.0.0",
Updated: "2015-02-10",
Version: "1.0.0",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "3.x.x",
@ -212,7 +214,8 @@ func TestLoader_Load(t *testing.T) {
Name: "Nginx Panel",
Type: string(plugins.TypePanel),
Role: org.RoleViewer,
Slug: "nginx-panel"},
Slug: "nginx-panel",
},
{
Name: "Nginx Datasource",
Type: string(plugins.TypeDataSource),
@ -230,7 +233,8 @@ func TestLoader_Load(t *testing.T) {
SignatureOrg: "Grafana Labs",
},
},
}, {
},
{
name: "Load an unsigned plugin (development)",
class: plugins.ClassExternal,
cfg: &config.Cfg{
@ -269,7 +273,8 @@ func TestLoader_Load(t *testing.T) {
Signature: "unsigned",
},
},
}, {
},
{
name: "Load an unsigned plugin (production)",
class: plugins.ClassExternal,
cfg: &config.Cfg{Features: featuremgmt.WithFeatures()},
@ -392,38 +397,40 @@ func TestLoader_Load(t *testing.T) {
},
pluginPaths: []string{filepath.Join(testDataDir(t), "test-app-with-includes")},
want: []*plugins.Plugin{
{JSONData: plugins.JSONData{
ID: "test-app",
Type: plugins.TypeApp,
Name: "Test App",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
{
JSONData: plugins.JSONData{
ID: "test-app",
Type: plugins.TypeApp,
Name: "Test App",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
},
Description: "Official Grafana Test App & Dashboard bundle",
Version: "1.0.0",
Links: []plugins.InfoLink{
{Name: "Project site", URL: "http://project.com"},
{Name: "License & Terms", URL: "http://license.com"},
},
Logos: plugins.Logos{
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Description: "Official Grafana Test App & Dashboard bundle",
Version: "1.0.0",
Links: []plugins.InfoLink{
{Name: "Project site", URL: "http://project.com"},
{Name: "License & Terms", URL: "http://license.com"},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=8.0.0",
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
Logos: plugins.Logos{
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
Includes: []*plugins.Includes{
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"},
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
},
Updated: "2015-02-10",
Backend: false,
},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=8.0.0",
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
Includes: []*plugins.Includes{
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"},
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
},
Backend: false,
},
DefaultNavURL: "/plugins/test-app/page/root-page-react",
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-includes")),
Class: plugins.ClassExternal,
@ -570,10 +577,12 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
backendFactoryProvider.BackendFactoryFunc = func(ctx context.Context, plugin *plugins.Plugin) backendplugin.PluginFactoryFunc {
return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) {
require.Equal(t, "grafana-test-datasource", pluginID)
require.Equal(t, []string{"GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=",
require.Equal(t, []string{
"GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=",
"GF_ENTERPRISE_APP_URL=", "GF_ENTERPRISE_LICENSE_TEXT=", "GF_APP_URL=",
"GF_PLUGIN_APP_CLIENT_ID=client-id", "GF_PLUGIN_APP_CLIENT_SECRET=secretz",
"GF_PLUGIN_APP_PRIVATE_KEY=priv@t3", "GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAuth"}, env())
"GF_PLUGIN_APP_PRIVATE_KEY=priv@t3", "GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAuth",
}, env())
return &fakes.FakeBackendPlugin{}, nil
}
}
@ -614,38 +623,39 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
}
pluginPaths := []string{filepath.Join(testDataDir(t), "external-registration")}
expected := []*plugins.Plugin{
{JSONData: plugins.JSONData{
ID: "grafana-test-datasource",
Type: plugins.TypeDataSource,
Name: "Test",
Backend: true,
Executable: "gpx_test_datasource",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Grafana Labs",
URL: "https://grafana.com",
{
JSONData: plugins.JSONData{
ID: "grafana-test-datasource",
Type: plugins.TypeDataSource,
Name: "Test",
Backend: true,
Executable: "gpx_test_datasource",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Grafana Labs",
URL: "https://grafana.com",
},
Version: "1.0.0",
Logos: plugins.Logos{
Small: "public/plugins/grafana-test-datasource/img/ds.svg",
Large: "public/plugins/grafana-test-datasource/img/ds.svg",
},
Updated: "2023-08-03",
Screenshots: []plugins.Screenshots{},
},
Version: "1.0.0",
Logos: plugins.Logos{
Small: "public/plugins/grafana-test-datasource/img/ds.svg",
Large: "public/plugins/grafana-test-datasource/img/ds.svg",
Dependencies: plugins.Dependencies{
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
Updated: "2023-08-03",
Screenshots: []plugins.Screenshots{},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
IAM: &plugindef.IAM{
Permissions: []plugindef.Permission{
{
Action: "read",
Scope: stringPtr("datasource"),
IAM: &plugindef.IAM{
Permissions: []plugindef.Permission{
{
Action: "read",
Scope: stringPtr("datasource"),
},
},
},
},
},
FS: mustNewStaticFSForTests(t, pluginPaths[0]),
Class: plugins.ClassExternal,
Signature: plugins.SignatureStatusUnsigned,
@ -662,10 +672,12 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
backendFactoryProvider.BackendFactoryFunc = func(ctx context.Context, plugin *plugins.Plugin) backendplugin.PluginFactoryFunc {
return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) {
require.Equal(t, "grafana-test-datasource", pluginID)
require.Equal(t, []string{"GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=",
require.Equal(t, []string{
"GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=",
"GF_ENTERPRISE_APP_URL=", "GF_ENTERPRISE_LICENSE_TEXT=", "GF_APP_URL=",
"GF_PLUGIN_APP_CLIENT_ID=client-id", "GF_PLUGIN_APP_CLIENT_SECRET=secretz",
"GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAuth"}, env())
"GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAuth",
}, env())
return &fakes.FakeBackendPlugin{}, nil
}
}
@ -906,7 +918,8 @@ func TestLoader_Load_RBACReady(t *testing.T) {
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "*",
@ -1049,7 +1062,8 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
},
Updated: "2015-02-10",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "3.x.x",
@ -1129,7 +1143,8 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
},
Updated: "2015-02-10",
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "3.x.x",
@ -1476,6 +1491,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Description: "Grafana App Plugin Template",
Version: "",
Updated: "",
Keywords: []string{"panel", "template"},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "7.0.0",
@ -1556,6 +1572,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Description: "Grafana Panel Plugin Template",
Version: "",
Updated: "",
Keywords: []string{"panel", "template"},
},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=7.0.0",
@ -1612,7 +1629,8 @@ type loaderDepOpts struct {
}
func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Manager,
backendFactory plugins.BackendFactoryProvider, sigErrTracker pluginerrs.SignatureErrorTracker) *Loader {
backendFactory plugins.BackendFactoryProvider, sigErrTracker pluginerrs.SignatureErrorTracker,
) *Loader {
assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg))
lic := fakes.NewFakeLicensingService()
angularInspector := angularinspector.NewStaticInspector()
@ -1662,7 +1680,8 @@ func newLoaderWithOpts(t *testing.T, cfg *config.Cfg, opts loaderDepOpts) *Loade
}
func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service,
procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) {
procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager,
) {
t.Helper()
for _, p := range ps {

View File

@ -19,7 +19,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -61,7 +62,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -98,7 +100,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -157,7 +160,14 @@
}
],
"version": "",
"updated": ""
"updated": "",
"keywords": [
"azure",
"monitor",
"Application Insights",
"Log Analytics",
"App Insights"
]
},
"dependencies": {
"grafanaDependency": "\u003e=10.3.0",
@ -194,7 +204,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -231,7 +242,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -268,7 +280,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -305,7 +318,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -342,7 +356,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -379,7 +394,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -416,7 +432,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -458,7 +475,10 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": [
"elasticsearch"
]
},
"dependencies": {
"grafanaDependency": "",
@ -495,7 +515,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -532,7 +553,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -569,7 +591,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -606,7 +629,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -643,7 +667,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -685,7 +710,16 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": [
"grafana",
"datasource",
"phlare",
"flamegraph",
"profiling",
"continuous profiling",
"pyroscope"
]
},
"dependencies": {
"grafanaDependency": "\u003e=10.3.0-0",
@ -722,7 +756,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -768,7 +803,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -805,7 +841,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -842,7 +879,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -879,7 +917,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -925,7 +964,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -962,7 +1002,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1008,7 +1049,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1045,7 +1087,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1082,7 +1125,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1119,7 +1163,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1156,7 +1201,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1193,7 +1239,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1235,7 +1282,13 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": [
"grafana",
"datasource",
"parca",
"profiling"
]
},
"dependencies": {
"grafanaDependency": "\u003e=10.3.0-0",
@ -1272,7 +1325,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1309,7 +1363,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1351,7 +1406,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1388,7 +1444,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1425,7 +1482,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1462,7 +1520,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1499,7 +1558,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1536,7 +1596,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1578,7 +1639,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "\u003e=10.3.0-0",
@ -1615,7 +1677,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "\u003e=10.3.0-0",
@ -1652,7 +1715,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1689,7 +1753,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1726,7 +1791,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1763,7 +1829,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1800,7 +1867,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1837,7 +1905,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
@ -1879,7 +1948,8 @@
"build": {},
"screenshots": null,
"version": "",
"updated": ""
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",

View File

@ -11,6 +11,7 @@ export default {
small: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.2.2/logos/small',
large: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.2.2/logos/large',
},
keywords: ['zabbix', 'monitoring', 'dashboard'],
},
isCore: false,
isDev: false,

View File

@ -9,6 +9,7 @@ export default {
downloads: 33645089,
featured: 180,
id: 74,
keywords: ['zabbix', 'monitoring', 'dashboard'],
typeId: 1,
typeName: 'Application',
internal: false,

View File

@ -17,6 +17,7 @@ const plugin: CatalogPlugin = {
id: 'test-plugin',
info: {
logos: { small: '', large: '' },
keywords: ['test', 'plugin'],
},
name: 'Testing Plugin',
orgName: 'Test',

View File

@ -15,6 +15,7 @@ const plugin: CatalogPlugin = {
id: 'test-plugin',
info: {
logos: { small: '', large: '' },
keywords: ['test', 'plugin'],
},
name: 'Testing Plugin',
orgName: 'Test',

View File

@ -32,6 +32,7 @@ const getMockPlugin = (id: string): CatalogPlugin => {
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
},
keywords: ['test', 'plugin'],
},
name: 'Testing Plugin',
orgName: 'Test',

View File

@ -42,6 +42,7 @@ describe('PluginListItem', () => {
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
},
keywords: ['test', 'plugin'],
},
name: 'Testing Plugin',
orgName: 'Test',

View File

@ -18,6 +18,7 @@ describe('PluginListItemBadges', () => {
small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
},
keywords: ['test', 'plugin'],
},
name: 'Testing Plugin',
orgName: 'Test',

View File

@ -140,6 +140,7 @@ describe('Plugins/Helpers', () => {
large: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/large',
small: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/small',
},
keywords: ['zabbix', 'monitoring', 'dashboard'],
},
error: undefined,
isCore: false,
@ -266,6 +267,7 @@ describe('Plugins/Helpers', () => {
small: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/small',
large: 'https://grafana.com/api/plugins/alexanderzobnin-zabbix-app/versions/4.1.5/logos/large',
},
keywords: ['zabbix', 'monitoring', 'dashboard'],
},
error: undefined,
isCore: false,

View File

@ -1,3 +1,5 @@
import uFuzzy from '@leeoniya/ufuzzy';
import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data';
import { config, featureEnabled } from '@grafana/runtime';
import configCore, { Settings } from 'app/core/config';
@ -86,6 +88,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
createdAt: publishedAt,
status,
angularDetected,
keywords,
} = plugin;
const isDisabled = !!error || isDisabledSecretsPlugin(typeCode);
@ -98,6 +101,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
small: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/small`,
large: `https://grafana.com/api/plugins/${id}/versions/${version}/logos/large`,
},
keywords,
},
name,
orgName,
@ -123,7 +127,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): CatalogPlugin {
const {
name,
info: { description, version, logos, updated, author },
info: { description, version, logos, updated, author, keywords },
id,
dev,
type,
@ -140,7 +144,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
description,
downloads: 0,
id,
info: { logos },
info: { logos, keywords },
name,
orgName: author.name,
popularity: 0,
@ -173,6 +177,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
const id = remote?.slug || local?.id || '';
const type = local?.type || remote?.typeCode;
const isDisabled = !!error || isDisabledSecretsPlugin(type);
const keywords = remote?.keywords || local?.info.keywords || [];
let logos = {
small: `/public/img/icn-${type}.svg`,
@ -195,6 +200,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
id,
info: {
logos,
keywords,
},
isCore: Boolean(remote?.internal || local?.signature === PluginSignatureStatus.internal),
isDev: Boolean(local?.dev),
@ -345,3 +351,26 @@ function isDisabledSecretsPlugin(type?: PluginType): boolean {
export function isLocalCorePlugin(local?: LocalPlugin): boolean {
return Boolean(local?.signature === 'internal');
}
function getId(inputString: string): string {
const parts = inputString.split(' - ');
return parts[0];
}
function getPluginDetailsForFuzzySearch(plugins: CatalogPlugin[]): string[] {
return plugins.reduce((result: string[], { id, name, type, orgName, info }: CatalogPlugin) => {
const keywordsForSearch = info.keywords?.join(' ').toLowerCase();
const pluginString = `${id} - ${name} - ${type} - ${orgName} - ${keywordsForSearch}`;
result.push(pluginString);
return result;
}, []);
}
export function filterByKeyword(plugins: CatalogPlugin[], query: string) {
const dataArray = getPluginDetailsForFuzzySearch(plugins);
let uf = new uFuzzy({});
let idxs = uf.filter(dataArray, query);
if (idxs === null) {
return null;
}
return idxs.map((id) => getId(dataArray[id]));
}

View File

@ -183,13 +183,19 @@ describe('Browse list of plugins', () => {
describe('when searching', () => {
it('should only list plugins matching search', async () => {
const { queryByText } = renderBrowse('/plugins?filterBy=all&q=zabbix', [
getCatalogPluginMock({ id: 'zabbix', name: 'Zabbix' }),
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2' }),
getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3' }),
const { queryByText } = renderBrowse('/plugins?filterBy=all&q=matches', [
getCatalogPluginMock({ id: 'matches-the-search', name: 'Matches the search' }),
getCatalogPluginMock({
id: 'plugin-2',
name: 'Plugin 2',
}),
getCatalogPluginMock({
id: 'plugin-3',
name: 'Plugin 3',
}),
]);
await waitFor(() => expect(queryByText('Zabbix')).toBeInTheDocument());
await waitFor(() => expect(queryByText('Matches the search')).toBeInTheDocument());
// Other plugin types shouldn't be shown
expect(queryByText('Plugin 2')).not.toBeInTheDocument();

View File

@ -39,6 +39,7 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
},
sortBy
);
const filterByOptions = [
{ value: 'all', label: 'All' },
{ value: 'installed', label: 'Installed' },

View File

@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { PluginError, PluginType, unEscapeStringFromRegex } from '@grafana/data';
import { filterByKeyword } from '../helpers';
import { RequestStatus, PluginCatalogStoreState } from '../types';
import { pluginsAdapter } from './reducer';
@ -32,11 +33,14 @@ export type PluginFilters = {
export const selectPlugins = (filters: PluginFilters) =>
createSelector(selectAll, (plugins) => {
const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';
const filteredPluginIds = keyword !== '' ? filterByKeyword(plugins, keyword) : null;
return plugins.filter((plugin) => {
const fieldsToSearchIn = [plugin.name, plugin.orgName].filter(Boolean).map((f) => f.toLowerCase());
if (keyword && filteredPluginIds == null) {
return false;
}
if (keyword && !fieldsToSearchIn.some((f) => f.includes(keyword))) {
if (keyword && !filteredPluginIds?.includes(plugin.id)) {
return false;
}

View File

@ -83,6 +83,7 @@ export interface CatalogPluginInfo {
large: string;
small: string;
};
keywords: string[];
}
export type RemotePlugin = {
@ -93,6 +94,7 @@ export type RemotePlugin = {
featured: number;
id: number;
internal: boolean;
keywords: string[];
json?: {
dependencies: PluginDependencies;
iam?: IdentityAccessManagement;
@ -161,6 +163,7 @@ export type LocalPlugin = WithAccessControlMetadata & {
small: string;
large: string;
};
keywords: string[];
build: Build;
screenshots?: Array<{
path: string;